feat(achievement): 添加管理侧成就 API 与阈值补发

This commit is contained in:
2026-08-04 01:20:58 -06:00
parent b9caf55c4c
commit 46e5d774f1
4 changed files with 165 additions and 0 deletions

View File

@@ -24,3 +24,39 @@ def check_achievements(user_id, submission_id):
notify_achievements(user_id, records)
except Exception as e:
logger.exception(f"check_achievements failed: user_id={user_id}, submission_id={submission_id}, error={e}")
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS())
def rescan_achievement(achievement_id):
"""新建成就或调低阈值后,补发给已达标的存量用户。
判定只在判题时发生,因此后台改了阈值不会自动补发,必须显式扫一遍。
"""
from django.db.models import IntegerField
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast
from achievement.models import Achievement, Operator, UserAchievement, UserStat
try:
achievement = Achievement.objects.get(id=achievement_id, visible=True)
except Achievement.DoesNotExist:
return
# JSONField 的默认比较是 JSON 值比较,数字会按字符串序比("9" > "50"
# 必须显式 cast 成整数,否则筛出来的用户是错的
qs = UserStat.objects.filter(metrics__has_key=achievement.metric).annotate(v=Cast(KeyTextTransform(achievement.metric, "metrics"), IntegerField()))
if achievement.operator == Operator.GTE:
qs = qs.filter(v__gte=achievement.threshold)
else:
qs = qs.filter(v__lte=achievement.threshold)
already = set(UserAchievement.objects.filter(achievement=achievement).values_list("user_id", flat=True))
for stat in qs.select_related("user").iterator():
if stat.user_id in already:
continue
try:
records = checker.unlock(stat.user, [achievement])
notify_achievements(stat.user_id, records)
except Exception as e:
logger.error(f"rescan_achievement failed for user {stat.user_id}: {e}")