From 9e0bc1fa8cc9bf2769bf8d70645c52a34c1acc26 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 4 Aug 2026 00:59:49 -0600 Subject: [PATCH] =?UTF-8?q?fix(plan):=20=E4=BF=AE=E6=AD=A3=20unlock()=20?= =?UTF-8?q?=E7=9A=84=E5=B9=B6=E5=8F=91=E9=87=8D=E5=A4=8D=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bulk_create(ignore_conflicts=True) 无法区分新建与已存在, 并发判题时会重复累加 unlock_count 并重复推送通知。 改为逐条 get_or_create,以 was_created 为唯一判据。 Co-Authored-By: Claude Opus 5 --- .../plans/2026-08-03-achievement-system.md | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/superpowers/plans/2026-08-03-achievement-system.md b/docs/superpowers/plans/2026-08-03-achievement-system.md index 9e161d2..97bfe0d 100644 --- a/docs/superpowers/plans/2026-08-03-achievement-system.md +++ b/docs/superpowers/plans/2026-08-03-achievement-system.md @@ -717,19 +717,25 @@ def evaluate(user, metrics, only_metrics=None): def unlock(user, achievements, backfilled=False, notified=False): - """写入解锁记录并累加 unlock_count,返回实际新建的记录。""" - if not achievements: - return [] - records = [ - UserAchievement(user=user, achievement=a, backfilled=backfilled, notified=notified) - for a in achievements - ] - with transaction.atomic(): - UserAchievement.objects.bulk_create(records, ignore_conflicts=True) - created = list( - UserAchievement.objects.filter(user=user, achievement__in=achievements).select_related("achievement") + """写入解锁记录并累加 unlock_count,返回实际新建的记录。 + + 刻意逐条 get_or_create 而不是 bulk_create:unique_user_achievement 约束负责 + 并发竞态,was_created 是"这一条确实是我新建的"的唯一可信判据。用 + bulk_create(ignore_conflicts=True) 则无法区分新建与已存在,并发判题时会把 + unlock_count 重复累加(获得率永久偏高),并对同一个奖杯重复推送通知。 + + 循环次数是"本次新解锁的成就数",常态为 0,因此常态零查询。 + """ + created = [] + for achievement in achievements: + record, was_created = UserAchievement.objects.get_or_create( + user=user, + achievement=achievement, + defaults={"backfilled": backfilled, "notified": notified}, ) - Achievement.objects.filter(id__in=[a.id for a in achievements]).update(unlock_count=F("unlock_count") + 1) + if was_created: + Achievement.objects.filter(id=achievement.id).update(unlock_count=F("unlock_count") + 1) + created.append(record) return created