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:
@@ -38,21 +38,13 @@ export async function getProblemList(
|
||||
contestID?: string,
|
||||
tagId?: number,
|
||||
) {
|
||||
const endpoint = !!contestID ? "admin/contest/problem" : "admin/problem"
|
||||
const res = await http.get<{ results: AdminProblem[]; total: number }>(
|
||||
endpoint,
|
||||
{
|
||||
params: {
|
||||
paging: true,
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
author,
|
||||
contest_id: contestID,
|
||||
tag_id: tagId,
|
||||
},
|
||||
},
|
||||
)
|
||||
const endpoint = contestID
|
||||
? `admin/contests/${contestID}/problems`
|
||||
: "admin/problems"
|
||||
const res = await legacyResponse<{
|
||||
results: AdminProblem[]
|
||||
total: number
|
||||
}>(api2.get(endpoint, { params: { offset, limit, keyword, author, tagId } }))
|
||||
return {
|
||||
results: res.data.results.map(toProblemListItem),
|
||||
total: res.data.total,
|
||||
@@ -60,15 +52,21 @@ export async function getProblemList(
|
||||
}
|
||||
|
||||
export function deleteProblem(id: number) {
|
||||
return http.delete("admin/problem", { params: { id } })
|
||||
return api2.delete(`admin/problems/${id}`)
|
||||
}
|
||||
|
||||
// 比赛题与公开题共用一条删除路由,比赛由后端从题目推导
|
||||
export function deleteContestProblem(id: number) {
|
||||
return http.delete("admin/contest/problem", { params: { id } })
|
||||
return api2.delete(`admin/problems/${id}`)
|
||||
}
|
||||
|
||||
export function editProblem(problem: AdminProblem | BlankProblem) {
|
||||
return http.put("admin/problem", problem)
|
||||
return legacyResponse(
|
||||
api2.put(
|
||||
`admin/problems/${(problem as AdminProblem).id}`,
|
||||
toProblemBody(problem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function toggleProblemVisible(problemID: number) {
|
||||
@@ -80,15 +78,20 @@ export function generateFlowchartFromPythonCode(python: string) {
|
||||
}
|
||||
|
||||
export function editContestProblem(problem: AdminProblem | BlankProblem) {
|
||||
return http.put("admin/contest/problem", problem)
|
||||
return legacyResponse(
|
||||
api2.put(
|
||||
`admin/problems/${(problem as AdminProblem).id}`,
|
||||
toProblemBody(problem),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function getProblem(id: string | number) {
|
||||
return http.get<AdminProblem>("admin/problem", { params: { id } })
|
||||
return legacyResponse<AdminProblem>(api2.get(`admin/problems/${id}`))
|
||||
}
|
||||
|
||||
export function getContestProblem(id: number) {
|
||||
return http.get("admin/contest/problem", { params: { id } })
|
||||
return legacyResponse<AdminProblem>(api2.get(`admin/problems/${id}`))
|
||||
}
|
||||
|
||||
// 标签管理
|
||||
@@ -239,12 +242,49 @@ export function generateSQLTestcase(data: {
|
||||
return http.post<{ sql: string }>("admin/sql_test_case_ai_gen", data)
|
||||
}
|
||||
|
||||
/** 组件里的题目对象是 snake_case,出站转成新后端要的 camelCase */
|
||||
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
||||
const p = problem as Record<string, any>
|
||||
return {
|
||||
_id: p._id,
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
inputDescription: p.input_description ?? "",
|
||||
outputDescription: p.output_description ?? "",
|
||||
samples: p.samples ?? [],
|
||||
testCaseId: p.test_case_id,
|
||||
testCaseScore: p.test_case_score ?? [],
|
||||
timeLimit: p.time_limit,
|
||||
memoryLimit: p.memory_limit,
|
||||
languages: p.languages ?? [],
|
||||
template: p.template ?? {},
|
||||
visible: p.visible,
|
||||
difficulty: p.difficulty,
|
||||
tags: p.tags ?? [],
|
||||
hint: p.hint ?? null,
|
||||
source: p.source ?? null,
|
||||
prompt: p.prompt ?? null,
|
||||
answers: p.answers ?? [],
|
||||
shareSubmission: p.share_submission ?? false,
|
||||
allowFlowchart: p.allow_flowchart ?? false,
|
||||
showFlowchart: p.show_flowchart ?? false,
|
||||
mermaidCode: p.mermaid_code ?? null,
|
||||
flowchartHint: p.flowchart_hint ?? null,
|
||||
astRules: p.ast_rules ?? null,
|
||||
sqlConfig: p.sql_config ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export function createProblem(problem: BlankProblem) {
|
||||
return http.post("admin/problem", problem)
|
||||
return legacyResponse(api2.post("admin/problems", toProblemBody(problem)))
|
||||
}
|
||||
|
||||
export function createContestProblem(problem: BlankProblem) {
|
||||
return http.post("admin/contest/problem", problem)
|
||||
// contest_id 由 detail.vue 在提交前写进 problem 对象
|
||||
const contestID = (problem as Record<string, any>).contest_id
|
||||
return legacyResponse(
|
||||
api2.post(`admin/contests/${contestID}/problems`, toProblemBody(problem)),
|
||||
)
|
||||
}
|
||||
|
||||
/** 组件里的比赛对象是 snake_case,出站转成新后端要的 camelCase */
|
||||
@@ -289,11 +329,12 @@ export function addProblemForContest(
|
||||
problemID: number,
|
||||
displayID: string,
|
||||
) {
|
||||
return http.post("admin/contest/add_problem_from_public", {
|
||||
contest_id: contestID,
|
||||
problem_id: problemID,
|
||||
display_id: displayID,
|
||||
})
|
||||
return legacyResponse(
|
||||
api2.post(`admin/contests/${contestID}/problems/from-public`, {
|
||||
problemId: problemID,
|
||||
displayId: displayID,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getWebsite() {
|
||||
@@ -451,10 +492,9 @@ export function deleteExercise(id: number) {
|
||||
|
||||
// 将竞赛题目转为公开题目
|
||||
export function makeProblemPublic(id: number, display_id: string) {
|
||||
return http.post("admin/contest_problem/make_public", {
|
||||
id,
|
||||
display_id,
|
||||
})
|
||||
return legacyResponse(
|
||||
api2.post(`admin/problems/${id}/make-public`, { displayId: display_id }),
|
||||
)
|
||||
}
|
||||
|
||||
// 比赛辅助检查
|
||||
|
||||
Reference in New Issue
Block a user