进度算法在学生路径和后台路径各写了一遍,于是各自漂了一段。后台那份 (resyncProgress)名叫「把所有参与者的进度重算一遍」,实际只更新分母和百分比: - 不碰 total_score。改题目分值时专门调了它,函数体里却没这个字段,score 类奖章 因此按陈旧分数判定。 - 不碰 is_completed。加一道题之后分母变大、百分比掉下来,人还标着「已完成」。 - 不清理 progress_detail。删掉一道题之后 least(completed, total) 只保证不超过分母, 会把没做的题算成做了 —— 3 题的题单只做出 C,删掉 C 就变成 1/2 = 50%。 - 不重算奖章。题目集一变 all_problems 的达标面就变了,没有任何地方补发。 最后一条攒下了实打实的欠账:8-07 的快照里 53 条应发未发,涉及 30 名学生,误发 0 条。 其中 23 条来自 2026-05-22 00:50 那次一分钟内跨 7 个题单的批量补进度 —— 进度补了, 奖章没人回头判。 两边不再分叉的唯一办法是只留一处算法,所以抽出 services/problemset.ts: computeProgress 是纯函数,学生做出一题和后台改题目都只是调用者; eligibleForBadge / recalculateBadge / resyncProgress 一并搬过来,补发脚本才能复用 同一套判定。批量写回仍是一条 UPDATE ... FROM (VALUES ...),没退回逐行往返。 顺带修掉空题单的坑:isCompleted 加了 total > 0 前提。原来 0 === 0 也成立,老师先建 题单、学生先加入、题目后加,加入那一刻就写下 complete_time 并计进「完成题单数」成就, 而且后面补上题目也不会自愈。 一处行为变化:退回未完成时 complete_time 会清空。后台那份原来保留旧时间,学生那份 一直是清空的,统一成后者 —— 否则同一行会出现「未完成 + 有完成时间」,破坏现有数据里 「complete_time 非空 ⟺ is_completed」这条不变量。代价是加题再删题会把历史完成时间 洗成「现在」。 scripts/backfill-problemset-badges.ts 补历史欠账,默认只读预演,--apply 才落库。 只要存在误发就拒绝执行并打出名单,要连收回一起做得显式加 --allow-revoke —— recalculateBadge 的删除是真删,user_badge 的 earned_time 没有别处备份。 在生产快照上实跑:补发 1180 → 1233 条,复核全部一致,历史 earned_time 未被刷新; resyncProgress 对题单 9(7 题 / 117 人 / 100 人完成)加题后正确退回 0 人完成并收回 100 枚奖章,删题后完整恢复;改题目分值 10→20 使总分和 7460 → 8590,正好 +113×10。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
This commit is contained in:
1 parent
ff31f7abd1
commit
f9354b0df1
5 files changed
+280
-89
No files matched your search
@@ -20,7 +20,6 @@ import {
|
||||
gt,
|
||||
ilike,
|
||||
inArray,
|
||||
isNull,
|
||||
ne,
|
||||
or,
|
||||
sql,
|
||||
@@ -33,6 +32,7 @@ import { publishAchievementNotification } from "../events"
|
||||
import { failure, success } from "../http"
|
||||
import { JudgeStatus } from "../judge/status"
|
||||
import { updateAchievementsForProblemSet } from "../services/achievements"
|
||||
import { computeProgress } from "../services/problemset"
|
||||
import { objectValue, queryInteger, sampleUser } from "./helpers"
|
||||
|
||||
export const problemsetRoutes = new Hono<AppEnv>()
|
||||
@@ -216,27 +216,9 @@ async function recomputeProgress(
|
||||
) {
|
||||
const links = await tx.select({ problemId: schema.problemsetProblem.problemId, score: schema.problemsetProblem.score })
|
||||
.from(schema.problemsetProblem).where(eq(schema.problemsetProblem.problemsetId, progress.problemsetId))
|
||||
const valid = new Map(links.map((link) => [String(link.problemId), link.score]))
|
||||
for (const key of Object.keys(detail)) if (!valid.has(key)) delete detail[key]
|
||||
let totalScore = 0
|
||||
for (const [key, value] of Object.entries(detail)) {
|
||||
const score = valid.get(key)
|
||||
if (score === undefined) continue
|
||||
totalScore += score
|
||||
detail[key] = { ...objectValue(value), score }
|
||||
}
|
||||
const completed = Object.keys(detail).length
|
||||
const total = links.length
|
||||
const isCompleted = completed === total
|
||||
const update = {
|
||||
progressDetail: detail,
|
||||
totalProblemsCount: total,
|
||||
completedProblemsCount: completed,
|
||||
totalScore,
|
||||
progressPercentage: total > 0 ? completed / total * 100 : 0,
|
||||
isCompleted,
|
||||
completeTime: isCompleted ? progress.completeTime ?? new Date().toISOString() : null,
|
||||
}
|
||||
// 算法本身在 services/problemset.ts —— 后台改题目后的批量重算走的是同一份,
|
||||
// 两边曾经各写一遍,结果后台那份少算了 total_score 和 is_completed
|
||||
const update = computeProgress(detail, links, progress.completeTime)
|
||||
await tx.update(schema.problemsetProgress).set(update).where(eq(schema.problemsetProgress.id, progress.id))
|
||||
return { ...progress, ...update }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user