feat(阶段4): 标签管理 / 批量打标签 / 题目可见性 / 卡点与 AC 趋势 / 流程图 AI
GET/PUT/DELETE admin/problem-tags[/:id] POST admin/problems/batch-tag PUT admin/problems/:id/visibility GET admin/problems/stuck GET admin/problems/ac-trend POST admin/problems/flowchart 要点: - 后台标签列表用 leftJoin 且不加 having,能看到 problemCount=0 的标签 —— 那正是要清理的那些。oj 侧的 /problem-tags 才过滤 >0。 - 标签改名撞上已有标签视为合并:只给「还没挂目标标签」的题目补关系, 否则会撞 (problem_id, problemtag_id) 唯一约束。 - 批量打标签:add 时按需新建标签、remove 时只认已有标签 —— 否则「移除」会顺手造出一堆空标签。名字去重且大小写不敏感。 - **旧 ProblemVisibleAPI 的 `self.error(...)` 少写了 return**,题目不存在时会继续 往下跑并抛 AttributeError(500)。这里正常返回 404。 顺带记下一条阶段 5 的必做项(见 phase3-coverage.md 文末):本地库按显式 id 从生产 导入,序列没跟着走,第一次新建标签就撞 problem_tag_pkey。只要切换流程里有 「导出→导入到新库」这一步,就必须重置全部序列,否则读全正常、第一次写才炸。 实测:学生 403;大小写重复与空白名去重后 tagCount=1、重复 add 幂等、 remove 不存在的标签 404 且不新建;纯改名 merged=false、合并 merged=true affectedCount=1 且只剩一个标签;可见性取反两次复原、不存在的题 404。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,11 +72,11 @@ export function editProblem(problem: AdminProblem | BlankProblem) {
|
||||
}
|
||||
|
||||
export function toggleProblemVisible(problemID: number) {
|
||||
return http.put("admin/problem/visible", { id: problemID })
|
||||
return legacyResponse(api2.put(`admin/problems/${problemID}/visibility`))
|
||||
}
|
||||
|
||||
export function generateFlowchartFromPythonCode(python: string) {
|
||||
return http.post("admin/problem/flowchart", { python })
|
||||
return legacyResponse(api2.post("admin/problems/flowchart", { python }))
|
||||
}
|
||||
|
||||
export function editContestProblem(problem: AdminProblem | BlankProblem) {
|
||||
@@ -93,20 +93,22 @@ export function getContestProblem(id: number) {
|
||||
|
||||
// 标签管理
|
||||
export function getTagAdminList(keyword = "") {
|
||||
return http.get<AdminTag[]>("admin/problem/tag", { params: { keyword } })
|
||||
return legacyResponse<AdminTag[]>(
|
||||
api2.get("admin/problem-tags", { params: { keyword } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function renameTag(id: number, name: string) {
|
||||
return http.put<{
|
||||
return legacyResponse<{
|
||||
merged: boolean
|
||||
id: number
|
||||
name: string
|
||||
affected_count: number
|
||||
}>("admin/problem/tag", { id, name })
|
||||
}>(api2.put(`admin/problem-tags/${id}`, { name }))
|
||||
}
|
||||
|
||||
export function deleteTag(id: number) {
|
||||
return http.delete("admin/problem/tag", { params: { id } })
|
||||
return api2.delete(`admin/problem-tags/${id}`)
|
||||
}
|
||||
|
||||
export function batchTagProblems(
|
||||
@@ -114,9 +116,12 @@ export function batchTagProblems(
|
||||
tagNames: string[],
|
||||
action: "add" | "remove",
|
||||
) {
|
||||
return http.post<{ problem_count: number; tag_count: number }>(
|
||||
"admin/problem/batch_tag",
|
||||
{ problem_ids: problemIds, tag_names: tagNames, action },
|
||||
return legacyResponse<{ problem_count: number; tag_count: number }>(
|
||||
api2.post("admin/problems/batch-tag", {
|
||||
problemIds,
|
||||
tagNames,
|
||||
action,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -670,7 +675,7 @@ export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
||||
|
||||
// 学生卡点分析
|
||||
export function getStuckProblems() {
|
||||
return http.get("admin/problem/stuck")
|
||||
return legacyResponse(api2.get("admin/problems/stuck"))
|
||||
}
|
||||
|
||||
export function getTopACTrend(params: {
|
||||
@@ -678,7 +683,15 @@ export function getTopACTrend(params: {
|
||||
until_year: number
|
||||
min_per_year: number
|
||||
}) {
|
||||
return http.get("admin/problem/top_ac_trend", { params })
|
||||
return legacyResponse(
|
||||
api2.get("admin/problems/ac-trend", {
|
||||
params: {
|
||||
sinceYear: params.since_year,
|
||||
untilYear: params.until_year,
|
||||
minPerYear: params.min_per_year,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// AI 学习分析报告
|
||||
|
||||
Reference in New Issue
Block a user