Files
OnlineJudge/achievement/views/oj.py
yuetsh 1d66e2be1f feat(achievement): 添加用户侧成就 API
新增 achievements 列表、summary、pending 三个用户侧端点,隐藏未解锁成就
在序列化层打码(name/description/icon 替换为占位符,不下发 progress)。
2026-08-04 01:12:16 -06:00

101 lines
4.1 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.models import Count
from account.decorators import login_required
from account.models import User
from achievement.models import Achievement, Rarity, UserAchievement, UserStat
from achievement.serializers import AchievementSerializer, PendingAchievementSerializer
from utils.api import APIView
def _resolve_user(request):
"""?name=<username> 指定他人,不传则为自己。与 /api/profile 的约定一致。"""
username = request.GET.get("name")
if username:
return User.objects.filter(username=username, is_disabled=False).first()
return request.user
def _decorate(achievements, unlocked_map, metrics):
"""给成就对象挂上该用户的解锁状态与进度,供序列化器读取。"""
for a in achievements:
record = unlocked_map.get(a.id)
a.unlocked = record is not None
a.unlock_time = record.unlock_time if record else None
a.backfilled = record.backfilled if record else False
value = metrics.get(a.metric)
# 隐藏且未解锁的成就不下发进度,否则能反推出条件
a.progress = None if (a.hidden and not a.unlocked) else (value or 0)
return achievements
class AchievementListAPI(APIView):
@login_required
def get(self, request):
user = _resolve_user(request)
if user is None:
return self.error("用户不存在")
achievements = list(Achievement.objects.filter(visible=True))
unlocked_map = {r.achievement_id: r for r in UserAchievement.objects.filter(user=user)}
stat = UserStat.objects.filter(user=user).first()
metrics = stat.metrics if stat else {}
_decorate(achievements, unlocked_map, metrics)
return self.success(
{
"username": user.username,
"achievements": AchievementSerializer(achievements, many=True).data,
}
)
class AchievementSummaryAPI(APIView):
@login_required
def get(self, request):
user = _resolve_user(request)
if user is None:
return self.error("用户不存在")
total_by_rarity = dict(Achievement.objects.filter(visible=True).values_list("rarity").annotate(c=Count("id")))
unlocked_by_rarity = dict(UserAchievement.objects.filter(user=user, achievement__visible=True).values_list("achievement__rarity").annotate(c=Count("id")))
total = sum(total_by_rarity.values())
unlocked = sum(unlocked_by_rarity.values())
recent = list(UserAchievement.objects.filter(user=user, achievement__visible=True).select_related("achievement").order_by("-unlock_time")[:5])
return self.success(
{
"username": user.username,
"total": total,
"unlocked": unlocked,
"percent": round(unlocked / total * 100, 1) if total else 0.0,
"rarity": [
{
"rarity": value,
"label": label,
"total": total_by_rarity.get(value, 0),
"unlocked": unlocked_by_rarity.get(value, 0),
}
for value, label in Rarity.choices
],
"recent": PendingAchievementSerializer(recent, many=True).data,
}
)
class AchievementPendingAPI(APIView):
@login_required
def get(self, request):
"""返回尚未弹过的解锁记录。前端在布局层路由切换时拉取。"""
records = UserAchievement.objects.filter(user=request.user, notified=False, achievement__visible=True).select_related("achievement").order_by("unlock_time")
return self.success(PendingAchievementSerializer(records, many=True).data)
@login_required
def post(self, request):
"""弹完后标记已读避免下次导航重复弹。body: {"ids": [成就 id]}"""
ids = request.data.get("ids") or []
if not isinstance(ids, list):
return self.error("参数错误")
UserAchievement.objects.filter(user=request.user, achievement_id__in=ids).update(notified=True)
return self.success("ok")