- 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>
394 lines
13 KiB
Python
394 lines
13 KiB
Python
from django.shortcuts import render, get_object_or_404
|
||
from rest_framework import viewsets, permissions, status
|
||
from rest_framework.decorators import action
|
||
from rest_framework.response import Response
|
||
from rest_framework.views import APIView
|
||
from django.db.models import Q, Count, Sum
|
||
from django.utils import timezone
|
||
from drf_yasg.utils import swagger_auto_schema
|
||
from drf_yasg import openapi
|
||
from .models import Food, UserFood, FoodUsageLog
|
||
from .serializers import (
|
||
FoodSerializer, FoodListSerializer, UserFoodSerializer,
|
||
FoodUsageLogSerializer, UseFoodSerializer
|
||
)
|
||
from userapp.authentication import RedisTokenAuthentication
|
||
|
||
|
||
class FoodViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
食物管理API
|
||
|
||
提供食物的完整CRUD功能,包括:
|
||
- 获取食物列表
|
||
- 获取食物详情
|
||
- 创建新食物
|
||
- 更新食物信息
|
||
- 删除食物
|
||
- 按类型、稀有度筛选
|
||
- 搜索功能
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
authentication_classes = [RedisTokenAuthentication]
|
||
|
||
def get_serializer_class(self):
|
||
if self.action == 'list':
|
||
return FoodListSerializer
|
||
return FoodSerializer
|
||
|
||
def get_queryset(self):
|
||
"""获取食物列表"""
|
||
# 管理员可以看到所有食物,普通用户只能看到已发布的食物
|
||
if self.request.user.is_staff or self.request.user.is_superuser:
|
||
queryset = Food.objects.all()
|
||
elif self.action in ['list', 'retrieve']:
|
||
queryset = Food.objects.filter(status='published')
|
||
else:
|
||
queryset = Food.objects.all()
|
||
|
||
# 筛选参数
|
||
food_type = self.request.query_params.get('food_type')
|
||
rarity = self.request.query_params.get('rarity')
|
||
search = self.request.query_params.get('search')
|
||
|
||
if food_type:
|
||
queryset = queryset.filter(food_type=food_type)
|
||
if rarity:
|
||
queryset = queryset.filter(rarity=rarity)
|
||
if search:
|
||
queryset = queryset.filter(
|
||
Q(name__icontains=search) |
|
||
Q(description__icontains=search) |
|
||
Q(taste_tags__icontains=search)
|
||
)
|
||
|
||
return queryset.order_by('-created_at')
|
||
|
||
@swagger_auto_schema(
|
||
operation_description="获取食物分类信息",
|
||
responses={200: openapi.Response(
|
||
description="分类信息",
|
||
examples={
|
||
"application/json": {
|
||
"food_types": [["fruit", "水果"], ["vegetable", "蔬菜"]],
|
||
"rarities": [["common", "普通"], ["rare", "稀有"]],
|
||
"statuses": [["draft", "草稿"], ["published", "已发布"]]
|
||
}
|
||
}
|
||
)}
|
||
)
|
||
@action(detail=False, methods=['get'])
|
||
def categories(self, request):
|
||
"""获取食物类型列表"""
|
||
return Response({
|
||
'food_types': Food.TYPE_CHOICES,
|
||
'rarities': Food.RARITY_CHOICES,
|
||
'statuses': Food.STATUS_CHOICES
|
||
})
|
||
|
||
@swagger_auto_schema(
|
||
operation_description="发布食物",
|
||
responses={
|
||
200: openapi.Response(description="发布成功"),
|
||
400: "食物状态不允许发布"
|
||
}
|
||
)
|
||
@action(detail=True, methods=['post'])
|
||
def publish(self, request, pk=None):
|
||
"""发布食物"""
|
||
food = self.get_object()
|
||
if food.status == 'published':
|
||
return Response(
|
||
{'error': '该食物已经是发布状态'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
if food.status == 'archived':
|
||
return Response(
|
||
{'error': '已归档的食物不能发布'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
food.publish()
|
||
serializer = self.get_serializer(food)
|
||
return Response({
|
||
'message': f'食物 "{food.name}" 发布成功',
|
||
'food': serializer.data
|
||
})
|
||
|
||
@swagger_auto_schema(
|
||
operation_description="归档食物",
|
||
responses={
|
||
200: openapi.Response(description="归档成功"),
|
||
400: "食物状态不允许归档"
|
||
}
|
||
)
|
||
@action(detail=True, methods=['post'])
|
||
def archive(self, request, pk=None):
|
||
"""归档食物"""
|
||
food = self.get_object()
|
||
if food.status == 'archived':
|
||
return Response(
|
||
{'error': '该食物已经是归档状态'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
food.archive()
|
||
serializer = self.get_serializer(food)
|
||
return Response({
|
||
'message': f'食物 "{food.name}" 已归档',
|
||
'food': serializer.data
|
||
})
|
||
|
||
|
||
class UserFoodViewSet(viewsets.ReadOnlyModelViewSet):
|
||
"""
|
||
用户食物管理API
|
||
|
||
用户查看自己拥有的食物,包括:
|
||
- 查看拥有的食物列表
|
||
- 查看食物详情和使用状态
|
||
- 查看获得时间和方式
|
||
"""
|
||
serializer_class = UserFoodSerializer
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
"""获取当前用户的食物"""
|
||
return UserFood.objects.filter(
|
||
user=self.request.user
|
||
).select_related('food', 'user').order_by('-obtained_at')
|
||
|
||
|
||
class FoodUsageLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||
"""
|
||
食物使用记录API
|
||
|
||
用户查看自己的食物使用历史,包括:
|
||
- 使用时间和场景
|
||
- 应用的效果
|
||
- 使用备注
|
||
"""
|
||
serializer_class = FoodUsageLogSerializer
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def get_queryset(self):
|
||
"""获取当前用户的使用记录"""
|
||
return FoodUsageLog.objects.filter(
|
||
user=self.request.user
|
||
).select_related('food', 'user').order_by('-used_at')
|
||
|
||
|
||
class UseFoodView(APIView):
|
||
"""
|
||
使用食物API
|
||
|
||
允许用户使用拥有的食物,记录使用效果和场景
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
@swagger_auto_schema(
|
||
operation_description="使用食物",
|
||
request_body=UseFoodSerializer,
|
||
responses={
|
||
200: openapi.Response(
|
||
description="使用成功",
|
||
examples={
|
||
"application/json": {
|
||
"message": "食物使用成功",
|
||
"remaining_quantity": 2,
|
||
"effect_applied": {"health": 10, "energy": 5},
|
||
"usage_log_id": 123
|
||
}
|
||
}
|
||
),
|
||
400: "请求参数错误或食物无法使用"
|
||
}
|
||
)
|
||
def post(self, request):
|
||
"""使用食物"""
|
||
serializer = UseFoodSerializer(data=request.data)
|
||
if not serializer.is_valid():
|
||
return Response(
|
||
serializer.errors,
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
food_id = serializer.validated_data['food_id']
|
||
usage_context = serializer.validated_data.get('usage_context', '')
|
||
notes = serializer.validated_data.get('notes', '')
|
||
|
||
# 获取用户食物
|
||
try:
|
||
user_food = UserFood.objects.get(
|
||
user=request.user,
|
||
food_id=food_id
|
||
)
|
||
except UserFood.DoesNotExist:
|
||
return Response(
|
||
{'error': '您没有这个食物'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 检查是否可以使用
|
||
if not user_food.can_use():
|
||
return Response(
|
||
{'error': '食物无法使用'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 使用食物
|
||
success = user_food.use_food()
|
||
if not success:
|
||
return Response(
|
||
{'error': '食物使用失败'},
|
||
status=status.HTTP_400_BAD_REQUEST
|
||
)
|
||
|
||
# 创建使用记录
|
||
usage_log = FoodUsageLog.objects.create(
|
||
user=request.user,
|
||
food=user_food.food,
|
||
usage_context=usage_context,
|
||
notes=notes,
|
||
effect_applied=user_food.food.boost_attributes
|
||
)
|
||
|
||
return Response({
|
||
'message': '食物使用成功',
|
||
'remaining_quantity': user_food.quantity,
|
||
'effect_applied': user_food.food.boost_attributes,
|
||
'usage_log_id': usage_log.id
|
||
})
|
||
|
||
|
||
class MyFoodsView(APIView):
|
||
"""
|
||
我的食物概览API
|
||
|
||
获取用户的食物统计信息和概览数据
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
@swagger_auto_schema(
|
||
operation_description="获取用户的食物统计概览",
|
||
responses={
|
||
200: openapi.Response(
|
||
description="用户食物统计",
|
||
examples={
|
||
"application/json": {
|
||
"total_foods": 15,
|
||
"total_quantity": 48,
|
||
"type_stats": {
|
||
"fruit": {"name": "水果", "count": 5},
|
||
"vegetable": {"name": "蔬菜", "count": 3}
|
||
},
|
||
"rarity_stats": {
|
||
"common": {"name": "普通", "count": 10},
|
||
"rare": {"name": "稀有", "count": 2}
|
||
}
|
||
}
|
||
}
|
||
)
|
||
}
|
||
)
|
||
def get(self, request):
|
||
"""获取用户的食物统计"""
|
||
user_foods = UserFood.objects.filter(user=request.user)
|
||
|
||
# 统计数据
|
||
total_foods = user_foods.count()
|
||
total_quantity = user_foods.aggregate(
|
||
total=Sum('quantity')
|
||
)['total'] or 0
|
||
|
||
# 按类型统计
|
||
type_stats = {}
|
||
for food_type, display_name in Food.TYPE_CHOICES:
|
||
count = user_foods.filter(food__food_type=food_type).count()
|
||
if count > 0:
|
||
type_stats[food_type] = {
|
||
'name': display_name,
|
||
'count': count
|
||
}
|
||
|
||
# 按稀有度统计
|
||
rarity_stats = {}
|
||
for rarity, display_name in Food.RARITY_CHOICES:
|
||
count = user_foods.filter(food__rarity=rarity).count()
|
||
if count > 0:
|
||
rarity_stats[rarity] = {
|
||
'name': display_name,
|
||
'count': count
|
||
}
|
||
|
||
# 最近获得的食物
|
||
recent_foods = user_foods.order_by('-obtained_at')[:5]
|
||
recent_foods_data = UserFoodSerializer(recent_foods, many=True).data
|
||
|
||
return Response({
|
||
'total_foods': total_foods,
|
||
'total_quantity': total_quantity,
|
||
'type_stats': type_stats,
|
||
'rarity_stats': rarity_stats,
|
||
'recent_foods': recent_foods_data
|
||
})
|
||
|
||
|
||
class FoodStatsView(APIView):
|
||
"""
|
||
食物系统统计API
|
||
|
||
获取整个食物系统的统计数据
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
@swagger_auto_schema(
|
||
operation_description="获取食物系统统计数据",
|
||
responses={
|
||
200: openapi.Response(
|
||
description="系统统计数据",
|
||
examples={
|
||
"application/json": {
|
||
"total_foods": 50,
|
||
"total_users_with_foods": 20,
|
||
"total_usage_count": 150,
|
||
"popular_foods": [
|
||
{"food__name": "苹果", "food__id": 1, "user_count": 15}
|
||
],
|
||
"most_used_foods": [
|
||
{"food__name": "面包", "food__id": 2, "usage_count": 30}
|
||
]
|
||
}
|
||
}
|
||
)
|
||
}
|
||
)
|
||
def get(self, request):
|
||
"""获取食物系统统计数据"""
|
||
# 总体统计
|
||
total_foods = Food.objects.filter(status='published').count()
|
||
total_users_with_foods = UserFood.objects.values('user').distinct().count()
|
||
total_usage_count = FoodUsageLog.objects.count()
|
||
|
||
# 最受欢迎的食物(被最多用户拥有)
|
||
popular_foods = (
|
||
UserFood.objects
|
||
.values('food__name', 'food__id')
|
||
.annotate(user_count=Count('user'))
|
||
.order_by('-user_count')[:5]
|
||
)
|
||
|
||
# 使用最多的食物
|
||
most_used_foods = (
|
||
FoodUsageLog.objects
|
||
.values('food__name', 'food__id')
|
||
.annotate(usage_count=Count('id'))
|
||
.order_by('-usage_count')[:5]
|
||
)
|
||
|
||
return Response({
|
||
'total_foods': total_foods,
|
||
'total_users_with_foods': total_users_with_foods,
|
||
'total_usage_count': total_usage_count,
|
||
'popular_foods': popular_foods,
|
||
'most_used_foods': most_used_foods
|
||
})
|