feat(阶段4): 题单管理(题目 / 奖章 / 进度三层)
GET/POST admin/problem-sets GET/PUT/DELETE admin/problem-sets/:id PUT admin/problem-sets/:id/visibility (取反语义,与旧一致) PUT admin/problem-sets/:id/status GET/POST admin/problem-sets/:id/problems PUT/DELETE admin/problem-sets/:id/problems/:itemId GET/POST admin/problem-sets/:id/badges PUT/DELETE admin/problem-sets/:id/badges/:badgeId GET admin/problem-sets/:id/progress DELETE admin/problem-sets/:id/progress/:userId 修掉旧后端三个问题: 1. **后台列表写死了 visible=True,可它同时又提供「切换可见性」的接口** —— 一旦把题单设成不可见,它就从后台列表消失,再也没法在界面上改回来。 新实现不按 visible 过滤,后台能看见自己管的全部题单。实测取反两次仍在列表里。 2. **加题/删题/改分值后不重算学生进度**。往题单里加一道题,学生那边的 totalProblemsCount 还是老数字,进度百分比因此偏高,甚至已「完成」的人分母变了 却还标着完成。新实现加了 resyncProgress,实测 2/2 加一道题后变成 2/3 = 66.67%。 只重算分母与百分比,不碰 completeTime —— 已完成过的事实不因加题而撤销。 3. 把人踢出题单时一并收回他基于这份题单拿到的奖章,否则奖章悬空。 奖章重算对齐旧 recalculate_user_badges(那边靠 post_save 信号,这里显式调): 只增删差集、保留已有记录的 earnedTime,否则每改一次条件所有人的获得时间都会 刷新成今天。实测 all_problems 建成时补发 1 人 → 加题后收回 → 改成 problem_count>=2 后重新发出 → 踢人后收回。 删题单要按序清五张子表(全是 NO ACTION 外键):user_badge 挂在 problemset_badge 上,得先于 badge 删。 实测:学生 403;重复加题 409、加不存在的题 404、野状态/野奖章条件 400; 级联删除后五张表全为 0。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -332,3 +332,104 @@ export const updateAcmHelperRequestSchema = z.object({
|
||||
problemId: z.string().min(1),
|
||||
checked: z.boolean(),
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 题单管理
|
||||
|
||||
export const problemSetDifficultySchema = z.enum(["Easy", "Medium", "Hard"])
|
||||
export const problemSetStatusSchema = z.enum(["draft", "active", "archived"])
|
||||
export const badgeConditionTypeSchema = z.enum(["all_problems", "problem_count", "score"])
|
||||
|
||||
export const adminProblemSetSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
difficulty: problemSetDifficultySchema,
|
||||
status: problemSetStatusSchema,
|
||||
endTime: z.string().nullable(),
|
||||
visible: z.boolean(),
|
||||
createdBy: sampleUserSchema,
|
||||
createTime: z.string(),
|
||||
lastUpdateTime: z.string(),
|
||||
problemsCount: z.number().int(),
|
||||
/** 加入这份题单的人数,后台用来判断改动会影响多少人 */
|
||||
participantCount: z.number().int(),
|
||||
})
|
||||
|
||||
export const adminProblemSetListSchema = paginatedSchema(adminProblemSetSchema)
|
||||
|
||||
export const createProblemSetRequestSchema = z.object({
|
||||
title: z.string().trim().min(1).max(200),
|
||||
description: z.string(),
|
||||
difficulty: problemSetDifficultySchema.default("Easy"),
|
||||
status: problemSetStatusSchema.default("active"),
|
||||
endTime: z.string().nullable().default(null),
|
||||
visible: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export const updateProblemSetRequestSchema = createProblemSetRequestSchema
|
||||
export const updateProblemSetStatusRequestSchema = z.object({ status: problemSetStatusSchema })
|
||||
|
||||
export const adminProblemSetProblemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
problemsetId: z.number().int(),
|
||||
problemId: z.number().int(),
|
||||
displayId: z.string(),
|
||||
title: z.string(),
|
||||
difficulty: z.string(),
|
||||
order: z.number().int(),
|
||||
isRequired: z.boolean(),
|
||||
score: z.number().int(),
|
||||
hint: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const addProblemToSetRequestSchema = z.object({
|
||||
/** 展示用题号(_id),不是自增主键 —— 老师手里只有题号 */
|
||||
problemId: z.string().trim().min(1),
|
||||
order: z.number().int().default(0),
|
||||
isRequired: z.boolean().default(true),
|
||||
score: z.number().int().default(0),
|
||||
hint: z.string().default(""),
|
||||
})
|
||||
|
||||
export const updateProblemInSetRequestSchema = z.object({
|
||||
order: z.number().int().optional(),
|
||||
isRequired: z.boolean().optional(),
|
||||
score: z.number().int().optional(),
|
||||
hint: z.string().optional(),
|
||||
})
|
||||
|
||||
export const adminProblemSetBadgeSchema = z.object({
|
||||
id: z.number().int(),
|
||||
problemsetId: z.number().int(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
conditionType: badgeConditionTypeSchema,
|
||||
conditionValue: z.number().int(),
|
||||
/** 已获得该奖章的人数,后台改条件前要能看到影响面 */
|
||||
earnedCount: z.number().int(),
|
||||
})
|
||||
|
||||
export const createProblemSetBadgeRequestSchema = z.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
conditionType: badgeConditionTypeSchema,
|
||||
conditionValue: z.number().int().default(0),
|
||||
})
|
||||
|
||||
export const updateProblemSetBadgeRequestSchema = createProblemSetBadgeRequestSchema
|
||||
|
||||
export const adminProblemSetProgressSchema = z.object({
|
||||
id: z.number().int(),
|
||||
userId: z.number().int(),
|
||||
username: z.string(),
|
||||
realName: z.string().nullable(),
|
||||
joinTime: z.string(),
|
||||
completeTime: z.string().nullable(),
|
||||
isCompleted: z.boolean(),
|
||||
progressPercentage: z.number(),
|
||||
completedProblemsCount: z.number().int(),
|
||||
totalProblemsCount: z.number().int(),
|
||||
totalScore: z.number().int(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user