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:
@@ -433,3 +433,57 @@ export const adminProblemSetProgressSchema = z.object({
|
||||
totalProblemsCount: z.number().int(),
|
||||
totalScore: z.number().int(),
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 标签与题目分析
|
||||
|
||||
export const adminTagSchema = z.object({
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
problemCount: z.number().int(),
|
||||
})
|
||||
|
||||
export const renameTagRequestSchema = z.object({ name: z.string().trim().min(1).max(64) })
|
||||
|
||||
export const renameTagResponseSchema = z.object({
|
||||
/** 改名撞上已有标签时视为合并:题目关系转移过去、原标签删除 */
|
||||
merged: z.boolean(),
|
||||
id: z.number().int(),
|
||||
name: z.string(),
|
||||
affectedCount: z.number().int(),
|
||||
})
|
||||
|
||||
export const batchProblemTagRequestSchema = z.object({
|
||||
problemIds: z.array(z.number().int().positive()).min(1),
|
||||
tagNames: z.array(z.string()).min(1),
|
||||
action: z.enum(["add", "remove"]),
|
||||
})
|
||||
|
||||
export const batchProblemTagResponseSchema = z.object({
|
||||
problemCount: z.number().int(),
|
||||
tagCount: z.number().int(),
|
||||
})
|
||||
|
||||
export const stuckProblemSchema = z.object({
|
||||
problemId: z.string(),
|
||||
problemTitle: z.string(),
|
||||
total: z.number().int(),
|
||||
failed: z.number().int(),
|
||||
failedUsers: z.number().int(),
|
||||
acRate: z.number(),
|
||||
})
|
||||
|
||||
export const acTrendYearSchema = z.object({
|
||||
year: z.number().int(),
|
||||
total: z.number().int(),
|
||||
accepted: z.number().int(),
|
||||
acRate: z.number(),
|
||||
})
|
||||
|
||||
export const acTrendSchema = z.object({
|
||||
problemId: z.string(),
|
||||
problemTitle: z.string(),
|
||||
yearly: z.array(acTrendYearSchema),
|
||||
})
|
||||
|
||||
export const generateFlowchartRequestSchema = z.object({ python: z.string().min(1).max(64 * 1024) })
|
||||
export const generateFlowchartResponseSchema = z.object({ flowchart: z.string() })
|
||||
|
||||
Reference in New Issue
Block a user