From 73789bda0c95e8e44517ceef68c32edb5ad1f2c1 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 4 Aug 2026 01:07:21 -0600 Subject: [PATCH] =?UTF-8?q?fix(plan):=20=E4=BF=AE=E6=AD=A3=E5=85=83?= =?UTF-8?q?=E6=8C=87=E6=A0=87=E5=9B=9E=E5=86=99=E7=9A=84=E4=B8=A2=E5=A4=B1?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第二轮 stat.save() 在锁外整体写回解锁前的快照, 并发判题时会覆盖掉另一条链路刚提交的指标增量。 改为重新取锁、重读、只合并元指标的 key。 Co-Authored-By: Claude Opus 5 --- .../plans/2026-08-03-achievement-system.md | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/plans/2026-08-03-achievement-system.md b/docs/superpowers/plans/2026-08-03-achievement-system.md index 97bfe0d..092c506 100644 --- a/docs/superpowers/plans/2026-08-03-achievement-system.md +++ b/docs/superpowers/plans/2026-08-03-achievement-system.md @@ -665,7 +665,7 @@ git commit -m "feat(achievement): 添加指标注册表与 16 个内置指标" - Produces: - `checker.evaluate(user, metrics: dict, only_metrics: set | None = None) -> list[Achievement]` — 纯判定,返回应解锁但尚未解锁的成就 - `checker.unlock(user, achievements: list, backfilled=False, notified=False) -> list[UserAchievement]` — 写库 + 更新 `unlock_count` - - `checker.run_for_submission(user_id: int, submission) -> list[UserAchievement]` — 完整两轮流程 + - `checker.run_for_submission(user: User, submission) -> list[UserAchievement]` — 完整两轮流程(传 User 实例,不是 id) - `notify.notify_achievements(user_id: int, records: list[UserAchievement]) -> None` - `notify.notify_badges(user_id: int, badges: list) -> None` - `tasks.check_achievements.send(user_id, submission_id)` @@ -759,13 +759,21 @@ def run_for_submission(user, submission): return [] # 第二轮:只重算元指标、只判定依赖元指标的成就,不再有第三轮 - for key in META_METRICS: - value = METRIC_REGISTRY[key].recompute(user) - if value is None: - stat.metrics.pop(key, None) - else: - stat.metrics[key] = value - stat.save(update_fields=["metrics", "update_time"]) + meta_values = {key: METRIC_REGISTRY[key].recompute(user) for key in META_METRICS} + + # 必须重新取锁并重新读一次 stat:上面那个 stat 对象的 metrics 是解锁前的快照, + # 直接 save 会把整份字典写回,覆盖掉并发判题在这期间已提交的增量 + # (同一用户两次提交并发判题时会让提交数/AC 数静默倒退,且无定期重算兜底)。 + # 这里只合并元指标那几个 key。 + with transaction.atomic(): + stat = UserStat.objects.select_for_update().get(user=user) + for key, value in meta_values.items(): + if value is None: + stat.metrics.pop(key, None) + else: + stat.metrics[key] = value + stat.save(update_fields=["metrics", "update_time"]) + second = unlock(user, evaluate(user, stat.metrics, only_metrics=META_METRICS)) return first + second