作为一个长期混迹餐饮行业的技术开发者,我见过太多传统点餐方式的痛点:服务员手写订单容易出错、高峰期顾客等待时间长、后厨订单堆积混乱...这些实际问题促使我决定用Python+Django打造一套高效的Web点餐系统。经过三个月的开发和实际部署测试,这套系统已经成功在5家不同规模的餐厅落地运行,平均节省人力成本30%,订单处理效率提升50%以上。
为什么选择Django?这个"自带电池"的框架简直就是为餐饮系统量身定制的。它内置的ORM让数据库操作变得异常简单,Admin后台开箱即用,强大的中间件机制完美处理用户认证和权限控制。最重要的是,Django的MTV模式(Model-Template-View)让前后端分离得清清楚楚,后期维护扩展特别方便。
系统采用经典的B/S架构,前端用Vue.js构建响应式界面,后端Django处理核心业务逻辑,MySQL作为数据存储。这种分层设计不仅保证了系统的高性能,也让团队协作开发更加高效。下面这张架构图能帮你快速理解整体设计:
code复制[系统架构图]
前端(Vue.js) ↔ 后端(Django REST Framework) ↔ 数据库(MySQL)
↑
Nginx反向代理
工欲善其事必先利其器,我们的开发环境是这样搭建的:
Python 3.8+环境:建议使用pyenv管理多版本Python,避免系统环境混乱
bash复制# 安装pyenv
curl https://pyenv.run | bash
# 安装指定Python版本
pyenv install 3.8.12
数据库选择:虽然SQLite适合开发测试,但生产环境强烈推荐MySQL 5.7+。一个重要经验是:一定要提前配置好字符集为utf8mb4,否则存储emoji表情时会报错!
sql复制# MySQL配置建议
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
开发工具:PyCharm专业版对Django的支持最好,特别是它的Database工具和Debug功能。社区版用户可以用VSCode+Django插件替代。
前端技术选型:
后端核心技术:
踩坑提醒:Django 4.0+默认不再支持MySQLdb,记得使用mysqlclient或pymysql作为数据库驱动。我推荐mysqlclient,性能更好。
餐饮系统的用户体系通常比较复杂,我们设计了多角色权限控制:
python复制# models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
ROLE_CHOICES = (
('customer', '顾客'),
('staff', '店员'),
('admin', '管理员'),
('chef', '厨师')
)
role = models.CharField(max_length=10, choices=ROLE_CHOICES)
phone = models.CharField(max_length=15, unique=True)
# 微信登录相关字段
openid = models.CharField(max_length=64, blank=True)
认证流程采用JWT(JSON Web Token),比传统的Session更适应当前移动端需求:
python复制# serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token['role'] = user.role
return token
菜品是系统的核心数据,我们设计了灵活的数据结构:
python复制class DishCategory(models.Model):
name = models.CharField(max_length=50)
display_order = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
class Dish(models.Model):
STATUS_CHOICES = (
('available', '可售'),
('sold_out', '售罄'),
('off', '下架')
)
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=8, decimal_places=2)
category = models.ForeignKey(DishCategory, on_delete=models.PROTECT)
description = models.TextField(blank=True)
image = models.ImageField(upload_to='dishes/')
status = models.CharField(max_length=10, choices=STATUS_CHOICES)
spicy_level = models.PositiveSmallIntegerField(default=0) # 辣度0-5
is_featured = models.BooleanField(default=False)
# 库存相关字段
stock = models.PositiveIntegerField(default=0)
daily_limit = models.PositiveIntegerField(null=True, blank=True)
实战经验:图片上传一定要做大小限制和格式校验!我们吃过亏,有用户上传了10MB的图片导致服务器负载飙升。
购物车设计需要考虑并发问题,我们采用Redis作为临时存储:
python复制# cart/utils.py
import redis
from django.conf import settings
redis_conn = redis.StrictRedis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB_CART
)
class CartManager:
@staticmethod
def add_item(user_id, dish_id, quantity=1):
key = f"cart:{user_id}"
redis_conn.hincrby(key, dish_id, quantity)
@staticmethod
def get_cart(user_id):
key = f"cart:{user_id}"
return redis_conn.hgetall(key)
订单生成是核心业务,必须保证事务完整性:
python复制# orders/services.py
from django.db import transaction
class OrderService:
@classmethod
@transaction.atomic
def create_order(cls, user, cart_items, address, remarks):
# 创建订单主表
order = Order.objects.create(
user=user,
total_amount=0,
status='pending',
delivery_address=address,
remarks=remarks
)
# 处理每个订单项
total = 0
for dish_id, quantity in cart_items.items():
dish = Dish.objects.select_for_update().get(pk=dish_id)
if dish.status != 'available':
raise ValueError(f"{dish.name}当前不可售")
# 计算单项总价
item_total = dish.price * int(quantity)
total += item_total
# 创建订单项
OrderItem.objects.create(
order=order,
dish=dish,
quantity=quantity,
price=dish.price,
item_total=item_total
)
# 扣减库存
dish.stock -= int(quantity)
dish.save()
# 更新订单总金额
order.total_amount = total
order.save()
# 清空购物车
CartManager.clear_cart(user.id)
return order
Django Admin虽然开箱即用,但针对餐饮场景需要深度定制:
python复制# admin.py
class DishAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price', 'status', 'spicy_level')
list_filter = ('category', 'status', 'is_featured')
search_fields = ('name', 'description')
readonly_fields = ('sales_count',)
fieldsets = (
(None, {
'fields': ('name', 'category', 'price', 'status')
}),
('详细信息', {
'fields': ('description', 'image', 'spicy_level', 'ingredients')
}),
('库存管理', {
'fields': ('stock', 'daily_limit', 'sales_count')
}),
)
def save_model(self, request, obj, form, change):
# 自动记录最后修改人
if not change:
obj.created_by = request.user
obj.modified_by = request.user
super().save_model(request, obj, form, change)
我们还开发了几个实用的自定义Action:
python复制@admin.action(description='批量上架选中菜品')
def make_available(modeladmin, request, queryset):
queryset.update(status='available')
@admin.action(description='生成菜品二维码')
def generate_qrcode(modeladmin, request, queryset):
from io import BytesIO
import qrcode
for dish in queryset:
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
url = f"{settings.SITE_URL}/menu/{dish.id}/"
qr.add_data(url)
img = qr.make_image(fill_color="black", back_color="white")
# 保存到菜品模型
buffer = BytesIO()
img.save(buffer)
filename = f"qrcodes/dish_{dish.id}.png"
dish.qr_code.save(filename, ContentFile(buffer.getvalue()))
索引优化:在经常查询的字段上添加索引
python复制class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.PROTECT, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
status = models.CharField(max_length=20, db_index=True)
查询优化:使用select_related和prefetch_related减少查询次数
python复制# 不好的写法:会产生N+1查询问题
orders = Order.objects.all()
for order in orders:
print(order.user.username) # 每次循环都会查询用户表
# 优化写法
orders = Order.objects.select_related('user').all()
分页处理:列表接口必须分页
python复制from django.core.paginator import Paginator
def dish_list(request):
page = request.GET.get('page', 1)
per_page = request.GET.get('per_page', 10)
dishes = Dish.objects.filter(status='available')
paginator = Paginator(dishes, per_page)
return paginator.get_page(page)
XSS防护:Django模板自动转义,但API接口需要额外处理
python复制from django.utils.html import escape
def api_view(request):
data = request.POST.get('content')
safe_data = escape(data) # 转义HTML特殊字符
CSRF防护:确保所有修改操作都带有CSRF token
javascript复制// 前端Axios配置
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
SQL注入防护:永远使用ORM或参数化查询
python复制# 危险!绝对不要这样做
query = "SELECT * FROM orders WHERE user_id = %s" % user_id
# 安全做法
orders = Order.objects.filter(user_id=user_id)
我们使用Docker+Nginx+Gunicorn的方案,稳定运行了2年多:
dockerfile复制# Dockerfile
FROM python:3.8-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi"]
Nginx配置关键点:
nginx复制server {
listen 80;
server_name yourdomain.com;
location /static/ {
alias /app/staticfiles/;
}
location /media/ {
alias /app/media/;
}
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
使用Sentry捕获错误,Prometheus+Grafana监控系统健康状态:
python复制# settings.py
INSTALLED_APPS += [
'sentry_sdk',
]
import sentry_sdk
sentry_sdk.init(
dsn="YOUR_DSN",
integrations=[DjangoIntegration()],
traces_sample_rate=1.0,
send_default_pii=True
)
日志配置建议:
python复制LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': '/var/log/django/debug.log',
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
}
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
通过Django REST Framework提供API给小程序端:
python复制# serializers.py
class MiniProgramAuthSerializer(serializers.Serializer):
code = serializers.CharField()
def validate(self, attrs):
# 调用微信接口获取openid
url = "https://api.weixin.qq.com/sns/jscode2session"
params = {
'appid': settings.WX_APPID,
'secret': settings.WX_SECRET,
'js_code': attrs['code'],
'grant_type': 'authorization_code'
}
response = requests.get(url, params=params)
data = response.json()
if 'openid' not in data:
raise serializers.ValidationError("微信登录失败")
attrs['openid'] = data['openid']
return attrs
使用Django Channels实现订单状态实时更新:
python复制# consumers.py
class OrderConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.order_id = self.scope['url_route']['kwargs']['order_id']
self.room_group_name = f'order_{self.order_id}'
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def order_update(self, event):
await self.send(text_data=json.dumps(event))
使用Pandas生成销售报表:
python复制# reports/utils.py
def generate_sales_report(start_date, end_date):
orders = Order.objects.filter(
created_at__range=(start_date, end_date),
status='completed'
).values('created_at__date', 'total_amount')
df = pd.DataFrame.from_records(orders)
df['created_at__date'] = pd.to_datetime(df['created_at__date'])
daily_sales = df.groupby('created_at__date').sum()
# 生成趋势图
plt.figure(figsize=(10, 6))
daily_sales.plot(kind='line')
plt.title('每日销售额趋势')
plt.xlabel('日期')
plt.ylabel('销售额(元)')
buffer = BytesIO()
plt.savefig(buffer, format='png')
return buffer.getvalue()
经过这个项目的完整开发周期,我总结了几个关键经验:
数据库设计要预留扩展空间:初期我们没考虑到菜品变体(如辣度选择),后来不得不修改数据结构。建议在设计阶段就考虑"商品属性"这种灵活的结构。
订单状态机要严谨:我们最初的状态转换设计有漏洞,导致出现过"已取消的订单又被完成"的情况。后来引入了状态机模式:
python复制class OrderStatus:
transitions = {
'pending': ['paid', 'cancelled'],
'paid': ['preparing', 'cancelled'],
'preparing': ['ready', 'cancelled'],
'ready': ['delivering'],
'delivering': ['completed'],
'cancelled': [],
'completed': []
}
测试覆盖率很重要:特别是支付相关功能,一定要模拟各种异常情况。我们使用pytest-django编写了300+测试用例,覆盖率达到了85%。
文档要跟代码同步更新:使用Swagger自动生成API文档:
python复制from drf_yasg import openapi
class OrderListView(APIView):
@swagger_auto_schema(
responses={200: OrderSerializer(many=True)},
manual_parameters=[
openapi.Parameter(
'status',
openapi.IN_QUERY,
description="Filter by status",
type=openapi.TYPE_STRING
)
]
)
def get(self, request):
pass
这套系统从最初的简单点餐功能,逐步发展成包含会员管理、营销活动、数据分析的完整解决方案。最大的收获是理解了餐饮行业的真实需求——技术不是越先进越好,而是要在稳定性和易用性之间找到平衡点。