fix(plan): 修正 unlock() 的并发重复计数缺陷

bulk_create(ignore_conflicts=True) 无法区分新建与已存在,
并发判题时会重复累加 unlock_count 并重复推送通知。
改为逐条 get_or_create,以 was_created 为唯一判据。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 00:59:49 -06:00
parent c1081e6193
commit 9e0bc1fa8c

View File

@@ -717,19 +717,25 @@ def evaluate(user, metrics, only_metrics=None):
def unlock(user, achievements, backfilled=False, notified=False): def unlock(user, achievements, backfilled=False, notified=False):
"""写入解锁记录并累加 unlock_count返回实际新建的记录。""" """写入解锁记录并累加 unlock_count返回实际新建的记录。
if not achievements:
return [] 刻意逐条 get_or_create 而不是 bulk_createunique_user_achievement 约束负责
records = [ 并发竞态was_created 是"这一条确实是我新建的"的唯一可信判据。用
UserAchievement(user=user, achievement=a, backfilled=backfilled, notified=notified) bulk_create(ignore_conflicts=True) 则无法区分新建与已存在,并发判题时会把
for a in achievements unlock_count 重复累加(获得率永久偏高),并对同一个奖杯重复推送通知。
]
with transaction.atomic(): 循环次数是"本次新解锁的成就数",常态为 0因此常态零查询。
UserAchievement.objects.bulk_create(records, ignore_conflicts=True) """
created = list( created = []
UserAchievement.objects.filter(user=user, achievement__in=achievements).select_related("achievement") 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 return created