Files
OnlineJudge/achievement/models.py
yuetsh e9c7793714 feat(achievement): 添加按难度的 AC 题数指标,删除 min_ac_code_chars
新增 mid_ac_count / hard_ac_count,去重统计中等和困难难度的 AC 题数。
在此之前所有成就对水题和难题一视同仁,这是最缺的一个维度。
增量走 build_ctx 新增的 problem_difficulty,它只在首次 AC 时才查库——
绝大多数提交都不是首次 AC,无条件预查等于给每次判题白加一条 SQL。

删除 min_ac_code_chars:线上实测 1314 个用户的分布,最小值 8、p5=10,
有道题 8 个字符就能通过,这个指标测的是"谁做过那道水题"而不是
"谁写得简洁",配不出有意义的成就。

自检里写死 min_ac_code_chars 的两处改成按 lte 成就自动发现:
_check_registry 不再要求某个具体指标存在,_check_min_metric_absent
改为遍历所有上架的 lte 成就检查其指标。没有 lte 成就时 SKIP,
将来配了自动开始检查。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:02:36 -06:00

76 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
from account.models import User
from utils.models import JSONField
class Rarity(models.TextChoices):
BRONZE = "bronze", "青铜"
SILVER = "silver", "白银"
GOLD = "gold", "黄金"
PLATINUM = "platinum", "白金"
class Operator(models.TextChoices):
GTE = "gte", "大于等于"
LTE = "lte", "小于等于"
class Achievement(models.Model):
name = models.TextField(verbose_name="成就名称")
description = models.TextField(verbose_name="成就描述")
# iconify 图标名(如 noto:owl不是 emoji 字符:
# 机房的老浏览器缺 emoji 字体会渲染成方块,前端统一渲染成 SVG
icon = models.TextField(verbose_name="图标")
rarity = models.TextField(default=Rarity.BRONZE, choices=Rarity.choices, verbose_name="稀有度")
hidden = models.BooleanField(default=False, db_default=False, verbose_name="是否隐藏")
metric = models.TextField(verbose_name="指标名")
operator = models.TextField(default=Operator.GTE, choices=Operator.choices, verbose_name="比较符")
threshold = models.IntegerField(verbose_name="阈值")
visible = models.BooleanField(default=True, db_default=True, verbose_name="是否上架")
unlock_count = models.IntegerField(default=0, db_default=0, verbose_name="已解锁人数")
order = models.IntegerField(default=0, db_default=0, verbose_name="排序")
create_time = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "achievement"
ordering = ("order", "id")
verbose_name = "成就"
verbose_name_plural = "成就"
class UserStat(models.Model):
"""成就系统唯一的指标源,不复用 UserProfile 的计数器,避免两处口径漂移。
metrics 为 {指标名: 数值};指标从未产生过有效值时 key 不存在(而非置 0
否则极小值型指标(求 min、配 lte 用的那种)会对新用户恒成立。
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="achievement_stat")
metrics = JSONField(default=dict, db_default=models.Value({}, output_field=models.JSONField()))
update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "user_stat"
class UserAchievement(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="achievements")
achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE)
unlock_time = models.DateTimeField(auto_now_add=True)
# 上线补发的记录:前端不显示具体日期,只显示"已获得"
backfilled = models.BooleanField(default=False, db_default=False)
# 是否已向用户弹过奖杯pending 端点据此查询
notified = models.BooleanField(default=False, db_default=False)
class Meta:
db_table = "user_achievement"
ordering = ("-unlock_time",)
constraints = [
models.UniqueConstraint(fields=["user", "achievement"], name="unique_user_achievement"),
]
indexes = [
models.Index(fields=["user", "-unlock_time"], name="user_achv_time_idx"),
models.Index(fields=["user", "notified"], name="user_achv_notified_idx"),
]