1023 lines
42 KiB
Python
1023 lines
42 KiB
Python
from django.shortcuts import render, get_object_or_404
|
||
from django.utils import timezone
|
||
from django.db import transaction
|
||
from django.http import HttpResponse
|
||
from rest_framework import viewsets, status, generics, permissions
|
||
from rest_framework.decorators import api_view, permission_classes, action
|
||
from rest_framework.response import Response
|
||
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
||
import uuid
|
||
import openpyxl
|
||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||
import io
|
||
import logging
|
||
from django.core.files.base import ContentFile
|
||
import secrets
|
||
import string
|
||
from drf_yasg.utils import swagger_auto_schema
|
||
from drf_yasg import openapi
|
||
from rest_framework import serializers
|
||
from common.swagger_utils import swagger_schema
|
||
from userapp.authentication import RedisTokenAuthentication
|
||
import csv
|
||
|
||
from .models import CardTemplate, Card, CardBatch, CardUsageLog
|
||
from .serializers import (
|
||
CardTemplateSerializer, CardTemplateDetailSerializer,
|
||
CardSerializer, CardDetailSerializer, CardBatchSerializer, CardUsageLogSerializer,
|
||
CardScanSerializer, CardUseSerializer, CardBatchGenerateSerializer,
|
||
CardTemplatePublishSerializer, CardBatchPublishSerializer, CardBatchManufactureSerializer,
|
||
CategoryTemplateSerializer, CategoryCardDetailSerializer,
|
||
ClothingAttributesSerializer, PropAttributesSerializer, SongAttributesSerializer,
|
||
DanceAttributesSerializer, FurnitureAttributesSerializer, DecorationAttributesSerializer
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Swagger Schema 定义
|
||
class CardTemplateResponseSchema(serializers.Serializer):
|
||
message = serializers.CharField(help_text="操作结果信息")
|
||
template = CardTemplateSerializer(help_text="卡片模板信息")
|
||
|
||
class CardScanResponseSchema(serializers.Serializer):
|
||
message = serializers.CharField(help_text="操作结果信息")
|
||
card = CardDetailSerializer(help_text="卡片信息")
|
||
|
||
class CardUseResponseSchema(serializers.Serializer):
|
||
message = serializers.CharField(help_text="操作结果信息")
|
||
card = CardDetailSerializer(help_text="卡片信息")
|
||
unlock_info = serializers.JSONField(help_text="解锁信息", required=False)
|
||
|
||
class CardBatchGenerateResponseSchema(serializers.Serializer):
|
||
message = serializers.CharField(help_text="操作结果信息")
|
||
batch = CardBatchSerializer(help_text="卡片批次信息")
|
||
cards_count = serializers.IntegerField(help_text="生成的卡片数量")
|
||
|
||
class CardListResponseSchema(serializers.Serializer):
|
||
count = serializers.IntegerField(help_text="卡片总数")
|
||
next = serializers.URLField(help_text="下一页URL", allow_null=True)
|
||
previous = serializers.URLField(help_text="上一页URL", allow_null=True)
|
||
results = CardSerializer(many=True, help_text="卡片列表")
|
||
|
||
class ErrorResponseSchema(serializers.Serializer):
|
||
error = serializers.CharField(help_text="错误信息")
|
||
|
||
class Meta:
|
||
ref_name = "CardErrorResponse"
|
||
|
||
# 分类参数
|
||
category_param = openapi.Parameter(
|
||
'category', openapi.IN_QUERY,
|
||
description="按分类筛选", type=openapi.TYPE_STRING, required=False
|
||
)
|
||
status_param = openapi.Parameter(
|
||
'status', openapi.IN_QUERY,
|
||
description="按状态筛选", type=openapi.TYPE_STRING, required=False
|
||
)
|
||
template_param = openapi.Parameter(
|
||
'template', openapi.IN_QUERY,
|
||
description="按模板ID筛选", type=openapi.TYPE_INTEGER, required=False
|
||
)
|
||
batch_param = openapi.Parameter(
|
||
'batch', openapi.IN_QUERY,
|
||
description="按批次ID筛选", type=openapi.TYPE_INTEGER, required=False
|
||
)
|
||
user_param = openapi.Parameter(
|
||
'user', openapi.IN_QUERY,
|
||
description="按用户ID筛选", type=openapi.TYPE_INTEGER, required=False
|
||
)
|
||
|
||
# Create your views here.
|
||
|
||
class IsAdminOrReadOnly(permissions.BasePermission):
|
||
"""
|
||
Custom permission to allow admin users to have full access and others to read only.
|
||
"""
|
||
def has_permission(self, request, view):
|
||
if request.method in permissions.SAFE_METHODS:
|
||
return True
|
||
return request.user and request.user.is_staff
|
||
|
||
|
||
class CardTemplateViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
卡片模板管理接口
|
||
|
||
提供卡片模板的增删改查功能。
|
||
支持管理不同类型的卡片模板,包括服装、道具、歌曲等。
|
||
"""
|
||
queryset = CardTemplate.objects.all()
|
||
serializer_class = CardTemplateSerializer
|
||
permission_classes = [IsAdminOrReadOnly]
|
||
authentication_classes = [RedisTokenAuthentication]
|
||
tags = ['card']
|
||
|
||
def get_serializer_class(self):
|
||
"""
|
||
根据不同的操作返回相应的序列化器
|
||
"""
|
||
if self.action == 'retrieve':
|
||
return CardTemplateDetailSerializer
|
||
return self.serializer_class
|
||
|
||
def get_queryset(self):
|
||
queryset = super().get_queryset()
|
||
|
||
# Filter by category if provided
|
||
category = self.request.query_params.get('category', None)
|
||
if category is not None:
|
||
queryset = queryset.filter(category=category)
|
||
|
||
# Filter by status if provided
|
||
status_param = self.request.query_params.get('status', None)
|
||
if status_param is not None:
|
||
queryset = queryset.filter(status=status_param)
|
||
|
||
# Filter by rarity if provided
|
||
rarity = self.request.query_params.get('rarity', None)
|
||
if rarity is not None:
|
||
queryset = queryset.filter(rarity=rarity)
|
||
|
||
# Filter by card_type if provided
|
||
card_type = self.request.query_params.get('card_type', None)
|
||
if card_type is not None:
|
||
queryset = queryset.filter(card_type=card_type)
|
||
|
||
return queryset
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('发布成功', CardTemplateResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema)
|
||
},
|
||
operation_description="将卡片模板发布,使其可用于卡片批次生成",
|
||
tags=['卡片管理'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=True, methods=['post'], permission_classes=[IsAdminUser], authentication_classes=[RedisTokenAuthentication])
|
||
def publish(self, request, pk=None):
|
||
"""
|
||
发布卡片模板
|
||
|
||
将卡片模板发布,使其可用于卡片批次生成。
|
||
"""
|
||
template = self.get_object()
|
||
|
||
if template.status == 'published':
|
||
return Response({
|
||
'error': 'This template is already published.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
template.publish()
|
||
|
||
return Response({
|
||
'message': 'Card template successfully published.',
|
||
'template': CardTemplateSerializer(template).data
|
||
})
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('归档成功', CardTemplateResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema)
|
||
},
|
||
operation_description="将不再使用的卡片模板归档",
|
||
tags=['卡片管理'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=True, methods=['post'], permission_classes=[IsAdminUser], authentication_classes=[RedisTokenAuthentication])
|
||
def archive(self, request, pk=None):
|
||
"""
|
||
归档卡片模板
|
||
|
||
将不再使用的卡片模板归档。
|
||
"""
|
||
template = self.get_object()
|
||
|
||
if template.status == 'archived':
|
||
return Response({
|
||
'error': 'This template is already archived.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
template.archive()
|
||
|
||
return Response({
|
||
'message': 'Card template successfully archived.',
|
||
'template': CardTemplateSerializer(template).data
|
||
})
|
||
|
||
def create(self, request, *args, **kwargs):
|
||
serializer = self.get_serializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
# 先保存主表
|
||
template = serializer.save()
|
||
# 处理专有属性
|
||
category = serializer.validated_data.get('category')
|
||
# 服装
|
||
if category == 'clothing' and 'clothing_attributes' in request.data:
|
||
clothing_data = request.data['clothing_attributes']
|
||
clothing_serializer = ClothingAttributesSerializer(data=clothing_data)
|
||
clothing_serializer.is_valid(raise_exception=True)
|
||
clothing_serializer.save(template=template)
|
||
# 道具
|
||
if category == 'prop' and 'prop_attributes' in request.data:
|
||
prop_data = request.data['prop_attributes']
|
||
prop_serializer = PropAttributesSerializer(data=prop_data)
|
||
prop_serializer.is_valid(raise_exception=True)
|
||
prop_serializer.save(template=template)
|
||
# 歌曲
|
||
if category == 'song' and 'song_attributes' in request.data:
|
||
song_data = request.data['song_attributes']
|
||
song_serializer = SongAttributesSerializer(data=song_data)
|
||
song_serializer.is_valid(raise_exception=True)
|
||
song_serializer.save(template=template)
|
||
# 舞蹈
|
||
if category == 'dance' and 'dance_attributes' in request.data:
|
||
dance_data = request.data['dance_attributes']
|
||
dance_serializer = DanceAttributesSerializer(data=dance_data)
|
||
dance_serializer.is_valid(raise_exception=True)
|
||
dance_serializer.save(template=template)
|
||
# 家具
|
||
if category == 'furniture' and 'furniture_attributes' in request.data:
|
||
furniture_data = request.data['furniture_attributes']
|
||
furniture_serializer = FurnitureAttributesSerializer(data=furniture_data)
|
||
furniture_serializer.is_valid(raise_exception=True)
|
||
furniture_serializer.save(template=template)
|
||
# 装饰
|
||
if category == 'decoration' and 'decoration_attributes' in request.data:
|
||
decoration_data = request.data['decoration_attributes']
|
||
decoration_serializer = DecorationAttributesSerializer(data=decoration_data)
|
||
decoration_serializer.is_valid(raise_exception=True)
|
||
decoration_serializer.save(template=template)
|
||
headers = self.get_success_headers(serializer.data)
|
||
return Response(
|
||
self.get_serializer(template).data,
|
||
status=status.HTTP_201_CREATED,
|
||
headers=headers
|
||
)
|
||
|
||
|
||
class CardViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
卡片管理接口
|
||
|
||
提供卡片的增删改查、扫描和使用等功能。
|
||
支持卡片的生命周期管理。
|
||
"""
|
||
queryset = Card.objects.all()
|
||
serializer_class = CardSerializer
|
||
permission_classes = [IsAdminOrReadOnly]
|
||
authentication_classes = [RedisTokenAuthentication]
|
||
tags = ['card']
|
||
|
||
def get_serializer_class(self):
|
||
"""
|
||
根据不同的操作返回相应的序列化器
|
||
"""
|
||
if self.action == 'retrieve':
|
||
return CategoryCardDetailSerializer
|
||
return self.serializer_class
|
||
|
||
@swagger_auto_schema(
|
||
manual_parameters=[category_param, template_param, batch_param, status_param, user_param],
|
||
responses={
|
||
200: openapi.Response('查询成功', CardListResponseSchema)
|
||
},
|
||
security=[{'Bearer': []}]
|
||
)
|
||
def list(self, request, *args, **kwargs):
|
||
return super().list(request, *args, **kwargs)
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('获取成功', schema=openapi.Schema(
|
||
type=openapi.TYPE_OBJECT,
|
||
description='卡片详情,包含类别专有属性'
|
||
)),
|
||
404: openapi.Response('卡片不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="获取单个卡片的详细信息,包含对应卡片类别的专有属性",
|
||
security=[{'Bearer': []}]
|
||
)
|
||
def retrieve(self, request, *args, **kwargs):
|
||
"""获取单个卡片的详情,包含专有属性"""
|
||
return super().retrieve(request, *args, **kwargs)
|
||
|
||
def get_queryset(self):
|
||
queryset = super().get_queryset()
|
||
|
||
# Filter by category if provided
|
||
category = self.request.query_params.get('category', None)
|
||
if category is not None:
|
||
queryset = queryset.filter(category=category)
|
||
|
||
# Filter by template if provided
|
||
template_id = self.request.query_params.get('template', None)
|
||
if template_id is not None:
|
||
queryset = queryset.filter(template_id=template_id)
|
||
|
||
# Filter by batch if provided
|
||
batch_id = self.request.query_params.get('batch', None)
|
||
if batch_id is not None:
|
||
queryset = queryset.filter(batch_id=batch_id)
|
||
|
||
# Filter by status if provided
|
||
status_param = self.request.query_params.get('status', None)
|
||
if status_param is not None:
|
||
queryset = queryset.filter(status=status_param)
|
||
|
||
# Filter by manufactured status if provided
|
||
manufactured = self.request.query_params.get('manufactured', None)
|
||
if manufactured is not None:
|
||
manufactured = manufactured.lower() == 'true'
|
||
queryset = queryset.filter(manufactured=manufactured)
|
||
|
||
# Filter by user if provided
|
||
user_id = self.request.query_params.get('user', None)
|
||
if user_id is not None:
|
||
queryset = queryset.filter(user_id=user_id)
|
||
|
||
return queryset
|
||
|
||
def get_client_ip(self, request):
|
||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||
if x_forwarded_for:
|
||
ip = x_forwarded_for.split(',')[0]
|
||
else:
|
||
ip = request.META.get('REMOTE_ADDR')
|
||
return ip
|
||
|
||
@swagger_schema(
|
||
request_schema=CardScanSerializer,
|
||
responses={
|
||
200: openapi.Response('扫描成功', CardScanResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('卡片不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="扫描卡片,获取卡片详细信息",
|
||
tags=['卡片使用'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated], authentication_classes=[RedisTokenAuthentication])
|
||
def scan(self, request):
|
||
"""
|
||
扫描卡片
|
||
|
||
通过卡片ID或编码扫描卡片,获取卡片详细信息。
|
||
"""
|
||
serializer = CardScanSerializer(data=request.data)
|
||
if serializer.is_valid():
|
||
unique_id = serializer.validated_data.get('unique_id')
|
||
|
||
try:
|
||
if unique_id:
|
||
card = Card.objects.get(unique_id=unique_id)
|
||
else:
|
||
return Response({
|
||
'error': 'unique_id must be provided.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 记录扫描操作
|
||
CardUsageLog.objects.create(
|
||
user=request.user,
|
||
action='scan',
|
||
old_status=card.status,
|
||
new_status=card.status,
|
||
ip_address=self.get_client_ip(request)
|
||
)
|
||
|
||
return Response({
|
||
'message': 'Card scanned successfully.',
|
||
'card': CardDetailSerializer(card).data
|
||
})
|
||
except Card.DoesNotExist:
|
||
return Response({
|
||
'error': 'Card not found.'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
@swagger_schema(
|
||
request_schema=CardScanSerializer,
|
||
responses={
|
||
200: openapi.Response('扫描成功', CardScanResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('卡片不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="公开扫描卡片API,无需鉴权,通过unique_id获取卡片详细信息",
|
||
tags=['公开接口']
|
||
)
|
||
@action(detail=False, methods=['post'], permission_classes=[], authentication_classes=[])
|
||
def scan_public(self, request):
|
||
"""
|
||
公开扫描卡片 (无需鉴权)
|
||
|
||
通过卡片的唯一标识(unique_id)扫描卡片,无需鉴权,获取卡片详细信息。
|
||
仅当卡片状态为'active'(可用)时才返回卡片详情,否则返回相应的提示信息。
|
||
"""
|
||
serializer = CardScanSerializer(data=request.data)
|
||
if serializer.is_valid():
|
||
unique_id = serializer.validated_data.get('unique_id')
|
||
|
||
try:
|
||
card = Card.objects.get(unique_id=unique_id)
|
||
|
||
# 检查卡片状态
|
||
if card.status != 'active':
|
||
# 不同状态返回不同的提示信息
|
||
status_messages = {
|
||
'inactive': '此卡片尚未激活,暂时不可用。',
|
||
'used': '此卡片已被使用。',
|
||
'void': '此卡片已作废。'
|
||
}
|
||
message = status_messages.get(card.status, '此卡片当前不可用。')
|
||
return Response({
|
||
'error': message
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 状态检查通过,返回卡片详情
|
||
return Response({
|
||
'message': '卡片扫描成功。',
|
||
'card': CardDetailSerializer(card).data
|
||
})
|
||
|
||
except Card.DoesNotExist:
|
||
return Response({
|
||
'error': '卡片不存在。'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
@swagger_schema(
|
||
request_schema=CardUseSerializer,
|
||
responses={
|
||
200: openapi.Response('使用成功', CardUseResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('卡片不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="使用卡片,激活卡片功能",
|
||
tags=['卡片使用'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated], authentication_classes=[RedisTokenAuthentication])
|
||
def use(self, request):
|
||
"""
|
||
使用卡片
|
||
|
||
使用卡片,激活卡片功能。
|
||
根据卡片类型提供不同的功能,如解锁歌曲、获取道具等。
|
||
"""
|
||
serializer = CardUseSerializer(data=request.data)
|
||
if serializer.is_valid():
|
||
unique_id = serializer.validated_data.get('unique_id')
|
||
|
||
try:
|
||
with transaction.atomic():
|
||
if unique_id:
|
||
card = Card.objects.select_for_update().get(unique_id=unique_id)
|
||
else:
|
||
return Response({
|
||
'error': 'unique_id must be provided.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查用户是否已经拥有此卡片
|
||
if card.user == request.user:
|
||
return Response({
|
||
'error': '您已经拥有此卡片,无需再次使用。'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查卡片是否属于当前用户
|
||
if card.user and card.user != request.user:
|
||
return Response({
|
||
'error': 'This card belongs to another user.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查卡片是否已经被使用
|
||
if card.status != 'active':
|
||
return Response({
|
||
'error': f'Card cannot be used. Current status: {card.get_status_display()}'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 记录卡片使用前的状态
|
||
old_status = card.status
|
||
|
||
# 如果卡片没有用户,则分配给当前用户
|
||
if not card.user:
|
||
card.user = request.user
|
||
|
||
# 将卡片状态改为已使用
|
||
card.status = 'used'
|
||
card.used_at = timezone.now()
|
||
card.save()
|
||
|
||
# 记录使用操作
|
||
CardUsageLog.objects.create(
|
||
card=card,
|
||
user=request.user,
|
||
action='use',
|
||
old_status=old_status,
|
||
new_status=card.status,
|
||
ip_address=self.get_client_ip(request)
|
||
)
|
||
|
||
return Response({
|
||
'message': 'Card used successfully.',
|
||
'card': CardDetailSerializer(card).data
|
||
})
|
||
|
||
except Card.DoesNotExist:
|
||
return Response({
|
||
'error': 'Card not found.'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"Error using card: {str(e)}")
|
||
return Response({
|
||
'error': str(e)
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
|
||
class CardBatchViewSet(viewsets.ModelViewSet):
|
||
"""
|
||
卡片批次管理接口
|
||
|
||
提供卡片批次的增删改查功能。
|
||
支持卡片批次的生成、导出、发布等功能。
|
||
"""
|
||
queryset = CardBatch.objects.all()
|
||
serializer_class = CardBatchSerializer
|
||
permission_classes = [IsAdminUser]
|
||
authentication_classes = [RedisTokenAuthentication]
|
||
tags = ['card']
|
||
|
||
def get_queryset(self):
|
||
queryset = super().get_queryset()
|
||
|
||
# Filter by template if provided
|
||
template_id = self.request.query_params.get('template', None)
|
||
if template_id is not None:
|
||
queryset = queryset.filter(template_id=template_id)
|
||
|
||
# Filter by status if provided
|
||
status_param = self.request.query_params.get('status', None)
|
||
if status_param is not None:
|
||
queryset = queryset.filter(status=status_param)
|
||
|
||
# Filter by published status if provided
|
||
published = self.request.query_params.get('published', None)
|
||
if published is not None:
|
||
published = published.lower() == 'true'
|
||
queryset = queryset.filter(published=published)
|
||
|
||
# Filter by manufactured status if provided
|
||
manufactured = self.request.query_params.get('manufactured', None)
|
||
if manufactured is not None:
|
||
manufactured = manufactured.lower() == 'true'
|
||
queryset = queryset.filter(manufactured=manufactured)
|
||
|
||
return queryset
|
||
|
||
@swagger_schema(
|
||
request_schema=CardBatchGenerateSerializer,
|
||
responses={
|
||
201: openapi.Response('生成成功', CardBatchGenerateResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('模板不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="根据模板生成卡片批次",
|
||
tags=['卡片管理'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=False, methods=['post'], authentication_classes=[RedisTokenAuthentication])
|
||
def generate(self, request):
|
||
"""
|
||
生成卡片批次
|
||
|
||
根据卡片模板生成一个新的卡片批次,并创建指定数量的卡片。
|
||
"""
|
||
serializer = CardBatchGenerateSerializer(data=request.data)
|
||
if serializer.is_valid():
|
||
template_id = serializer.validated_data['template'].id
|
||
quantity = serializer.validated_data['quantity']
|
||
description = serializer.validated_data.get('description', '')
|
||
|
||
try:
|
||
template = CardTemplate.objects.get(id=template_id)
|
||
|
||
# Check if template is published
|
||
if template.status != 'published':
|
||
return Response({
|
||
'error': 'Template is not published. Only published templates can be used to generate cards.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Create batch
|
||
batch_number = self.generate_batch_number(template)
|
||
batch = CardBatch.objects.create(
|
||
template=template,
|
||
batch_number=batch_number,
|
||
quantity=quantity,
|
||
description=description,
|
||
category=template.category,
|
||
status='draft',
|
||
)
|
||
|
||
# 只创建少量样本卡片,不是全部quantity
|
||
sample_count = min(10, quantity) # 最多创建10张样本卡片
|
||
|
||
# Generate sample cards
|
||
card_ids = []
|
||
for i in range(1, sample_count + 1):
|
||
unique_id = f"{batch_number}-{i:03d}"
|
||
card = Card(
|
||
batch=batch,
|
||
template=template,
|
||
unique_id=unique_id,
|
||
name=template.name,
|
||
category=template.category,
|
||
price=template.price,
|
||
status='inactive', # 默认为未激活状态
|
||
)
|
||
card_ids.append(unique_id)
|
||
card.save()
|
||
|
||
# 设置批次的起始ID和结束ID
|
||
if card_ids:
|
||
# 起始ID是第一张卡的ID
|
||
batch.start_id = f"{batch_number}-001"
|
||
# 结束ID是根据quantity计算的最后一张卡的ID
|
||
batch.end_id = f"{batch_number}-{quantity:03d}"
|
||
batch.save(update_fields=["start_id", "end_id"])
|
||
|
||
return Response({
|
||
'message': 'Batch and cards generated successfully.',
|
||
'batch': CardBatchSerializer(batch).data,
|
||
'cards_count': sample_count
|
||
}, status=status.HTTP_201_CREATED)
|
||
|
||
except CardTemplate.DoesNotExist:
|
||
return Response({
|
||
'error': 'Template not found.'
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
except Exception as e:
|
||
logger.error(f"Error generating batch: {str(e)}")
|
||
return Response({
|
||
'error': f'An error occurred: {str(e)}'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
def generate_batch_number(self, template):
|
||
"""生成唯一的批次号,使用时间戳和模板ID确保唯一性"""
|
||
import random
|
||
prefix = template.category.upper()
|
||
timestamp = timezone.now().strftime('%y%m%d%H%M')
|
||
# 使用模板ID作为后缀的一部分
|
||
batch_count = CardBatch.objects.filter(template=template).count() + 1
|
||
return f"{prefix}-{timestamp}-{template.id}{batch_count:02d}"
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('导出成功'),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('批次不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="导出卡片批次为Excel或CSV文件",
|
||
tags=['卡片管理'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=True, methods=['get'], url_path='export(?:/(?P<export_format>[^/.]+))?', authentication_classes=[RedisTokenAuthentication])
|
||
def export(self, request, pk=None, export_format=None):
|
||
"""
|
||
导出卡片批次
|
||
|
||
将卡片批次导出为Excel或CSV文件,包含批次内所有卡片的详细信息。
|
||
可通过URL路径指定导出格式(如: /export/xlsx 或 /export/csv),支持xlsx和csv。
|
||
默认格式为xlsx。
|
||
"""
|
||
try:
|
||
batch = self.get_object()
|
||
|
||
# Get requested format (default to xlsx)
|
||
export_format = export_format or request.query_params.get('format', 'xlsx').lower()
|
||
|
||
# Get cards in this batch
|
||
cards = Card.objects.filter(batch=batch).order_by('id')
|
||
|
||
# Create a map of existing cards by their unique_id
|
||
card_map = {card.unique_id: card for card in cards}
|
||
|
||
# Define file headers - remove ID and Created At columns
|
||
headers = ['Unique ID', 'Status', 'Template', 'Category', 'Rarity', 'Type']
|
||
|
||
# Get batch number and total quantity
|
||
batch_number = batch.batch_number
|
||
total_quantity = batch.quantity
|
||
|
||
if export_format == 'csv':
|
||
# Create CSV response
|
||
response = HttpResponse(content_type='text/csv')
|
||
response['Content-Disposition'] = f'attachment; filename=card_batch_{batch.id}.csv'
|
||
|
||
# Create CSV writer
|
||
writer = csv.writer(response)
|
||
|
||
# Write header
|
||
writer.writerow(headers)
|
||
|
||
# Write data rows for all cards in batch quantity
|
||
for i in range(1, total_quantity + 1):
|
||
unique_id = f"{batch_number}-{i:03d}"
|
||
|
||
# Check if card exists
|
||
if unique_id in card_map:
|
||
card = card_map[unique_id]
|
||
writer.writerow([
|
||
card.unique_id,
|
||
card.get_status_display(),
|
||
card.template.name if card.template else '',
|
||
card.get_category_display(),
|
||
card.template.get_rarity_display() if card.template else '',
|
||
card.template.get_card_type_display() if card.template else ''
|
||
])
|
||
else:
|
||
# For non-existent cards, create a placeholder row
|
||
writer.writerow([
|
||
unique_id,
|
||
'未激活', # Inactive status
|
||
batch.template.name if batch.template else '',
|
||
batch.get_category_display(),
|
||
batch.template.get_rarity_display() if batch.template else '',
|
||
batch.template.get_card_type_display() if batch.template else ''
|
||
])
|
||
|
||
return response
|
||
|
||
else: # xlsx format
|
||
# Create workbook
|
||
wb = openpyxl.Workbook()
|
||
ws = wb.active
|
||
ws.title = f"Batch {batch.id}"
|
||
|
||
# Add header
|
||
for col_num, column_title in enumerate(headers, 1):
|
||
cell = ws.cell(row=1, column=col_num)
|
||
cell.value = column_title
|
||
cell.font = Font(bold=True)
|
||
cell.alignment = Alignment(horizontal='center')
|
||
cell.fill = PatternFill(start_color='E6E6E6', end_color='E6E6E6', fill_type='solid')
|
||
|
||
# Add data rows for all cards in batch quantity
|
||
for i in range(1, total_quantity + 1):
|
||
row_num = i + 1 # +1 for header row
|
||
unique_id = f"{batch_number}-{i:03d}"
|
||
|
||
# Check if card exists
|
||
if unique_id in card_map:
|
||
card = card_map[unique_id]
|
||
ws.cell(row=row_num, column=1).value = card.unique_id
|
||
ws.cell(row=row_num, column=2).value = card.get_status_display()
|
||
ws.cell(row=row_num, column=3).value = card.template.name if card.template else ''
|
||
ws.cell(row=row_num, column=4).value = card.get_category_display()
|
||
ws.cell(row=row_num, column=5).value = card.template.get_rarity_display() if card.template else ''
|
||
ws.cell(row=row_num, column=6).value = card.template.get_card_type_display() if card.template else ''
|
||
else:
|
||
# For non-existent cards, create a placeholder row
|
||
ws.cell(row=row_num, column=1).value = unique_id
|
||
ws.cell(row=row_num, column=2).value = '未激活' # Inactive status
|
||
ws.cell(row=row_num, column=3).value = batch.template.name if batch.template else ''
|
||
ws.cell(row=row_num, column=4).value = batch.get_category_display()
|
||
ws.cell(row=row_num, column=5).value = batch.template.get_rarity_display() if batch.template else ''
|
||
ws.cell(row=row_num, column=6).value = batch.template.get_card_type_display() if batch.template else ''
|
||
|
||
# Adjust column width
|
||
for column in ws.columns:
|
||
max_length = 0
|
||
column_letter = openpyxl.utils.get_column_letter(column[0].column)
|
||
for cell in column:
|
||
try:
|
||
if len(str(cell.value)) > max_length:
|
||
max_length = len(str(cell.value))
|
||
except:
|
||
pass
|
||
adjusted_width = (max_length + 2) * 1.2
|
||
ws.column_dimensions[column_letter].width = adjusted_width
|
||
|
||
# Save to buffer
|
||
buffer = io.BytesIO()
|
||
wb.save(buffer)
|
||
buffer.seek(0)
|
||
|
||
# Create response
|
||
response = HttpResponse(
|
||
buffer.getvalue(),
|
||
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||
)
|
||
response['Content-Disposition'] = f'attachment; filename=card_batch_{batch.id}.xlsx'
|
||
|
||
return response
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error exporting batch: {str(e)}")
|
||
return Response({
|
||
'error': f'An error occurred: {str(e)}'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('标记成功', CardTemplateResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('批次不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="将批次标记为已制造",
|
||
tags=['卡片管理'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=True, methods=['post'], authentication_classes=[RedisTokenAuthentication])
|
||
def mark_produced(self, request, pk=None):
|
||
"""
|
||
标记批次为已制造
|
||
|
||
将卡片批次标记为已制造,表示实体卡片已经生产完成。
|
||
"""
|
||
try:
|
||
batch = self.get_object()
|
||
|
||
if batch.status == 'produced':
|
||
return Response({
|
||
'error': 'This batch is already marked as produced.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Update batch status to produced
|
||
batch.status = 'produced'
|
||
batch.manufactured_at = timezone.now()
|
||
batch.save()
|
||
|
||
# Update all cards in this batch
|
||
Card.objects.filter(batch=batch).update(status='produced')
|
||
|
||
return Response({
|
||
'message': 'Batch successfully marked as produced.',
|
||
'batch': CardBatchSerializer(batch).data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error marking batch as manufactured: {str(e)}")
|
||
return Response({
|
||
'error': f'An error occurred: {str(e)}'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('发布成功', CardTemplateResponseSchema),
|
||
400: openapi.Response('请求错误', ErrorResponseSchema),
|
||
404: openapi.Response('批次不存在', ErrorResponseSchema)
|
||
},
|
||
operation_description="发布卡片批次,激活其中的卡片",
|
||
tags=['卡片管理'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
@action(detail=True, methods=['post'], authentication_classes=[RedisTokenAuthentication])
|
||
def publish(self, request, pk=None):
|
||
"""
|
||
发布卡片批次
|
||
|
||
发布卡片批次,使批次中的卡片变为可激活状态。
|
||
发布后,卡片可以被用户扫描和使用。
|
||
"""
|
||
try:
|
||
batch = self.get_object()
|
||
|
||
if batch.published:
|
||
return Response({
|
||
'error': 'This batch is already published.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Check if batch is manufactured
|
||
if not batch.manufactured:
|
||
return Response({
|
||
'error': 'Batch must be marked as manufactured before publishing.'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# Update batch and all cards
|
||
batch.published = True
|
||
batch.published_at = timezone.now()
|
||
batch.save()
|
||
|
||
Card.objects.filter(batch=batch, status='inactive').update(
|
||
status='activated',
|
||
activated_at=timezone.now()
|
||
)
|
||
|
||
return Response({
|
||
'message': 'Batch successfully published.',
|
||
'batch': CardBatchSerializer(batch).data
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error publishing batch: {str(e)}")
|
||
return Response({
|
||
'error': f'An error occurred: {str(e)}'
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class UserCardListView(generics.ListAPIView):
|
||
"""
|
||
用户卡片列表接口
|
||
|
||
获取当前用户拥有的所有卡片。
|
||
"""
|
||
serializer_class = CardSerializer
|
||
permission_classes = [IsAuthenticated]
|
||
authentication_classes = [RedisTokenAuthentication]
|
||
tags = ['card']
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('查询成功', CardListResponseSchema)
|
||
},
|
||
operation_description="获取当前用户的所有卡片",
|
||
tags=['卡片使用'],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
def get_queryset(self):
|
||
"""Return the cards owned by the current user"""
|
||
return Card.objects.filter(user=self.request.user)
|
||
|
||
def list(self, request, *args, **kwargs):
|
||
queryset = self.get_queryset()
|
||
|
||
# Filter by category if provided
|
||
category = self.request.query_params.get('category', None)
|
||
if category is not None:
|
||
queryset = queryset.filter(category=category)
|
||
|
||
# Filter by status if provided
|
||
status_param = self.request.query_params.get('status', None)
|
||
if status_param is not None:
|
||
queryset = queryset.filter(status=status_param)
|
||
|
||
serializer = self.get_serializer(queryset, many=True)
|
||
return Response(serializer.data)
|
||
|
||
class CategoryCardTemplateListView(generics.ListAPIView):
|
||
"""
|
||
分类卡片模板列表接口
|
||
|
||
获取特定分类的卡片模板列表。
|
||
支持分页查询。
|
||
可获取各分类特有的属性数据。
|
||
"""
|
||
serializer_class = CategoryTemplateSerializer
|
||
permission_classes = [IsAdminOrReadOnly]
|
||
authentication_classes = [RedisTokenAuthentication]
|
||
tags = ['card']
|
||
|
||
def get_queryset(self):
|
||
"""根据URL参数获取相应分类的卡片模板"""
|
||
category = self.kwargs.get('category')
|
||
queryset = CardTemplate.objects.filter(category=category)
|
||
|
||
# Filter by status if provided
|
||
status_param = self.request.query_params.get('status', None)
|
||
if status_param is not None:
|
||
queryset = queryset.filter(status=status_param)
|
||
|
||
# Filter by rarity if provided
|
||
rarity = self.request.query_params.get('rarity', None)
|
||
if rarity is not None:
|
||
queryset = queryset.filter(rarity=rarity)
|
||
|
||
# Filter by card_type if provided
|
||
card_type = self.request.query_params.get('card_type', None)
|
||
if card_type is not None:
|
||
queryset = queryset.filter(card_type=card_type)
|
||
|
||
return queryset
|
||
|
||
@swagger_schema(
|
||
responses={
|
||
200: openapi.Response('查询成功', schema=openapi.Schema(
|
||
type=openapi.TYPE_OBJECT,
|
||
properties={
|
||
'count': openapi.Schema(type=openapi.TYPE_INTEGER, description='总数'),
|
||
'next': openapi.Schema(type=openapi.TYPE_STRING, description='下一页URL', nullable=True),
|
||
'previous': openapi.Schema(type=openapi.TYPE_STRING, description='上一页URL', nullable=True),
|
||
'results': openapi.Schema(
|
||
type=openapi.TYPE_ARRAY,
|
||
items=openapi.Schema(type=openapi.TYPE_OBJECT, description='卡片模板'),
|
||
),
|
||
},
|
||
)),
|
||
},
|
||
operation_description="获取特定分类的卡片模板列表,包含该分类特有的属性",
|
||
tags=['卡片查询'],
|
||
manual_parameters=[
|
||
openapi.Parameter('status', openapi.IN_QUERY, description="按状态筛选", type=openapi.TYPE_STRING, required=False),
|
||
openapi.Parameter('rarity', openapi.IN_QUERY, description="按稀有度筛选", type=openapi.TYPE_STRING, required=False),
|
||
openapi.Parameter('card_type', openapi.IN_QUERY, description="按卡片类型筛选", type=openapi.TYPE_STRING, required=False),
|
||
],
|
||
security=[{'Bearer': []}]
|
||
)
|
||
def list(self, request, *args, **kwargs):
|
||
"""实现分页的列表查询"""
|
||
return super().list(request, *args, **kwargs)
|