from django.shortcuts import render, get_object_or_404 from django.utils import timezone from django.db import transaction from django.http import HttpResponse, JsonResponse import json 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, MobileProductSerializer, ClothingAttributesSerializer, PropAttributesSerializer, SongAttributesSerializer, DanceAttributesSerializer, 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 == '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[^/.]+))?', 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) class MobileProductsDownloadView(generics.GenericAPIView): """ 手机端产品数据下载接口 将所有已发布的产品(服装、道具、歌曲、舞蹈、家居装饰、食物) 整合为一个 JSON 文件供手机端下载。 """ permission_classes = [] authentication_classes = [] def get(self, request): from food_app.models import Food from food_app.serializers import FoodSerializer templates = CardTemplate.objects.filter( status='published', ).order_by('category', '-published_at') categories = ['clothing', 'prop', 'song', 'dance', 'decoration'] category_labels = { 'clothing': '服装', 'prop': '道具', 'song': '歌曲', 'dance': '舞蹈', 'decoration': '家居装饰', } def normalize_image_url(items): """统一 image_url 字段:有值则保留,空值设为 null;删除多余的 image 字段""" for item in items: # 兼容 food_app 使用 image 而非 image_url if 'image_url' not in item and 'image' in item: item['image_url'] = item['image'] # 删除原始 image 字段,只保留 image_url item.pop('image', None) # 空字符串统一为 null if not item.get('image_url'): item['image_url'] = None return items data = {} for cat in categories: cat_templates = templates.filter(category=cat) if cat_templates.exists(): serializer = MobileProductSerializer(cat_templates, many=True) items = normalize_image_url(serializer.data) data[cat] = { 'label': category_labels.get(cat, cat), 'count': cat_templates.count(), 'items': items, } # 添加食物数据(来自独立的 food_app 模块) published_foods = Food.objects.filter(status='published').order_by('-published_at') if published_foods.exists(): food_serializer = FoodSerializer(published_foods, many=True) items = normalize_image_url(food_serializer.data) data['food'] = { 'label': '食物', 'count': published_foods.count(), 'items': items, } content = json.dumps(data, ensure_ascii=False, indent=2) response = HttpResponse(content, content_type='application/json; charset=utf-8') response['Content-Disposition'] = 'attachment; filename="products.json"' return response