From d14251b53da1f30846f300875044fafa622fcc9f Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 4 Aug 2026 01:28:31 -0600 Subject: [PATCH] =?UTF-8?q?fix(achievement):=20=E8=A1=A5=E5=8F=91=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=20backfilled=E3=80=81=E6=94=BE=E5=AE=BD=E8=A1=A5?= =?UTF-8?q?=E5=8F=91=E8=A7=A6=E5=8F=91=E6=9D=A1=E4=BB=B6=E3=80=81=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E7=A8=80=E6=9C=89=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- achievement/tasks.py | 5 ++++- achievement/views/admin.py | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/achievement/tasks.py b/achievement/tasks.py index c271e18..5095a8d 100644 --- a/achievement/tasks.py +++ b/achievement/tasks.py @@ -56,7 +56,10 @@ def rescan_achievement(achievement_id): if stat.user_id in already: continue try: - records = checker.unlock(stat.user, [achievement]) + # backfilled=True:这是补发,不是学生刚刚挣到的。 + # 前端据此只显示"已获得"而不显示具体日期——否则一次补发会给几百人 + # 盖上同一个时间戳,把"最近获得"板块彻底冲垮 + records = checker.unlock(stat.user, [achievement], backfilled=True) notify_achievements(stat.user_id, records) except Exception as e: logger.error(f"rescan_achievement failed for user {stat.user_id}: {e}") diff --git a/achievement/views/admin.py b/achievement/views/admin.py index 88646ff..2f3f994 100644 --- a/achievement/views/admin.py +++ b/achievement/views/admin.py @@ -1,6 +1,6 @@ from account.decorators import super_admin_required from achievement.metrics import METRIC_REGISTRY -from achievement.models import Achievement +from achievement.models import Achievement, Rarity from achievement.tasks import rescan_achievement from utils.api import APIView from utils.shortcuts import check_is_id @@ -53,20 +53,19 @@ class AchievementAdminAPI(APIView): if error: return self.error(error) - old_threshold = achievement.threshold - old_operator = achievement.operator + before = (achievement.metric, achievement.operator, achievement.threshold, achievement.visible) for field in ("name", "description", "icon", "rarity", "hidden", "metric", "operator", "threshold", "visible", "order"): if field in data: setattr(achievement, field, data[field]) achievement.save() - # 条件放宽(gte 调低阈值 / lte 调高阈值 / 换了比较符)时补发 - loosened = ( - achievement.operator != old_operator - or (achievement.operator == "gte" and achievement.threshold < old_threshold) - or (achievement.operator == "lte" and achievement.threshold > old_threshold) - ) - if loosened and achievement.visible: + # 只要"谁能达成"这件事可能变了就补发,不去精细判断是否放宽。 + # 补发是幂等的后台任务(unlock 用 get_or_create),多跑一次只花一次扫描; + # 漏跑却是学生已达标却拿不到,两个方向代价不对称。 + # 早先的 loosened 谓词只看 operator/threshold,会漏掉两种情况: + # 换了 metric(换了维度)、以及从下架改成上架(草稿期已达标的人)。 + after = (achievement.metric, achievement.operator, achievement.threshold, achievement.visible) + if achievement.visible and before != after: rescan_achievement.send(achievement.id) return self.success(_serialize(achievement)) @@ -94,6 +93,10 @@ def _validate(data): return "指标不存在" if data["operator"] not in ("gte", "lte"): return "比较符不合法" + # rarity 不校验的话,一个乱填的值会让 AchievementSummaryAPI 的四档统计 + # 对不上:它按 Rarity.choices 遍历,野值算进总数却不出现在任何一档里 + if data["rarity"] not in Rarity.values: + return "稀有度不合法" if not isinstance(data.get("threshold"), int): return "阈值必须是整数" return None