pmc bd95ba470c feat: update admin panel, API modules, and add migrations
- Update food, outfits, props, home-decor pages and components
- Add permissions page and sidebar updates
- Update API client and all API modules (auth, food, dances, etc.)
- Add card model migrations for optional fields
- Update Django views, serializers, and authentication
- Add affinity level migrations and user app updates
- Add project documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:06:50 +08:00

251 lines
9.4 KiB
Python

from django.shortcuts import render
from rest_framework import viewsets, mixins, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from django.db import transaction, models
from django.utils import timezone
from userapp.authentication import RedisTokenAuthentication
from .models import Achievement, UserAchievement
from .serializers import (
AchievementSerializer,
UserAchievementSerializer,
UserAchievementCreateSerializer,
UserAchievementListSerializer
)
class IsAdminOrReadOnly(permissions.BasePermission):
"""管理员可写,其他人只读"""
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return request.user and request.user.is_authenticated
return request.user and request.user.is_staff
class AchievementViewSet(viewsets.ModelViewSet):
"""
成就相关视图集
管理员可增删改查,普通用户只读
"""
serializer_class = AchievementSerializer
permission_classes = [IsAdminOrReadOnly]
authentication_classes = [RedisTokenAuthentication]
def get_queryset(self):
"""根据请求过滤成就列表"""
queryset = Achievement.objects.all()
# 排除隐藏成就(除非客户端特别请求)
show_hidden = self.request.query_params.get('show_hidden', 'false').lower() == 'true'
if not show_hidden:
# 过滤出用户已获得的隐藏成就和非隐藏成就
user_achievement_ids = UserAchievement.objects.filter(
user=self.request.user
).values_list('achievement_id', flat=True)
queryset = queryset.filter(
# 非隐藏成就或已经获得的隐藏成就
models.Q(is_hidden=False) | models.Q(id__in=user_achievement_ids)
)
# 按类型过滤
achievement_type = self.request.query_params.get('type')
if achievement_type:
queryset = queryset.filter(achievement_type=achievement_type)
# 按稀有度过滤
rarity = self.request.query_params.get('rarity')
if rarity:
queryset = queryset.filter(rarity=rarity)
return queryset
@action(detail=True, methods=['get'])
def check_achievement(self, request, pk=None):
"""
检查当前用户是否获得了指定成就
"""
achievement = self.get_object()
has_achievement = UserAchievement.objects.filter(
user=request.user,
achievement=achievement
).exists()
return Response({
'has_achievement': has_achievement,
'achievement': AchievementSerializer(achievement).data
})
class UserAchievementViewSet(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
"""
用户成就相关视图集
"""
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [RedisTokenAuthentication]
def get_serializer_class(self):
if self.action == 'create_achievement':
return UserAchievementCreateSerializer
elif self.action == 'list':
return UserAchievementListSerializer
return UserAchievementSerializer
def get_queryset(self):
"""获取当前用户的成就"""
return UserAchievement.objects.filter(user=self.request.user)
@action(detail=False, methods=['post'], permission_classes=[permissions.IsAdminUser])
def create_achievement(self, request):
"""
管理员手动授予用户成就
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['post'])
def check_and_grant(self, request):
"""
检查并授予成就
前端传递相关数据,后端根据成就条件检查是否满足条件并授予成就
"""
achievement_id = request.data.get('achievement_id')
check_data = request.data.get('check_data', {})
if not achievement_id:
return Response(
{"detail": "必须提供成就ID"},
status=status.HTTP_400_BAD_REQUEST
)
# 获取成就
achievement = get_object_or_404(Achievement, id=achievement_id)
# 检查用户是否已经获得该成就
if UserAchievement.objects.filter(
user=request.user,
achievement=achievement
).exists():
return Response(
{"detail": "用户已获得该成就"},
status=status.HTTP_200_OK
)
# TODO: 根据成就的conditions字段和前端传递的check_data检查是否满足条件
# 这部分逻辑需要根据具体成就类型和条件来实现
# 这里只是一个简化的示例
conditions_met = True # 默认满足条件
if conditions_met:
# 满足条件,授予成就
user_achievement = UserAchievement.objects.create(
user=request.user,
achievement=achievement,
acquisition_data=check_data
)
return Response({
"detail": "成就获得成功",
"achievement": AchievementSerializer(achievement).data
}, status=status.HTTP_201_CREATED)
else:
return Response({
"detail": "未满足成就条件"
}, status=status.HTTP_400_BAD_REQUEST)
@action(detail=False, methods=['get'])
def unread_notifications(self, request):
"""
获取用户未读的成就通知
"""
unread_achievements = UserAchievement.objects.filter(
user=request.user,
notification_shown=False
)
serializer = UserAchievementSerializer(unread_achievements, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def mark_notification_shown(self, request, pk=None):
"""
标记成就通知为已显示
"""
user_achievement = self.get_object()
user_achievement.notification_shown = True
user_achievement.save()
return Response({"detail": "成功标记为已显示"})
@action(detail=False, methods=['post'])
def mark_all_notifications_shown(self, request):
"""
标记所有成就通知为已显示
"""
UserAchievement.objects.filter(
user=request.user,
notification_shown=False
).update(notification_shown=True)
return Response({"detail": "所有通知已标记为已显示"})
@action(detail=False, methods=['get'])
def stats(self, request):
"""
获取用户成就统计信息
"""
user = request.user
# 获取成就总数
total_achievements = Achievement.objects.count()
hidden_achievements = Achievement.objects.filter(is_hidden=True).count()
visible_achievements = total_achievements - hidden_achievements
# 获取用户已获得的成就数
user_achievements = UserAchievement.objects.filter(user=user).count()
# 按稀有度统计
rarity_stats = {}
for rarity_code, rarity_name in Achievement.RARITY_CHOICES:
total = Achievement.objects.filter(rarity=rarity_code).count()
acquired = UserAchievement.objects.filter(
user=user,
achievement__rarity=rarity_code
).count()
rarity_stats[rarity_code] = {
'name': rarity_name,
'total': total,
'acquired': acquired,
'percentage': round(acquired / total * 100, 2) if total > 0 else 0
}
# 按类型统计
type_stats = {}
for type_code, type_name in Achievement.TYPE_CHOICES:
total = Achievement.objects.filter(achievement_type=type_code).count()
acquired = UserAchievement.objects.filter(
user=user,
achievement__achievement_type=type_code
).count()
type_stats[type_code] = {
'name': type_name,
'total': total,
'acquired': acquired,
'percentage': round(acquired / total * 100, 2) if total > 0 else 0
}
return Response({
'total_achievements': total_achievements,
'visible_achievements': visible_achievements,
'hidden_achievements': hidden_achievements,
'user_achievements': user_achievements,
'completion_percentage': round(user_achievements / total_achievements * 100, 2) if total_achievements > 0 else 0,
'rarity_stats': rarity_stats,
'type_stats': type_stats
})