feat(比赛): 榜单一次给全,带名次变化和每题谁最先做对;期中、期末考试中学生看不到排名;老师能看全班情况

- GET /contests/:id/scoreboard 代替 /rank:整张榜、5 分钟前的名次、每题做对 / 做过人数和学生里最先做对的人(不再用 is_first_ac,老师赛中试做会占掉它)
- 期中、期末进行中:学生只拿到自己那一行,人数清零、「全班最先」也不给,考完一起公布
- GET /contests/:id/class-view(老师):真名、每人最后一次交、没进来的人 —— 比赛不绑班,来了一半以上的班才算这场的班
- 比赛列表带题数、参赛人数、我的名次和做对数,能筛「我参加过的」「期中 · 期末」
- PUT /admin/contests/:id/problems/order:比赛题按顺序重编 1、2、3,比赛结束后不让动
- 后台「审核」列表接口去掉,并进前台老师的成绩页;「看过了」的写接口留着

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 20:58:59 +08:00
1 parent 4ef2794552
commit b94e028ff4
10 files changed
+528 -740

No files matched your search

+5 -11
View File
@@ -350,16 +350,6 @@ export const createContestRequestSchema = z.object({
export const updateContestRequestSchema = createContestRequestSchema
export const acmHelperItemSchema = z.object({
id: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
problemId: z.string(),
problemDisplayId: z.string(),
acInfo: z.record(z.string(), z.unknown()),
checked: z.boolean(),
})
export const updateAcmHelperRequestSchema = z.object({
rankId: z.number().int().positive(),
problemId: z.string().min(1),
@@ -788,6 +778,11 @@ export const addContestProblemRequestSchema = z.object({
displayId: z.string().trim().min(1).max(32),
})
/** 比赛题排顺序:给出这场比赛**全部**题目的 id,按这个顺序把编号改成 1、2、3… */
export const reorderContestProblemsRequestSchema = z.object({
problemIds: z.array(z.number().int().positive()).min(1).max(100),
})
export const testCaseEntrySchema = z.object({
stripped_output_md5: z.string(),
input_size: z.number().int(),
@@ -901,7 +896,6 @@ export type CreateProblemRequest = z.infer<typeof createProblemRequestSchema>
export type AdminContest = z.infer<typeof adminContestSchema>
export type AdminContestList = z.infer<typeof adminContestListSchema>
export type AcmHelperItem = z.infer<typeof acmHelperItemSchema>
export type AdminUser = z.infer<typeof adminUserSchema>
export type AdminUserList = z.infer<typeof adminUserListSchema>
export type JudgeServer = z.infer<typeof judgeServerSchema>
+94 -11
View File
@@ -18,6 +18,21 @@ export const contestSchema = z.object({
status: contestStatusSchema,
contestType: z.enum(["Public", "Password Protected"]),
now: z.string().optional(),
/** 列表才带:几道题(可见的)、几个学生交过题 */
problemCount: z.number().int().optional(),
participantCount: z.number().int().optional(),
/**
* 列表才带,登录了才有:我在这场里的成绩,没参加是 null。
* `rank` 在期中 / 期末进行中是 null —— 那时排名不公布(见 {@link isExamTag})
*/
mine: z
.object({
rank: z.number().int().nullable(),
total: z.number().int(),
solved: z.number().int(),
})
.nullable()
.optional(),
})
export const contestListSchema = paginatedSchema(contestSchema)
@@ -49,25 +64,93 @@ export const contestSubmissionInfoSchema = z.object({
checked: z.boolean().optional(),
})
export const contestRankItemSchema = z.object({
id: z.number().int(),
user: sampleUserSchema,
submissionNumber: z.number().int(),
acceptedNumber: z.number().int(),
totalTime: z.number().int(),
submissionInfo: z.record(z.string(), contestSubmissionInfoSchema),
contestId: z.number().int(),
/**
* 期中、期末:进行中学生看不到排名、每题做对人数、谁最先做对,考完一起公布(2026-10 用户定的)。
* 练习照常全看得到 —— 「我需要竞争」。标签只有 练习 / 期中 / 期末 三个值
*/
export const EXAM_TAGS = ["期中", "期末"] as const
export function isExamTag(tag: string) {
return (EXAM_TAGS as readonly string[]).includes(tag)
}
/** 榜单上一个人一道题的格子。时间都是秒,从比赛开始算 */
export const contestScoreCellSchema = z.object({
isAc: z.boolean(),
acTime: z.number(),
/** 做对的题:做对前错了几次(编译失败不算);没做对的题:到现在错了几次 */
errors: z.number().int(),
/** 学生里最先做对这题的(同一秒并列都算)。不用库里的 is_first_ac:老师赛中试做也会占掉它 */
firstAc: z.boolean(),
/** 老师在成绩页标的「看过了」 */
checked: z.boolean(),
})
export const contestRankSchema = paginatedSchema(contestRankItemSchema)
export const contestScoreRowSchema = z.object({
/** acm_contest_rank.id,标「看过了」要用 */
rankId: z.number().int(),
userId: z.number().int(),
username: z.string(),
/** 只给老师 */
realName: z.string().nullable(),
className: z.string().nullable(),
/** 名次(1 起)。排名不公布时自己那一行是 null */
rank: z.number().int().nullable(),
/** 5 分钟前的名次,比赛结束后是 null(名次不再变) */
prevRank: z.number().int().nullable(),
solved: z.number().int(),
/** 罚时,秒:每道做对的题做对时是第几秒,加起来,错一次多 20 分钟 */
totalTime: z.number().int(),
/** 题目 id → 格子,只有交过的题有 */
cells: z.record(z.string(), contestScoreCellSchema),
})
export const contestScoreProblemSchema = z.object({
id: z.number().int(),
_id: z.string(),
title: z.string(),
solvedUsers: z.number().int(),
triedUsers: z.number().int(),
firstSolver: z.object({ username: z.string(), acTime: z.number() }).nullable(),
})
/**
* 比赛的整张榜,一次全给(一场最多百来人):学生比赛页右边的名次、排名页、老师的全班情况都用它。
* `hidden` = 期中期末进行中、看的人是学生:problems 的人数清零,rows 只有自己那一行
*/
export const contestScoreboardSchema = z.object({
hidden: z.boolean(),
problems: z.array(contestScoreProblemSchema),
rows: z.array(contestScoreRowSchema),
})
/**
* 老师看的「全班情况 / 成绩」:在榜单之外,多每个人最后一次交的时间,和没进来的人。
* 比赛不绑班:参赛的人里,一个班来了一半以上就算这场是这个班的,没进来的从这些班的名单里找
*/
export const contestClassViewSchema = contestScoreboardSchema.extend({
lastSubmit: z.record(z.string(), z.object({ time: z.string(), problemId: z.number().int() })),
classes: z.array(z.string()),
absent: z.array(
z.object({
userId: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
className: z.string().nullable(),
}),
),
})
export type Contest = z.infer<typeof contestSchema>
export type ContestList = z.infer<typeof contestListSchema>
export type ContestRankItem = z.infer<typeof contestRankItemSchema>
export type ContestRank = z.infer<typeof contestRankSchema>
export type ContestAccess = z.infer<typeof contestAccessSchema>
export type ContestSubmissionInfo = z.infer<typeof contestSubmissionInfoSchema>
export type ContestScoreCell = z.infer<typeof contestScoreCellSchema>
export type ContestScoreRow = z.infer<typeof contestScoreRowSchema>
export type ContestScoreProblem = z.infer<typeof contestScoreProblemSchema>
export type ContestScoreboard = z.infer<typeof contestScoreboardSchema>
export type ContestClassView = z.infer<typeof contestClassViewSchema>
export type ContestStatus = z.infer<typeof contestStatusSchema>
export type ContestPasswordRequest = z.infer<typeof contestPasswordRequestSchema>
export type ContestProblems = z.infer<typeof contestProblemsSchema>