feat(阶段4): 比赛管理 + 克隆 + ACM 赛后核查
GET/POST admin/contests GET/PUT admin/contests/:id POST admin/contests/:id/clone GET/PUT admin/contests/:id/acm-helper 克隆是深拷贝:新比赛从 10 分钟后开始、时长与原比赛相同、一律不可见(时间是拍脑袋定的, 直接开放会让学生看到一场没准备好的赛),比赛题目连同标签一起复制, 提交数/通过数/statistic_info 归零 —— 克隆的是题面不是历史战绩。 实测:源题 99/55 两个标签,克隆出来 0/0 两个标签俱在。 两处比旧后端更严: - **ACM 核查的 rank 必须属于本场比赛**。旧后端只按 rank_id 取,不校验归属, 带上任意 rank_id 就能改别的比赛的核查标记。 - 比赛详情/编辑越权时报「不存在」而不是「无权限」,不泄露「有这么个东西但你看不到」。 其余对齐:非超管只看得到自己建的比赛;空串密码归一成 null(否则 contestType 会把 「密码是空字符串」当成密码保护赛);CIDR 按 ip_network(strict=False) 的口径校验, 允许主机位非零。 核查页的 realName 是**有意下发**的:这个页面就是老师对着名单确认谁抄了, 接口已由 requireTeacher + 归属校验双重把关。 实测:学生 403;结束早于开始 400、非法 CIDR 400 且文案带具体网段;创建空串密码 → Public、改密码后 → Password Protected 且后台能读到密码原文;克隆时长一致且不可见; 不可见比赛的核查页 404;rank 不属于本场 404。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -280,3 +280,55 @@ export const uploadImageResponseSchema = z.object({
|
||||
msg: z.string(),
|
||||
filePath: z.string(),
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 比赛管理
|
||||
|
||||
export const adminContestSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
tag: z.string(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
createTime: z.string(),
|
||||
lastUpdateTime: z.string(),
|
||||
// 后台要能看到自己设的密码(用来告诉学生),oj 侧的 contestSchema 则永远不含它
|
||||
password: z.string().nullable(),
|
||||
visible: z.boolean(),
|
||||
allowedIpRanges: z.array(z.string()),
|
||||
createdBy: sampleUserSchema,
|
||||
status: z.enum(["1", "0", "-1"]),
|
||||
contestType: z.enum(["Public", "Password Protected"]),
|
||||
})
|
||||
|
||||
export const adminContestListSchema = paginatedSchema(adminContestSchema)
|
||||
|
||||
export const createContestRequestSchema = z.object({
|
||||
title: z.string().trim().min(1).max(128),
|
||||
description: z.string(),
|
||||
tag: z.string().max(64),
|
||||
startTime: z.string().min(1),
|
||||
endTime: z.string().min(1),
|
||||
// 空串等同于「不设密码」,与旧 CreateConetestSeriaizer 的 allow_blank 一致
|
||||
password: z.string().max(32).nullable().default(null),
|
||||
visible: z.boolean(),
|
||||
allowedIpRanges: z.array(z.string().max(32)).default([]),
|
||||
})
|
||||
|
||||
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),
|
||||
checked: z.boolean(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user