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:
1 parent
4ef2794552
commit
b94e028ff4
10 files changed
+528
-740
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user