fix(题单): 「选做」终于作数了,分母只算必做题
isRequired 一直只是卡片上的一行字:卡片写着「(选做)」,进度分母和 all_problems 奖章却照样要求做完。结果是学生按提示跳过选做题,进度条卡在 100% 以下、全通奖章也 拿不到 —— 快照里 22 个人做完了全部必做题却显示未完成(题单 5 三人、6 十人、8 两人、 11 七人)。旧栈的 update_progress 同样不区分,是一路继承下来的。 改成:分母只算必做题,选做题做了仍然计分(totalScore 把它算进去),只是不卡完成。 一道必做都没标的题单退回「全部都算必做」—— 那种题单多半是没用这个字段,而不是真的 整单选做,不兜住的话它永远完不成。 ## problem_count 奖章不能跟着改 老师当初是按题单的**总题数**设阈值的:题单 5 的「一职欧拉」要 8 题,而它的必做只有 7 道。要是 problem_count 也改用只数必做的 completedProblemsCount,这枚奖章一夜之间 不可得,76 个已经拿到的人会被 recalculateBadge 收回。所以它数的是「做出的题目总数 (含选做)」,从 progress_detail 的键数来。score 类同理不受影响。 预演证实了这道闸门有效:22 人拿到完成状态,奖章补发 56 条、**收回 0 条**。 ## 顺带:第三份手抄的达标逻辑 PUT /problem-set-progress 里还藏着一份 inline 的奖章判定,和 services 里那份、 补发脚本里那份是三份各写各的 —— 这次改 problem_count 的语义,漏掉任何一份都会让 学生提交时发的奖章和后台重算的结果对不上。三处统一到 eligibleForBadge。 ## 工具改名并扩到进度 backfill-badges → backfill-problemsets。语义变更之后,已有的进度行要跑一遍才会按新 规则重算,否则那 22 个人得等到老师下次动题单才生效。两笔账本来也是同一笔:进度一变, 奖章达标面就跟着变,所以落库走 resyncProgress(它重算进度后会顺手重算该题单全部奖章), 预演里的奖章差异也是照着订正后的进度算的,保证预演和 --apply 的结果一致。 在生产快照上实跑: 合计:进度 494 条要重算(完成 +22 / -0),奖章补发 56 条、收回 0 条 已订正 10 个题单 → 复核通过:题单数据与规则一致 user_badge 1180 → 1236,已完成 621 → 643 「未完成但有完成时间」仍是 4 条(d7a6414 那条语义保住了) 重复跑幂等:进度 0 条、奖章 0 条 抽查题单 6(9 必做 + 1 选做):只做必做的显示 9/9 100% 已完成、80 分;连选做一起做的 同样 9/9 已完成,但 90 分。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
This commit is contained in:
@@ -31,7 +31,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 { computeProgress, eligibleForBadge } from "../services/problemset"
|
||||
import { objectValue, queryInteger, sampleUser } from "./helpers"
|
||||
|
||||
export const problemsetRoutes = new Hono<AppEnv>()
|
||||
@@ -206,8 +206,11 @@ async function recomputeProgress(
|
||||
progress: typeof schema.problemsetProgress.$inferSelect,
|
||||
detail: Record<string, unknown>,
|
||||
) {
|
||||
const links = await tx.select({ problemId: schema.problemsetProblem.problemId, score: schema.problemsetProblem.score })
|
||||
.from(schema.problemsetProblem).where(eq(schema.problemsetProblem.problemsetId, progress.problemsetId))
|
||||
const links = await tx.select({
|
||||
problemId: schema.problemsetProblem.problemId,
|
||||
score: schema.problemsetProblem.score,
|
||||
isRequired: schema.problemsetProblem.isRequired,
|
||||
}).from(schema.problemsetProblem).where(eq(schema.problemsetProblem.problemsetId, progress.problemsetId))
|
||||
// 算法本身在 services/problemset.ts —— 后台改题目后的批量重算走的是同一份,
|
||||
// 两边曾经各写一遍,结果后台那份少算了 total_score 和 is_completed
|
||||
const update = computeProgress(detail, links, progress.completeTime)
|
||||
@@ -283,11 +286,9 @@ problemsetRoutes.put("/problem-set-progress", requireAuth, async (c) => {
|
||||
})
|
||||
}
|
||||
const badges = await tx.select().from(schema.problemsetBadge).where(eq(schema.problemsetBadge.problemsetId, problemSet.id))
|
||||
const hits = badges.filter((badge) => badge.conditionType === "all_problems"
|
||||
? updated.totalProblemsCount > 0 && updated.completedProblemsCount === updated.totalProblemsCount
|
||||
: badge.conditionType === "problem_count"
|
||||
? updated.completedProblemsCount >= badge.conditionValue
|
||||
: badge.conditionType === "score" && updated.totalScore >= badge.conditionValue)
|
||||
// 判定走 services/problemset.ts 那一份 —— 这里原来是第三份手抄的达标逻辑,
|
||||
// 后台重算和补发脚本各有各的,改一处规则就会漏掉另外两处
|
||||
const hits = badges.filter((badge) => eligibleForBadge(badge, updated))
|
||||
if (hits.length === 0) return { earned: [] as (typeof schema.problemsetBadge.$inferSelect)[] }
|
||||
// 达标的奖章一次插完,冲突忽略后 returning 回来的就是这次真拿到的
|
||||
const inserted = await tx.insert(schema.userBadge).values(hits.map((badge) => ({
|
||||
|
||||
Reference in New Issue
Block a user