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

138 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 成就系统上线与运维
首次上线日期:待填(部署当天补)
设计文档:`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-input``deploy/supervisord.conf` 的 dramatiq 随容器重启。**正常重新部署即可完成迁移和 worker 加载,不需要手动做这两步**,但必须确认它们成功了。
前后端尽量一起发:前端在每次路由切换时请求 `/api/achievements/pending`,后端没上会在日志里刷一片 404失败是静默吞掉的不会白屏
### 第一步 确认基础设施
```bash
python manage.py showmigrations achievement # 应为 [X] 0001_initial
grep -c "check_achievements" /data/log/dramatiq.log # 应 > 0
```
**worker 日志里没有 `check_achievements` 这个 actor说明它没加载新代码——成就永远不会判定而且不报任何错。** 这是最容易被忽略的失败模式。
### 第二步 跑自检
```bash
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"` 成立。
### 第三步 单人试跑重算
```bash
python manage.py recompute_achievements --user <某活跃学生id>
python manage.py check_achievement_deploy
```
「重算重建了增量辅助键」这项应从 SKIP 变 PASS。不 PASS 的话,那个学生**下次提交时活跃天数会从几十掉回 1**,并且要等到下一次重算才恢复。
### 第四步 配成就并全量补发
在管理后台「成就」页配置。参考配置表见实施计划文档 Task 1317 条,累积型 `hidden=False`、隐藏型 `hidden=True`)。
```bash
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` 确认
### 定期检查
每周跑一次自检,重点看「疑似配错阈值的成就」。**公开成就配置一周后仍然零解锁,多半是阈值配错了而不是太难**——学生拿不到不会来问你,管理后台列表的「已解锁人数」列是唯一的信号。
## 出问题了怎么办
### 配错了想重来
```bash
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 与实际解锁人数一致」这项就是在防这个。
### 某个学生的指标看起来不对
```bash
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_badges``problemset/models.py:_is_eligible` 各自实现了同样三个条件,今天一致。将来加条件类型时只改一处,`recalculate_user_badges` 会把另一条路径发出的奖章当作"不再符合条件"而删掉。
3. **全量重算时 4 个指标的查询跑两遍**`recompute``recompute_state` 各一次)。离线批处理命令,不影响正确性。
4. **`POST /api/achievements/pending``ids` 未做元素类型校验。** 传非整数会在 ORM 层抛异常,表现为不透明的 500。需要登录无越权风险。
5. **`threshold` 的校验用 `isinstance(x, int)`Python 里 `bool``int` 的子类**,所以 `{"threshold": true}` 会被存成 1。仅管理员可达。