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:
@@ -478,19 +478,34 @@ export function getProblemSetList(
|
||||
difficulty = "",
|
||||
status = "",
|
||||
) {
|
||||
return http.get("admin/problemset", {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
difficulty,
|
||||
status,
|
||||
},
|
||||
})
|
||||
return legacyResponse(
|
||||
api2.get("admin/problem-sets", {
|
||||
params: { offset, limit, keyword, difficulty, status },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getProblemSetDetail(id: number) {
|
||||
return http.get(`admin/problemset/${id}`)
|
||||
return legacyResponse(api2.get(`admin/problem-sets/${id}`))
|
||||
}
|
||||
|
||||
/** 组件传的是 snake_case,出站转成新后端要的 camelCase */
|
||||
function toProblemSetBody(data: {
|
||||
title?: string
|
||||
description?: string
|
||||
difficulty?: string
|
||||
status?: string
|
||||
end_time?: Date | null
|
||||
visible?: boolean
|
||||
}) {
|
||||
return {
|
||||
title: data.title,
|
||||
description: data.description ?? "",
|
||||
difficulty: data.difficulty ?? "Easy",
|
||||
status: data.status ?? "active",
|
||||
endTime: data.end_time ? new Date(data.end_time).toISOString() : null,
|
||||
visible: data.visible ?? true,
|
||||
}
|
||||
}
|
||||
|
||||
export function createProblemSet(data: {
|
||||
@@ -500,7 +515,7 @@ export function createProblemSet(data: {
|
||||
status: string
|
||||
end_time?: Date | null
|
||||
}) {
|
||||
return http.post("admin/problemset", data)
|
||||
return legacyResponse(api2.post("admin/problem-sets", toProblemSetBody(data)))
|
||||
}
|
||||
|
||||
export function editProblemSet(data: {
|
||||
@@ -512,24 +527,26 @@ export function editProblemSet(data: {
|
||||
end_time?: Date | null
|
||||
visible?: boolean
|
||||
}) {
|
||||
return http.put("admin/problemset", data)
|
||||
return legacyResponse(
|
||||
api2.put(`admin/problem-sets/${data.id}`, toProblemSetBody(data)),
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteProblemSet(id: number) {
|
||||
return http.delete("admin/problemset", { params: { id } })
|
||||
return api2.delete(`admin/problem-sets/${id}`)
|
||||
}
|
||||
|
||||
export function toggleProblemSetVisible(id: number) {
|
||||
return http.put("admin/problemset/visible", { id })
|
||||
return legacyResponse(api2.put(`admin/problem-sets/${id}/visibility`))
|
||||
}
|
||||
|
||||
export function updateProblemSetStatus(id: number, status: string) {
|
||||
return http.put("admin/problemset/status", { id, status })
|
||||
return legacyResponse(api2.put(`admin/problem-sets/${id}/status`, { status }))
|
||||
}
|
||||
|
||||
// 题单题目管理 API
|
||||
export function getProblemSetProblems(problemSetId: number) {
|
||||
return http.get(`admin/problemset/${problemSetId}/problems`)
|
||||
return legacyResponse(api2.get(`admin/problem-sets/${problemSetId}/problems`))
|
||||
}
|
||||
|
||||
export function addProblemToSet(
|
||||
@@ -542,7 +559,13 @@ export function addProblemToSet(
|
||||
hint?: string
|
||||
},
|
||||
) {
|
||||
return http.post(`admin/problemset/${problemSetId}/problems`, data)
|
||||
return api2.post(`admin/problem-sets/${problemSetId}/problems`, {
|
||||
problemId: data.problem_id,
|
||||
order: data.order ?? 0,
|
||||
isRequired: data.is_required ?? true,
|
||||
score: data.score ?? 0,
|
||||
hint: data.hint ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
export function editProblemInSet(
|
||||
@@ -555,9 +578,14 @@ export function editProblemInSet(
|
||||
hint?: string
|
||||
},
|
||||
) {
|
||||
return http.put(
|
||||
`admin/problemset/${problemSetId}/problems/${problemSetProblemId}`,
|
||||
data,
|
||||
return api2.put(
|
||||
`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`,
|
||||
{
|
||||
order: data.order,
|
||||
isRequired: data.is_required,
|
||||
score: data.score,
|
||||
hint: data.hint,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -565,14 +593,30 @@ export function removeProblemFromSet(
|
||||
problemSetId: number,
|
||||
problemSetProblemId: number,
|
||||
) {
|
||||
return http.delete(
|
||||
`admin/problemset/${problemSetId}/problems/${problemSetProblemId}`,
|
||||
return api2.delete(
|
||||
`admin/problem-sets/${problemSetId}/problems/${problemSetProblemId}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 题单奖章管理 API
|
||||
export function getProblemSetBadges(problemSetId: number) {
|
||||
return http.get(`admin/problemset/${problemSetId}/badges`)
|
||||
return legacyResponse(api2.get(`admin/problem-sets/${problemSetId}/badges`))
|
||||
}
|
||||
|
||||
function toBadgeBody(data: {
|
||||
name?: string
|
||||
description?: string
|
||||
icon?: string
|
||||
condition_type?: string
|
||||
condition_value?: number
|
||||
}) {
|
||||
return {
|
||||
name: data.name,
|
||||
description: data.description ?? "",
|
||||
icon: data.icon ?? "",
|
||||
conditionType: data.condition_type,
|
||||
conditionValue: data.condition_value ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function createProblemSetBadge(
|
||||
@@ -586,7 +630,9 @@ export function createProblemSetBadge(
|
||||
level?: number
|
||||
},
|
||||
) {
|
||||
return http.post(`admin/problemset/${problemSetId}/badges`, data)
|
||||
return legacyResponse(
|
||||
api2.post(`admin/problem-sets/${problemSetId}/badges`, toBadgeBody(data)),
|
||||
)
|
||||
}
|
||||
|
||||
export function editProblemSetBadge(
|
||||
@@ -601,20 +647,25 @@ export function editProblemSetBadge(
|
||||
level?: number
|
||||
},
|
||||
) {
|
||||
return http.put(`admin/problemset/${problemSetId}/badges/${badgeId}`, data)
|
||||
return legacyResponse(
|
||||
api2.put(
|
||||
`admin/problem-sets/${problemSetId}/badges/${badgeId}`,
|
||||
toBadgeBody(data),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteProblemSetBadge(problemSetId: number, badgeId: number) {
|
||||
return http.delete(`admin/problemset/${problemSetId}/badges/${badgeId}`)
|
||||
return api2.delete(`admin/problem-sets/${problemSetId}/badges/${badgeId}`)
|
||||
}
|
||||
|
||||
// 题单进度管理 API
|
||||
export function getProblemSetProgress(problemSetId: number) {
|
||||
return http.get(`admin/problemset/${problemSetId}/progress`)
|
||||
return legacyResponse(api2.get(`admin/problem-sets/${problemSetId}/progress`))
|
||||
}
|
||||
|
||||
export function removeUserFromProblemSet(problemSetId: number, userId: number) {
|
||||
return http.delete(`admin/problemset/${problemSetId}/progress/${userId}`)
|
||||
return api2.delete(`admin/problem-sets/${problemSetId}/progress/${userId}`)
|
||||
}
|
||||
|
||||
// 学生卡点分析
|
||||
|
||||
Reference in New Issue
Block a user