fix(题单): complete_time 翻回「只设不清」;完成题单后那个必然报错的 tab
三件事,都是上一轮结论没查扎实留下的。
## complete_time
f9354b0 把「退回未完成时清空 complete_time」定成两边统一的行为,理由写的是
「快照里 complete_time 非空 ⟺ is_completed」。那个检查只做了一个方向(已完成但
没有完成时间 = 0 条),反方向没查 —— 实际有 4 条 is_completed=false 却留着
complete_time,全在题单 8:那批人 2025-11-14 在它还只有 6 题时完成过,老师后来
加到 12 题,进度退回未完成,完成时间保留了下来。
旧栈是故意不清的(problemset/models.py:218 只有 `if is_completed and not
complete_time` 这一条赋值),语义是「曾经完成于」。清空是重写时在学生路径上引入的,
上一版又把它推广到了后台路径。
翻回只设不清。「未完成 + 有完成时间」是允许的组合。反过来清空的代价不可逆:往一个
100 人已完成的题单里加一道题、再改主意删掉,这 100 个人的历史完成时间就一起被冲成
「现在」—— 上一轮的实跑已经在题单 9 上复现过。
## 两处注释订正
「旧后端不做加题后的重算」这个说法是错的,本仓早先的注释里有,上一版我照搬进了
services/problemset.ts 和提交信息。旧栈用 signals 做了,而且两件事都做:
problemset/signals.py 在 ProblemSetProblem 的 post_save / post_delete 上重算全部
参与者进度、再重算该题单全部奖章。重写时 views 里翻不到显式调用就当成没做,于是
奖章那一半漏了 —— 53 条应发未发正是这么来的。
另外补上 53 条里那 23 条的出处:旧栈的管理命令 fix_problemset_progress 按实际 AC
补 progress_detail,而 signals 不挂在 Progress 上,所以进度补了、奖章没补。
## 用户进度 tab
detail.vue 的 showTabs 写的是「超管 或 自己完成了题单」,而它渲染的
UserProgressView 调的是 requireTeacher 的接口。两边正好错开:
- 学生做完题单 → tab 出现 → 点进去 403(实跑确认:已完成该题单的 student 拿到
403 permission-denied,devadmin 拿到 200)。而 loadUserProgress 没有 try/catch、
loading.value = false 又写在 await 之后,转圈永远停不下来。
- Teacher Admin 看不到这一栏,尽管他们才是它的目标用户、也是唯一调得动的角色。
条件换成 isTeacherOrAbove,取数补 try/finally。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
This commit is contained in:
@@ -249,7 +249,8 @@ adminProblemSetRoutes.post("/problem-sets/:id/problems", requireTeacher, async (
|
||||
hint: parsed.data.hint,
|
||||
}).returning({ id: schema.problemsetProblem.id })
|
||||
// 题目集变了,已加入的人的 totalProblemsCount / 百分比都得跟着变,
|
||||
// 否则学生看到的进度分母还是老的。旧后端没做这一步。
|
||||
// 否则学生看到的进度分母还是老的。旧栈是靠 ProblemSetProblem 的 post_save 信号做的,
|
||||
// 不在 views 里,别因为翻不到显式调用就以为它没做(见 services/problemset.ts)。
|
||||
await resyncProgress(row.id)
|
||||
return success(c, { id: created!.id }, 201)
|
||||
})
|
||||
|
||||
@@ -47,9 +47,13 @@ export function computeProgress(
|
||||
// 乘 10000 四舍五入再除 100,保留两位小数
|
||||
progressPercentage: total > 0 ? Math.round((completed / total) * 10000) / 100 : 0,
|
||||
isCompleted,
|
||||
// 完成状态没了,complete_time 也不该留着。学生那一路本来就是这么写的,
|
||||
// 后台这一路以前保留旧时间,于是同一行会出现「未完成 + 有完成时间」。
|
||||
completeTime: isCompleted ? previousCompleteTime ?? now : null,
|
||||
// 只设不清,语义是「曾经完成于」,对齐旧栈 problemset/models.py:218。
|
||||
//
|
||||
// 「未完成 + 有完成时间」是允许的组合,快照里就有 4 条 —— 题单 8 那批人在它
|
||||
// 还只有 6 题时完成过,老师后来加到 12 题,进度退回未完成,完成时间留了下来。
|
||||
// 反过来清空的代价是不可逆:往一个 100 人已完成的题单里加一道题、再改主意删掉,
|
||||
// 这 100 个人的历史完成时间就一起被冲成了「现在」。
|
||||
completeTime: previousCompleteTime ?? (isCompleted ? now : null),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +143,14 @@ export async function recalculateBadge(badge: BadgeRow, known?: (BadgeCheck & {
|
||||
/**
|
||||
* 题目集或分值变动后,把所有参与者的进度整体重算一遍,再重算这份题单的奖章。
|
||||
*
|
||||
* 旧后端两件事都不做:往题单里加一道题,学生那边的 totalProblemsCount 还是老数字,
|
||||
* 进度百分比因此偏高;奖章那边更是没人回头判过,生产快照里因此攒下 53 条应发未发
|
||||
* (30 名学生,其中 23 条来自 2026-05-22 那次批量补进度)。
|
||||
* 别信「旧后端不做这件事」那个说法(本仓早先的注释里有,是错的):旧栈用 signals 做了,
|
||||
* 而且两件事都做 —— problemset/signals.py 在 ProblemSetProblem 的 post_save / post_delete
|
||||
* 上重算全部参与者的进度、再重算该题单全部奖章的资格。重写时 views 里看不到显式调用,
|
||||
* 就当成没做,于是奖章那一半漏了,生产快照里攒下 53 条应发未发(30 名学生)。
|
||||
*
|
||||
* 那 53 条里有 23 条另有出处:旧栈的管理命令 fix_problemset_progress 按实际 AC 记录补
|
||||
* progress_detail,可 signals 只挂在 ProblemSetProblem 和 ProblemSetBadge 上、不挂 Progress,
|
||||
* 所以进度补了、奖章一枚没补。OJ2 这边目前也还没有补进度的对应工具。
|
||||
*/
|
||||
export async function resyncProgress(problemsetId: number) {
|
||||
const [links, progresses, badges] = await Promise.all([
|
||||
|
||||
@@ -9,6 +9,7 @@ import { usePagination } from "shared/composables/pagination"
|
||||
import Pagination from "shared/components/Pagination.vue"
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||
const progress = ref<ProblemSetProgress[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -54,19 +55,26 @@ async function loadUserProgress() {
|
||||
if (completionFilter.value) {
|
||||
params.completionStatus = completionFilter.value
|
||||
}
|
||||
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
||||
try {
|
||||
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
||||
|
||||
progress.value = res.results
|
||||
total.value = res.total
|
||||
// 使用后端返回的统计数据(基于所有数据)
|
||||
if (res.statistics) {
|
||||
statistics.value = res.statistics
|
||||
progress.value = res.results
|
||||
total.value = res.total
|
||||
// 使用后端返回的统计数据(基于所有数据)
|
||||
if (res.statistics) {
|
||||
statistics.value = res.statistics
|
||||
}
|
||||
// 保存所有题目信息
|
||||
if (res.problems) {
|
||||
allProblems.value = res.problems
|
||||
}
|
||||
} catch (err: any) {
|
||||
// finally 里收掉 loading:以前 loading.value = false 写在 await 之后,
|
||||
// 请求一失败(403、断网)转圈就永远停不下来
|
||||
message.error("加载用户进度失败:" + (err.data || "未知错误"))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
// 保存所有题目信息
|
||||
if (res.problems) {
|
||||
allProblems.value = res.problems
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 监听分页参数变化
|
||||
|
||||
@@ -97,11 +97,10 @@ async function handleJoinProblemSet() {
|
||||
}
|
||||
}
|
||||
|
||||
const showTabs = computed(
|
||||
() =>
|
||||
userStore.isSuperAdmin ||
|
||||
(isJoined.value && problemSet.value?.userProgress?.isCompleted),
|
||||
)
|
||||
// 「用户进度」那一栏调的是 requireTeacher 的接口(Teacher Admin | Super Admin),
|
||||
// 所以这里的条件必须跟它一致。以前写的是「超管 或 自己完成了题单」,两边正好错开:
|
||||
// 学生做完题单会看到这一栏、点进去 403;而真正该用它的 Teacher Admin 反倒看不到。
|
||||
const showTabs = computed(() => userStore.isTeacherOrAbove)
|
||||
|
||||
onMounted(init)
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user