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:
@@ -178,9 +178,9 @@ export function deleteUsers(userIDs: number[]) {
|
||||
}
|
||||
|
||||
export function getContestList(offset = 0, limit = 10, keyword: string) {
|
||||
return http.get("admin/contest", {
|
||||
params: { paging: true, offset, limit, keyword },
|
||||
})
|
||||
return legacyResponse(
|
||||
api2.get("admin/contests", { params: { offset, limit, keyword } }),
|
||||
)
|
||||
}
|
||||
|
||||
// 上传图片
|
||||
@@ -242,22 +242,41 @@ export function createContestProblem(problem: BlankProblem) {
|
||||
return http.post("admin/contest/problem", problem)
|
||||
}
|
||||
|
||||
/** 组件里的比赛对象是 snake_case,出站转成新后端要的 camelCase */
|
||||
function toContestBody(contest: Contest | BlankContest) {
|
||||
return {
|
||||
title: contest.title,
|
||||
description: contest.description,
|
||||
tag: contest.tag,
|
||||
startTime: contest.start_time,
|
||||
endTime: contest.end_time,
|
||||
password: contest.password || null,
|
||||
visible: contest.visible,
|
||||
allowedIpRanges: contest.allowed_ip_ranges ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
export function createContest(contest: BlankContest) {
|
||||
return http.post("admin/contest", contest)
|
||||
return legacyResponse(api2.post("admin/contests", toContestBody(contest)))
|
||||
}
|
||||
|
||||
export function editContest(contest: Contest | BlankContest) {
|
||||
return http.put("admin/contest", contest)
|
||||
return legacyResponse(
|
||||
api2.put(
|
||||
`admin/contests/${(contest as Contest).id}`,
|
||||
toContestBody(contest),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function cloneContest(contest_id: number) {
|
||||
return http.post("admin/contest/clone", { contest_id })
|
||||
return legacyResponse(api2.post(`admin/contests/${contest_id}/clone`))
|
||||
}
|
||||
|
||||
export function getContest(id: string) {
|
||||
return http.get<Contest & { password: string }>("admin/contest", {
|
||||
params: { id },
|
||||
})
|
||||
return legacyResponse<Contest & { password: string }>(
|
||||
api2.get(`admin/contests/${id}`),
|
||||
)
|
||||
}
|
||||
|
||||
export function addProblemForContest(
|
||||
@@ -435,9 +454,7 @@ export function makeProblemPublic(id: number, display_id: string) {
|
||||
|
||||
// 比赛辅助检查
|
||||
export function getACMHelperList(contest_id: number) {
|
||||
return http.get("admin/contest/acm_helper", {
|
||||
params: { contest_id },
|
||||
})
|
||||
return legacyResponse(api2.get(`admin/contests/${contest_id}/acm-helper`))
|
||||
}
|
||||
|
||||
export function updateACMHelperChecked(
|
||||
@@ -446,10 +463,9 @@ export function updateACMHelperChecked(
|
||||
problem_id: string,
|
||||
checked: boolean,
|
||||
) {
|
||||
return http.put("admin/contest/acm_helper", {
|
||||
contest_id,
|
||||
rank_id,
|
||||
problem_id,
|
||||
return api2.put(`admin/contests/${contest_id}/acm-helper`, {
|
||||
rankId: rank_id,
|
||||
problemId: problem_id,
|
||||
checked,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user