feat(阶段4): 题目管理 / 比赛题目 / 比赛题⇄公开题互转
GET/POST admin/problems GET/PUT/DELETE admin/problems/:id POST admin/problems/:id/make-public GET/POST admin/contests/:contestId/problems POST admin/contests/:contestId/problems/from-public 合并了旧后端拆开的两套路由:旧 admin/problem 与 admin/contest/problem 的 GET 详情、PUT、DELETE 都只按题目 id 取、比赛是从题目推导出来的,分开没有意义, 还逼前端多传一个它未必知道的 contestId。现在共用 /admin/problems/:id。 归属判断按旧后端的口径分开:**比赛题看比赛的创建者,公开题看题目自己的创建者** (旧 ensure_created_by(problem.contest, user) vs ensure_created_by(problem, user))。 一道比赛题的 created_by 可能是克隆时的操作人,跟谁有权改它没关系。 题号唯一性的作用域也跟着走:公开题在全部公开题里唯一,比赛题在本场比赛内唯一。 删题不删测试用例目录,与旧后端一致(它把 rmtree 注释掉了):删错了还能从磁盘捞回来, 而误删的测试数据没有别处备份;孤儿目录另有清理入口。有提交记录的题目拒绝删除。 ## SQL 题目前只能编辑、不能新建 新后端**没有 SQL 判题链路** —— 旧 judge/sql_runner.py(378 行)+ sql_dispatcher.py (113 行)无对应实现,languages.ts 里也只有 C/C++/Java/Golang/JavaScript/Python3 六种。 题目页给学生看的表结构与期望结果(sql_display)要跑 SQLite 现算,算不出来就没法建题。 因此:新建 SQL 题返回 501 并说明原因;**编辑已有 SQL 题时原样保留 sqlDisplay 不动** —— 不动它比生成一个错的安全,它直接决定学生看到的期望结果。SQL 的前置校验 (必须是唯一语言、要有 sql_config、要有 SQL 标准答案)照旧执行。 实测:学生 403;缺样例/缺输入描述/SQL 混语言三条校验文案与旧后端一致; 题号重复 409、跨比赛题号互不影响;公开题加入比赛后计数归零且标签带过来; 比赛题转公开后 visible=false、contestId=null、重复转 409;删除级联正常。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -487,3 +487,102 @@ export const acTrendSchema = z.object({
|
||||
|
||||
export const generateFlowchartRequestSchema = z.object({ python: z.string().min(1).max(64 * 1024) })
|
||||
export const generateFlowchartResponseSchema = z.object({ flowchart: z.string() })
|
||||
|
||||
// ---------------------------------------------------------------- 题目管理
|
||||
|
||||
export const adminProblemListItemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
createdBy: sampleUserSchema,
|
||||
visible: z.boolean(),
|
||||
createTime: z.string(),
|
||||
difficulty: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
hasAstRules: z.boolean(),
|
||||
allowFlowchart: z.boolean(),
|
||||
showFlowchart: z.boolean(),
|
||||
topReaction: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema)
|
||||
|
||||
/** 后台题目详情:包含 oj 侧永不下发的 answers / testCase* / astRules */
|
||||
export const adminProblemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
inputDescription: z.string(),
|
||||
outputDescription: z.string(),
|
||||
samples: z.array(z.record(z.string(), z.unknown())),
|
||||
testCaseId: z.string(),
|
||||
testCaseScore: z.array(z.record(z.string(), z.unknown())),
|
||||
hint: z.string().nullable(),
|
||||
languages: z.array(z.string()),
|
||||
template: z.record(z.string(), z.string()),
|
||||
createTime: z.string(),
|
||||
lastUpdateTime: z.string(),
|
||||
timeLimit: z.number().int(),
|
||||
memoryLimit: z.number().int(),
|
||||
visible: z.boolean(),
|
||||
difficulty: z.string(),
|
||||
source: z.string().nullable(),
|
||||
submissionNumber: z.number().int(),
|
||||
acceptedNumber: z.number().int(),
|
||||
statisticInfo: z.record(z.string(), z.unknown()),
|
||||
shareSubmission: z.boolean(),
|
||||
contestId: z.number().int().nullable(),
|
||||
createdBy: sampleUserSchema,
|
||||
isPublic: z.boolean(),
|
||||
tags: z.array(z.string()),
|
||||
allowFlowchart: z.boolean(),
|
||||
showFlowchart: z.boolean(),
|
||||
mermaidCode: z.string().nullable(),
|
||||
flowchartHint: z.string().nullable(),
|
||||
astRules: z.unknown(),
|
||||
answers: z.array(z.record(z.string(), z.unknown())),
|
||||
prompt: z.string().nullable(),
|
||||
sqlConfig: z.record(z.string(), z.unknown()).nullable(),
|
||||
sqlDisplay: z.record(z.string(), z.unknown()).nullable(),
|
||||
})
|
||||
|
||||
export const createProblemRequestSchema = z.object({
|
||||
_id: z.string().trim().min(1).max(32),
|
||||
title: z.string().trim().min(1).max(1024),
|
||||
description: z.string(),
|
||||
inputDescription: z.string(),
|
||||
outputDescription: z.string(),
|
||||
samples: z.array(z.record(z.string(), z.unknown())),
|
||||
testCaseId: z.string().regex(/^[a-zA-Z0-9]+$/).max(32),
|
||||
testCaseScore: z.array(z.record(z.string(), z.unknown())),
|
||||
timeLimit: z.number().int().min(1).max(1000 * 60),
|
||||
memoryLimit: z.number().int().min(1).max(1024),
|
||||
languages: z.array(z.string()).min(1),
|
||||
template: z.record(z.string(), z.string()),
|
||||
visible: z.boolean(),
|
||||
difficulty: z.enum(["Low", "Mid", "High"]),
|
||||
tags: z.array(z.string().max(32)).min(1),
|
||||
hint: z.string().nullable().default(null),
|
||||
source: z.string().max(256).nullable().default(null),
|
||||
prompt: z.string().nullable().default(null),
|
||||
answers: z.array(z.record(z.string(), z.unknown())).default([]),
|
||||
shareSubmission: z.boolean(),
|
||||
allowFlowchart: z.boolean().default(false),
|
||||
showFlowchart: z.boolean().default(false),
|
||||
mermaidCode: z.string().nullable().default(null),
|
||||
flowchartHint: z.string().nullable().default(null),
|
||||
astRules: z.unknown().default(null),
|
||||
sqlConfig: z.record(z.string(), z.unknown()).nullable().default(null),
|
||||
})
|
||||
|
||||
export const updateProblemRequestSchema = createProblemRequestSchema
|
||||
|
||||
export const makeProblemPublicRequestSchema = z.object({
|
||||
displayId: z.string().trim().min(1).max(32),
|
||||
})
|
||||
|
||||
export const addContestProblemRequestSchema = z.object({
|
||||
problemId: z.number().int().positive(),
|
||||
displayId: z.string().trim().min(1).max(32),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user