Files
DeveloperandClaude Opus 4.6 94647e2b4b perf: 性能与资源优化 - 提取内联资源、数据库查询优化、限流与缓存增强
主要优化:
- templates/index.html: 220KB → 28KB,内联CSS/JS提取为静态文件
- menu/serializers.py: 修复 Order.ingredients_summary N+1 查询问题
- menu/services/dish_service.py: 移除多余查询,添加 only() 限制字段
- menu/views.py: 上传接口添加每分钟20次限流
- static/js/api.js: 统一API错误处理
- zhangmenu/settings.py: 添加Whitenoise压缩、Redis缓存支持、环境变量配置
- static/css/app.css, static/js/app.js: 提取的内联资源文件
- 新增 migrations, requirements.txt, .env.example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:18:59 +08:00

139 lines
4.9 KiB
Python

from pathlib import Path
from urllib.parse import urljoin
import uuid
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.files.storage import default_storage
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from menu.models import Dish, DishCategory, Order
from menu.serializers import DishSerializer, DishCategorySerializer, OrderSerializer
from menu.services import DishService, OrderService, ParserService
@login_required
def index(request):
"""首页"""
return render(request, 'index.html')
@csrf_exempt
@login_required
def upload_image(request):
"""上传图片"""
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
# Rate limit: 20 uploads per minute per user
from django.core.cache import cache
user_key = f'upload_throttle_{request.user.id}'
cache.add(user_key, 0, 60)
count = cache.get(user_key, 0)
if count >= 20:
return JsonResponse({'error': '上传太频繁,请稍后再试'}, status=429)
cache.set(user_key, count + 1, 60)
image = request.FILES.get('image')
if not image:
return JsonResponse({'error': 'No image provided'}, status=400)
ext = Path(image.name).suffix.lower()
if ext not in settings.ALLOWED_UPLOAD_IMAGE_EXTENSIONS:
return JsonResponse({'error': 'Unsupported image extension'}, status=400)
content_type = (image.content_type or '').lower()
if content_type not in settings.ALLOWED_UPLOAD_IMAGE_CONTENT_TYPES:
return JsonResponse({'error': 'Unsupported image content type'}, status=400)
if image.size > settings.MAX_UPLOAD_IMAGE_SIZE:
return JsonResponse({'error': 'Image file too large'}, status=400)
filename = f"dishes/{uuid.uuid4().hex}{ext}"
saved_path = default_storage.save(filename, image)
media_path = str(saved_path).lstrip('/')
media_url = urljoin(settings.MEDIA_URL, media_path)
return JsonResponse({'url': media_url})
class DishCategoryViewSet(viewsets.ModelViewSet):
"""分类视图"""
queryset = DishCategory.objects.all()
serializer_class = DishCategorySerializer
pagination_class = None
class DishViewSet(viewsets.ModelViewSet):
"""菜品视图"""
serializer_class = DishSerializer
pagination_class = None
def get_queryset(self):
category_id = self.request.query_params.get('category')
search = self.request.query_params.get('search')
return DishService.get_queryset(category_id, search)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
DishService.increment_view_count(instance)
serializer = self.get_serializer(instance)
return Response(serializer.data)
class OrderViewSet(viewsets.ModelViewSet):
"""订单视图"""
serializer_class = OrderSerializer
pagination_class = None
def get_queryset(self):
status_filter = self.request.query_params.get('status')
return OrderService.get_queryset(status_filter)
def create(self, request, *args, **kwargs):
request_data = request.data.copy()
dish_ids = request_data.pop('dish_ids', [])
serializer = self.get_serializer(data=request_data)
serializer.is_valid(raise_exception=True)
order = OrderService.create_order(serializer, dish_ids)
response_serializer = self.get_serializer(order)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=['post'])
def complete(self, request, pk=None):
order = self.get_object()
order = OrderService.complete_order(order)
serializer = self.get_serializer(order)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def share(self, request, pk=None):
order = self.get_object()
serializer = self.get_serializer(order)
return Response(serializer.data)
class ParseUrlView(APIView):
"""解析外部链接"""
throttle_scope = 'parse-url'
permission_classes = [AllowAny]
def post(self, request):
url = request.data.get('url', '')
try:
result = ParserService.parse_url(url)
return Response(result)
except ValueError as e:
return Response({'error': f'解析失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
except TypeError as e:
return Response({'error': f'解析失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': f'解析失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)