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,
|
hint: parsed.data.hint,
|
||||||
}).returning({ id: schema.problemsetProblem.id })
|
}).returning({ id: schema.problemsetProblem.id })
|
||||||
// 题目集变了,已加入的人的 totalProblemsCount / 百分比都得跟着变,
|
// 题目集变了,已加入的人的 totalProblemsCount / 百分比都得跟着变,
|
||||||
// 否则学生看到的进度分母还是老的。旧后端没做这一步。
|
// 否则学生看到的进度分母还是老的。旧栈是靠 ProblemSetProblem 的 post_save 信号做的,
|
||||||
|
// 不在 views 里,别因为翻不到显式调用就以为它没做(见 services/problemset.ts)。
|
||||||
await resyncProgress(row.id)
|
await resyncProgress(row.id)
|
||||||
return success(c, { id: created!.id }, 201)
|
return success(c, { id: created!.id }, 201)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,9 +47,13 @@ export function computeProgress(
|
|||||||
// 乘 10000 四舍五入再除 100,保留两位小数
|
// 乘 10000 四舍五入再除 100,保留两位小数
|
||||||
progressPercentage: total > 0 ? Math.round((completed / total) * 10000) / 100 : 0,
|
progressPercentage: total > 0 ? Math.round((completed / total) * 10000) / 100 : 0,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
// 完成状态没了,complete_time 也不该留着。学生那一路本来就是这么写的,
|
// 只设不清,语义是「曾经完成于」,对齐旧栈 problemset/models.py:218。
|
||||||
// 后台这一路以前保留旧时间,于是同一行会出现「未完成 + 有完成时间」。
|
//
|
||||||
completeTime: isCompleted ? previousCompleteTime ?? now : null,
|
// 「未完成 + 有完成时间」是允许的组合,快照里就有 4 条 —— 题单 8 那批人在它
|
||||||
|
// 还只有 6 题时完成过,老师后来加到 12 题,进度退回未完成,完成时间留了下来。
|
||||||
|
// 反过来清空的代价是不可逆:往一个 100 人已完成的题单里加一道题、再改主意删掉,
|
||||||
|
// 这 100 个人的历史完成时间就一起被冲成了「现在」。
|
||||||
|
completeTime: previousCompleteTime ?? (isCompleted ? now : null),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,9 +143,14 @@ export async function recalculateBadge(badge: BadgeRow, known?: (BadgeCheck & {
|
|||||||
/**
|
/**
|
||||||
* 题目集或分值变动后,把所有参与者的进度整体重算一遍,再重算这份题单的奖章。
|
* 题目集或分值变动后,把所有参与者的进度整体重算一遍,再重算这份题单的奖章。
|
||||||
*
|
*
|
||||||
* 旧后端两件事都不做:往题单里加一道题,学生那边的 totalProblemsCount 还是老数字,
|
* 别信「旧后端不做这件事」那个说法(本仓早先的注释里有,是错的):旧栈用 signals 做了,
|
||||||
* 进度百分比因此偏高;奖章那边更是没人回头判过,生产快照里因此攒下 53 条应发未发
|
* 而且两件事都做 —— problemset/signals.py 在 ProblemSetProblem 的 post_save / post_delete
|
||||||
* (30 名学生,其中 23 条来自 2026-05-22 那次批量补进度)。
|
* 上重算全部参与者的进度、再重算该题单全部奖章的资格。重写时 views 里看不到显式调用,
|
||||||
|
* 就当成没做,于是奖章那一半漏了,生产快照里攒下 53 条应发未发(30 名学生)。
|
||||||
|
*
|
||||||
|
* 那 53 条里有 23 条另有出处:旧栈的管理命令 fix_problemset_progress 按实际 AC 记录补
|
||||||
|
* progress_detail,可 signals 只挂在 ProblemSetProblem 和 ProblemSetBadge 上、不挂 Progress,
|
||||||
|
* 所以进度补了、奖章一枚没补。OJ2 这边目前也还没有补进度的对应工具。
|
||||||
*/
|
*/
|
||||||
export async function resyncProgress(problemsetId: number) {
|
export async function resyncProgress(problemsetId: number) {
|
||||||
const [links, progresses, badges] = await Promise.all([
|
const [links, progresses, badges] = await Promise.all([
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { usePagination } from "shared/composables/pagination"
|
|||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
const problemSetId = computed(() => Number(route.params.problemSetId))
|
const problemSetId = computed(() => Number(route.params.problemSetId))
|
||||||
const progress = ref<ProblemSetProgress[]>([])
|
const progress = ref<ProblemSetProgress[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -54,6 +55,7 @@ async function loadUserProgress() {
|
|||||||
if (completionFilter.value) {
|
if (completionFilter.value) {
|
||||||
params.completionStatus = completionFilter.value
|
params.completionStatus = completionFilter.value
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
||||||
|
|
||||||
progress.value = res.results
|
progress.value = res.results
|
||||||
@@ -66,7 +68,13 @@ async function loadUserProgress() {
|
|||||||
if (res.problems) {
|
if (res.problems) {
|
||||||
allProblems.value = res.problems
|
allProblems.value = res.problems
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// finally 里收掉 loading:以前 loading.value = false 写在 await 之后,
|
||||||
|
// 请求一失败(403、断网)转圈就永远停不下来
|
||||||
|
message.error("加载用户进度失败:" + (err.data || "未知错误"))
|
||||||
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听分页参数变化
|
// 监听分页参数变化
|
||||||
|
|||||||
@@ -97,11 +97,10 @@ async function handleJoinProblemSet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showTabs = computed(
|
// 「用户进度」那一栏调的是 requireTeacher 的接口(Teacher Admin | Super Admin),
|
||||||
() =>
|
// 所以这里的条件必须跟它一致。以前写的是「超管 或 自己完成了题单」,两边正好错开:
|
||||||
userStore.isSuperAdmin ||
|
// 学生做完题单会看到这一栏、点进去 403;而真正该用它的 Teacher Admin 反倒看不到。
|
||||||
(isJoined.value && problemSet.value?.userProgress?.isCompleted),
|
const showTabs = computed(() => userStore.isTeacherOrAbove)
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user