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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f1994c869d
commit
94647e2b4b
+24
-1
@@ -1,3 +1,26 @@
|
||||
from django.contrib import admin
|
||||
from .models import Dish, DishCategory, Order
|
||||
|
||||
# Register your models here.
|
||||
|
||||
@admin.register(DishCategory)
|
||||
class DishCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name', 'icon', 'order']
|
||||
list_editable = ['name', 'icon', 'order']
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
@admin.register(Dish)
|
||||
class DishAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name', 'category', 'view_count', 'created_at']
|
||||
list_filter = ['category', 'created_at']
|
||||
search_fields = ['name', 'description']
|
||||
raw_id_fields = ['category']
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name', 'status', 'party_date', 'created_at', 'completed_at']
|
||||
list_filter = ['status', 'created_at']
|
||||
search_fields = ['name', 'note']
|
||||
readonly_fields = ['created_at', 'completed_at']
|
||||
filter_horizontal = ['dishes']
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-19 05:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('menu', '0006_order_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='party_date',
|
||||
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='聚会时间'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('in_progress', '进行中'), ('completed', '已完成')], db_index=True, default='in_progress', max_length=20, verbose_name='状态'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='dish',
|
||||
index=models.Index(fields=['name'], name='menu_dish_name_6ad09b_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='dish',
|
||||
index=models.Index(fields=['-created_at'], name='menu_dish_created_8b2b23_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='order',
|
||||
index=models.Index(fields=['status', '-created_at'], name='menu_order_status_7e2ff4_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='order',
|
||||
index=models.Index(fields=['party_date'], name='menu_order_party_d_e06c2c_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.11 on 2026-03-19 06:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('menu', '0007_add_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('in_progress', '进行中'), ('completed', '已完成')], default='in_progress', max_length=20, verbose_name='状态'),
|
||||
),
|
||||
]
|
||||
+10
-2
@@ -19,7 +19,7 @@ class DishCategory(models.Model):
|
||||
class Dish(models.Model):
|
||||
"""菜品"""
|
||||
name = models.CharField(max_length=100, verbose_name="菜品名称")
|
||||
category = models.ForeignKey(DishCategory, on_delete=models.SET_NULL, null=True, blank=True, related_name='dishes', verbose_name="分类")
|
||||
category = models.ForeignKey(DishCategory, on_delete=models.SET_NULL, null=True, blank=True, related_name='dishes', verbose_name="分类", db_index=True)
|
||||
cover_image = models.CharField(max_length=500, blank=True, null=True, verbose_name="封面图URL")
|
||||
description = models.TextField(blank=True, verbose_name="做法描述(Markdown格式)")
|
||||
ingredients = models.JSONField(default=list, verbose_name="材料清单")
|
||||
@@ -31,6 +31,10 @@ class Dish(models.Model):
|
||||
verbose_name = "菜品"
|
||||
verbose_name_plural = "菜品"
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['-created_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -46,7 +50,7 @@ class Order(models.Model):
|
||||
name = models.CharField(max_length=100, verbose_name="订单名称")
|
||||
dishes = models.ManyToManyField(Dish, related_name='orders', verbose_name="菜品")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='in_progress', verbose_name="状态")
|
||||
party_date = models.DateTimeField(null=True, blank=True, verbose_name="聚会时间")
|
||||
party_date = models.DateTimeField(null=True, blank=True, verbose_name="聚会时间", db_index=True)
|
||||
participants = models.JSONField(default=list, verbose_name="参与人员")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
completed_at = models.DateTimeField(null=True, blank=True, verbose_name="完成时间")
|
||||
@@ -57,6 +61,10 @@ class Order(models.Model):
|
||||
verbose_name = "订单"
|
||||
verbose_name_plural = "订单"
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['status', '-created_at']),
|
||||
models.Index(fields=['party_date']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
+13
-3
@@ -46,10 +46,20 @@ class OrderSerializer(serializers.ModelSerializer):
|
||||
}
|
||||
|
||||
def get_ingredients_summary(self, obj):
|
||||
return obj.get_ingredients_summary()
|
||||
# Compute in Python using prefetched dishes (avoids N+1)
|
||||
summary = {}
|
||||
for dish in obj.dishes.all():
|
||||
for ing in dish.ingredients:
|
||||
name = ing.get('name', '').strip()
|
||||
amount = ing.get('amount', '').strip()
|
||||
if name:
|
||||
if name in summary:
|
||||
if amount:
|
||||
summary[name] += f" + {amount}"
|
||||
else:
|
||||
summary[name] = amount
|
||||
return [{'name': k, 'amount': v} for k, v in summary.items()]
|
||||
|
||||
def validate_dishes(self, value):
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
dish_ids = validated_data.pop('dish_ids', [])
|
||||
|
||||
@@ -8,7 +8,10 @@ class DishService:
|
||||
@staticmethod
|
||||
def get_queryset(category_id=None, search=None):
|
||||
"""获取菜品查询集"""
|
||||
queryset = Dish.objects.select_related('category')
|
||||
queryset = Dish.objects.select_related('category').only(
|
||||
'id', 'name', 'category', 'category__name', 'cover_image',
|
||||
'description', 'ingredients', 'view_count', 'created_at', 'updated_at'
|
||||
)
|
||||
|
||||
if category_id:
|
||||
queryset = queryset.filter(category_id=category_id)
|
||||
@@ -21,5 +24,3 @@ class DishService:
|
||||
def increment_view_count(dish):
|
||||
"""增加浏览次数"""
|
||||
Dish.objects.filter(pk=dish.pk).update(view_count=F('view_count') + 1)
|
||||
dish.refresh_from_db(fields=['view_count'])
|
||||
return dish
|
||||
|
||||
+3
-3
@@ -3,9 +3,9 @@ from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'categories', views.DishCategoryViewSet)
|
||||
router.register(r'dishes', views.DishViewSet)
|
||||
router.register(r'orders', views.OrderViewSet)
|
||||
router.register(r'categories', views.DishCategoryViewSet, basename='dishcategory')
|
||||
router.register(r'dishes', views.DishViewSet, basename='dish')
|
||||
router.register(r'orders', views.OrderViewSet, basename='order')
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
|
||||
+22
-4
@@ -3,12 +3,14 @@ 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
|
||||
|
||||
@@ -17,17 +19,28 @@ from menu.serializers import DishSerializer, DishCategorySerializer, OrderSerial
|
||||
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)
|
||||
@@ -55,12 +68,13 @@ class DishCategoryViewSet(viewsets.ModelViewSet):
|
||||
"""分类视图"""
|
||||
queryset = DishCategory.objects.all()
|
||||
serializer_class = DishCategorySerializer
|
||||
pagination_class = None
|
||||
|
||||
|
||||
class DishViewSet(viewsets.ModelViewSet):
|
||||
"""菜品视图"""
|
||||
queryset = Dish.objects.all()
|
||||
serializer_class = DishSerializer
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
category_id = self.request.query_params.get('category')
|
||||
@@ -76,8 +90,8 @@ class DishViewSet(viewsets.ModelViewSet):
|
||||
|
||||
class OrderViewSet(viewsets.ModelViewSet):
|
||||
"""订单视图"""
|
||||
queryset = Order.objects.all()
|
||||
serializer_class = OrderSerializer
|
||||
pagination_class = None
|
||||
|
||||
def get_queryset(self):
|
||||
status_filter = self.request.query_params.get('status')
|
||||
@@ -108,6 +122,8 @@ class OrderViewSet(viewsets.ModelViewSet):
|
||||
|
||||
class ParseUrlView(APIView):
|
||||
"""解析外部链接"""
|
||||
throttle_scope = 'parse-url'
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
url = request.data.get('url', '')
|
||||
@@ -115,6 +131,8 @@ class ParseUrlView(APIView):
|
||||
result = ParserService.parse_url(url)
|
||||
return Response(result)
|
||||
except ValueError as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception 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)
|
||||
|
||||
Reference in New Issue
Block a user