Files
OnlineJudge/docs/ops/achievement-deploy.md
yuetsh f95c3a8bae docs: 成就系统上线与运维文档
部署步骤、日常运维(加成就/加指标/定期检查)、
出问题的处置、已知未验证部分与遗留问题。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 07:30:51 -06:00

7.3 KiB
Raw Permalink Blame History

成就系统上线与运维

首次上线日期:待填(部署当天补) 设计文档:docs/superpowers/specs/2026-08-03-achievement-system-design.md 实施计划:docs/superpowers/plans/2026-08-03-achievement-system.md

这套系统在做什么

学生刷题时后台异步累积一批指标AC 题数、连续 AC 天数、凌晨提交次数……),管理员在后台把「指标 + 比较符 + 阈值」组合成成就。判题完成后投递一个 dramatiq 任务重算该用户的指标并判定解锁,解锁后弹奖杯。

关键分工,配置时必须理解:

定义在哪 改动成本
能测量什么(指标) 代码 achievement/metrics.py 改代码 + 部署 + 跑重算
多少算达成、叫什么、藏不藏 管理后台表单 纯配置,即时生效

所以「凌晨提交 10 次 → 夜猫子」和「凌晨提交 100 次 → 作息已崩坏」是两条成就、同一个指标,纯后台加即可。「代码里写了 goto」则是新维度必须先改代码。

部署

deploy/entrypoint.sh 会自动 migrate --no-inputdeploy/supervisord.conf 的 dramatiq 随容器重启。正常重新部署即可完成迁移和 worker 加载,不需要手动做这两步,但必须确认它们成功了。

前后端尽量一起发:前端在每次路由切换时请求 /api/achievements/pending,后端没上会在日志里刷一片 404失败是静默吞掉的不会白屏

第一步 确认基础设施

python manage.py showmigrations achievement          # 应为 [X] 0001_initial
grep -c "check_achievements" /data/log/dramatiq.log  # 应 > 0

worker 日志里没有 check_achievements 这个 actor说明它没加载新代码——成就永远不会判定而且不报任何错。 这是最容易被忽略的失败模式。

第二步 跑自检

python manage.py check_achievement_deploy

只读,可反复跑,七项检查。此刻大部分是 [SKIP](还没配成就、还没重算),但这两项必须现在就 PASS

  • 极小值指标对零 AC 用户返回 None —— 不 PASS 就别配任何 lte 类成就。min_ac_code_chars 若返回 0,「最短 AC 代码 ≤ 50 字符」会白送给每一个从没做出过题的新生。
  • JSONB 阈值比较按整数 —— 不 PASS 的话以后在后台调低阈值触发的补发会发给错误的人群。JSONB 里的数字若不显式 castPostgres 按字符串序比较,"9" > "50" 成立。

第三步 单人试跑重算

python manage.py recompute_achievements --user <某活跃学生id>
python manage.py check_achievement_deploy

「重算重建了增量辅助键」这项应从 SKIP 变 PASS。不 PASS 的话,那个学生下次提交时活跃天数会从几十掉回 1,并且要等到下一次重算才恢复。

第四步 配成就并全量补发

在管理后台「成就」页配置。参考配置表见实施计划文档 Task 1317 条,累积型 hidden=False、隐藏型 hidden=True)。

python manage.py recompute_achievements --silent
python manage.py check_achievement_deploy    # 七项应全部 PASS

--silent 不能省。 省了的话,几百个学生下次登录会被几十个奖杯连续糊脸,一个接一个弹三秒。

--silent 只在首次上线用。以后加了新指标再跑重算时不带这个参数,新解锁会正常弹出。

日常运维

加一条成就

后台直接配。若指标已存在,不需要部署。

新建和「放宽条件」(改 metric / 改比较符 / 调阈值 / 从下架转上架)会自动触发 rescan_achievement 补发给已达标的存量用户——判定只在判题时发生,不补发的话他们要等到下次提交才拿到。

加一个新指标

必须改 achievement/metrics.py

  1. 写一个 @metric(...) 类,实现 on_submission(增量)和 recompute(全量)
  2. 如果 on_submission 依赖 _ 前缀的辅助键,必须同时实现 recompute_state —— 否则全量重算丢掉辅助键后,用户下一次提交会把该指标打回初始值
  3. 部署后跑 python manage.py recompute_achievements(不带 --silent,让新解锁正常弹出)
  4. check_achievement_deploy 确认

定期检查

每周跑一次自检,重点看「疑似配错阈值的成就」。公开成就配置一周后仍然零解锁,多半是阈值配错了而不是太难——学生拿不到不会来问你,管理后台列表的「已解锁人数」列是唯一的信号。

出问题了怎么办

配错了想重来

python manage.py shell -c "
from achievement.models import Achievement, UserAchievement
UserAchievement.objects.all().delete()
Achievement.objects.update(unlock_count=0)   # 这行别忘
"
python manage.py recompute_achievements --silent

unlock_count 是独立的计数器,删解锁记录不会清零它。 忘了这行再重跑,获得率会翻倍显示。check_achievement_deploy 的「unlock_count 与实际解锁人数一致」这项就是在防这个。

某个学生的指标看起来不对

python manage.py recompute_achievements --user <id>

全量重算是增量逻辑的安全网,怀疑漂移时对单人跑一次即可。

想临时关掉某条成就

后台把它改成下架(visible=False)。已解锁记录保留,只是不再展示也不再判定。重新上架会自动补发给期间达标的人。

已知未验证的部分

这套系统开发全程没有可用的数据库,后端全部逻辑只经过静态审查。开发期修掉的六个缺陷里没有一个是转录错误,全是设计疏漏——这说明纸面审查有效,但它抓不到运行期行为。

check_achievement_deploy 就是为这一层准备的。下面这些只有真跑起来才能确认,都属于"错了也不报错、只是悄悄发错奖杯"

  • JSONB 阈值比较是否真按整数(自检第 4 项)
  • 极小值指标对零 AC 用户是否返回 None(自检第 3 项)
  • 重算是否重建了增量辅助键(自检第 5 项)
  • 并发判题时 unlock_count 是否重复累加(自检第 6 项)

另有一份逐条的验证清单在 .superpowers/sdd/2026-08-03-achievement-system/deferred-verification.md(附命令与期望输出)。

已知遗留问题

按影响排序,都不阻塞上线:

  1. 题单奖章的管理员重算路径仍然静默发放。 problemset/models.py:recalculate_user_badges 不调 notify_badges,学生不会收到弹窗。提交触发的路径(_check_badges)已经接了通知。
  2. 奖章的条件判断逻辑有两份。 problemset/views/oj.py:_check_badgesproblemset/models.py:_is_eligible 各自实现了同样三个条件,今天一致。将来加条件类型时只改一处,recalculate_user_badges 会把另一条路径发出的奖章当作"不再符合条件"而删掉。
  3. 全量重算时 4 个指标的查询跑两遍recomputerecompute_state 各一次)。离线批处理命令,不影响正确性。
  4. POST /api/achievements/pendingids 未做元素类型校验。 传非整数会在 ORM 层抛异常,表现为不透明的 500。需要登录无越权风险。
  5. threshold 的校验用 isinstance(x, int)Python 里 boolint 的子类,所以 {"threshold": true} 会被存成 1。仅管理员可达。