我做了这么多年Django项目餐厅后台管理系统这种业务其实非常适合拿来当完整案例讲。它不仅覆盖了用户认证、数据建模、权限控制这些后台系统的通用需求还牵扯到订单状态机、库存联动、多条件查询这类真实业务场景。今天我就从零开始把整个基于Django的餐厅后台管理系统拆开揉碎从环境搭建到宝塔部署把代码、踩坑、优化全过一遍希望能给正在做类似项目的朋友一些参考。1. 项目整体设计与思路拆解1.1 为什么选Django而不是其他框架很多朋友一提到Python Web开发第一个想到的就是Flask。确实Flask轻量灵活但餐厅后台管理系统这种项目我反而强烈推荐Django原因很直接后台管理系统拼的就是CRUD效率和权限体系而这些恰恰是Django的强项。Django自带Admin后台模型一注册增删改查界面立刻能用这对餐厅这种需要快速上手的业务场景非常友好。再从团队协作角度讲餐厅后台往往不是一个人维护的。Django的Model-View-Template架构规范清晰数据库迁移、表单校验、认证授权这些模块都是内置的新人接手成本低后期维护也不会变成“屎山”。那句话怎么说来着——Django是“全家桶”但恰恰是这种全家桶让餐厅后台这种需求明确的业务少操很多心。1.2 核心需求梳理与模块拆分餐厅后台管理系统核心场景其实就那几个菜品管理、桌台管理、订单管理、员工账号管理再加上老板最关心的营收统计。我按业务模块拆解大致是这样菜品模块菜品分类热菜、凉菜、主食、饮品、菜品信息名称、价格、图片、是否上架、库存预警。桌台模块桌号、可容纳人数、当前状态空闲/占用/清洁中。订单模块下单、加菜、订单状态流转待支付、制作中、已上菜、已完成、已取消。统计模块按天/按月查询营业额、菜品销量排行榜。权限模块店长、服务员、后厨、收银员不同角色不同权限。这里有个很关键的设计决策订单和菜品的关联用中间表。一个订单可能包含多个菜品一个菜品也可能出现在多个订单里如果你不做中间表订单状态一改关联数据全乱了。1.3 技术选型背后的取舍我在项目技术选型时做了三个取舍分享出来供大家参考第一前后端要不要分离搜索结果里很多人问基于Django Vue的前后端分离方案。但餐厅后台这类内部系统业务逻辑不复杂、用户量不大上Vue纯属增加复杂度。我最终选择了Django模板 少量原生JavaScript的方案一个服务员用的操作页面没必要搞那么重的工程化。当然如果后面要做小程序端、做App再抽离RESTful API也不迟。第二数据库用SQLite还是MySQL开发阶段我用SQLite偷懒但上线后必须换MySQL。餐厅数据有实时写入、有并发SQLite扛不住。后面我会详细讲mysqlclient这个“大坑”怎么填。第三Admin后台是直接用还是二次开发我的建议先全部用Django Admin打通流程跑通后再针对核心页面做定制。Admin是Django给我们的礼物不用白不用。2. 环境准备与项目初始化2.1 开发环境与依赖版本组合先交代一下我的实测环境版本组合这套组合我踩过不少坑最后稳定运行操作系统CentOS 7.9部署环境/ Windows 10开发环境Python版本3.8.10Django版本3.2.18LTS版本稳定优先MySQL版本5.7宝塔面板7.9.x为什么锁死这个版本组合因为Django 4.x虽然功能更新但mysqlclient的兼容性和第三方库生态还没有完全跟上。做生产项目稳定永远优先。用virtualenv或pipenv创建虚拟环境别把依赖装到全局这是老生常谈但必须说。virtualenv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows2.2 创建项目和应用Django项目里有个概念要分清楚项目Project对应整个网站应用App对应一个功能模块。一个餐厅后台系统我建议拆成多个App这和“一个App干一件事”的设计哲学完全一致。我习惯这么建django-admin startproject restaurant_manage cd restaurant_manage python manage.py startapp dish # 菜品模块 python manage.py startapp table # 桌台模块 python manage.py startapp order # 订单模块 python manage.py startapp stats # 统计模块 python manage.py startapp account # 账号权限模块模块化拆分的优势在后期维护时特别明显。比如老板临时说菜品要加一个“辣度”属性你只需要改dish这个App其他模块完全不受影响。这就是业务边界的价值。2.3 修改settings.py核心配置项目建好后最重要的就是修改settings.py。我一般最先改三处INSTALLED_APPS、DATABASES、LANGUAGE_CODE/TIME_ZONE。# restaurant_manage/settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, dish, table, order, stats, account, ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: restaurant_db, USER: restaurant_user, PASSWORD: 你的数据库密码, HOST: 127.0.0.1, PORT: 3306, } } LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ False # 这个很关键后面细说USE_TZ这个参数很多人不注意默认是TrueDjango会用UTC时间存数据库结果你会发现后台录入的订单时间比北京时间早了8个小时。餐厅系统结算时间错了账都对不上。我直接设置USE_TZ False用本地时间简单粗暴适合纯国内业务。3. 核心模型设计与数据库实现3.1 餐厅业务的四大核心模型模型是整个系统的地基地基建歪了后面全是麻烦。我详细讲讲菜品、桌台、订单、订单明细这四个模型的设计思路。菜品模型核心字段包括名称、分类、价格、图片、是否上架。价格用DecimalField而不是FloatField这个必须强调。FloatField是浮点数会有精度丢失餐饮系统涉及钱一分一毫都不能差。# dish/models.py from django.db import models class DishCategory(models.Model): 菜品分类 name models.CharField(分类名称, max_length50) sort models.IntegerField(排序, default0) class Meta: verbose_name 菜品分类 verbose_name_plural verbose_name def __str__(self): return self.name class Dish(models.Model): 菜品 name models.CharField(菜品名称, max_length100) category models.ForeignKey( DishCategory, on_deletemodels.PROTECT, verbose_name所属分类 ) price models.DecimalField(价格, max_digits8, decimal_places2) image models.ImageField(菜品图片, upload_todishes/%Y/%m/, blankTrue, nullTrue) is_available models.BooleanField(是否上架, defaultTrue) stock models.IntegerField(库存, default0) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: verbose_name 菜品 verbose_name_plural verbose_name def __str__(self): return self.name分类外键的on_deletemodels.PROTECT这里要解释一下。PROTECT的意思是这个分类下如果有菜品就不允许删除分类只能先把菜品清空才能删分类。为什么不用CASCADE因为餐厅操作失误删了一个分类结果下面几十个菜品全被连带删了老板绝对会崩溃。PROTECT能强制约束操作流程防止手滑。桌台模型重点是状态字段。桌台状态用IntegerField存数字通过choices做映射而不是直接用字符串这样查询效率更高。# table/models.py from django.db import models class DiningTable(models.Model): TABLE_STATUS ( (0, 空闲), (1, 占用), (2, 清洁中), ) table_no models.CharField(桌号, max_length10, uniqueTrue) seats models.IntegerField(可容纳人数, default4) status models.IntegerField(状态, choicesTABLE_STATUS, default0) class Meta: verbose_name 桌台 verbose_name_plural verbose_name def __str__(self): return f{self.table_no}号桌订单模型和订单明细模型这是整个系统最核心的部分。订单保存的是“主信息”哪桌点的、谁来操作、总价、状态、下单时间订单明细保存的是“具体的菜”点了哪几个菜、各几份。这两者的关系是典型的“一主多从”必须拆成两张表不能把菜品信息直接塞到订单里。# order/models.py from django.db import models from django.contrib.auth.models import User from dish.models import Dish from table.models import DiningTable class Order(models.Model): ORDER_STATUS ( (0, 待支付), (1, 制作中), (2, 已上菜), (3, 已完成), (4, 已取消), ) order_no models.CharField(订单号, max_length30, uniqueTrue, editableFalse) table models.ForeignKey( DiningTable, on_deletemodels.PROTECT, verbose_name桌台 ) waiter models.ForeignKey( User, on_deletemodels.PROTECT, verbose_name服务员, related_nameorders ) total_amount models.DecimalField(订单金额, max_digits10, decimal_places2, default0) status models.IntegerField(订单状态, choicesORDER_STATUS, default0) remark models.CharField(备注, max_length200, blankTrue) created_at models.DateTimeField(下单时间, auto_now_addTrue) finished_at models.DateTimeField(完成时间, nullTrue, blankTrue) class Meta: verbose_name 订单 verbose_name_plural verbose_name class OrderItem(models.Model): 订单明细 order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems, verbose_name所属订单) dish models.ForeignKey(Dish, on_deletemodels.PROTECT, verbose_name菜品) dish_name models.CharField(菜品名称快照, max_length100) price models.DecimalField(单价快照, max_digits8, decimal_places2) quantity models.IntegerField(数量, default1) class Meta: verbose_name 订单明细 verbose_name_plural verbose_name这里加了两个“快照”字段dish_name和price。为什么要冗余这两列因为如果哪天菜品改名或调价了历史订单不能跟着变。快照字段能让每一笔订单保留下单那一刻的真实信息这在财务审计时非常重要。这个设计思路叫“历史数据不可变”做业务系统一定得有这个意识。3.2 数据迁移与初始数据准备模型定义好了接下来就是生成数据库表结构。Django的迁移机制简直是开发提速神器改完模型跑两条命令数据库结构自动同步不用手写一句SQL。python manage.py makemigrations python manage.py migrate python manage.py createsuperuser迁移跑完后到MySQL里看一眼你会发现Django自动创建了一堆表auth_user用户表、dish_dish菜品表、order_order订单表等等。注意Django有一套自己的命名规则App名小写 下划线 模型名小写。3.3 把模型注册进Django Admin模型建好了注册到Admin后台立马就能用了。注册时的配置直接决定了后台的易用程度我详细演示一下# dish/admin.py from django.contrib import admin from .models import DishCategory, Dish admin.register(DishCategory) class DishCategoryAdmin(admin.ModelAdmin): list_display (id, name, sort) ordering (sort,) admin.register(Dish) class DishAdmin(admin.ModelAdmin): list_display (id, name, category, price, is_available, stock) list_filter (category, is_available) # 侧边栏筛选器 search_fields (name,) # 搜索框 list_editable (price, is_available, stock) # 列表页直接编辑list_editable这个属性特别实用。菜品价格波动时直接在列表页改价格不用一个个点进编辑页效率翻倍。但注意list_editable的字段不能同时在list_display的第一个字段位置因为第一个字段默认是链接入口。3.4 Admin界面美化——用SimpleUI让后台脱离“原味”Django原生Admin功能没得说就是界面有点“学术”餐厅老板看到会怀疑这工具是不是上个世纪的。热搜里有人问“django admin界面美化”这里我推荐直接用django-simpleui一个第三方美化插件界面现代化中文支持好集成简单。pip install django-simpleui然后在settings.py里把它放到INSTALLED_APPS的最前面INSTALLED_APPS [ simpleui, django.contrib.admin, # ... 其他应用 ] # 隐藏SimpleUI首页默认的升级提示 SIMPLEUI_HOME_INFO False SIMPLEUI_LOGO https://你的logo地址/logo.png刷新后台界面立刻变成一个现代化管理系统还自带仪表盘、图标和菜单配置关键是后端代码一行都不用改。我的实际经验是先花十分钟把Admin定制好再用它来录入初始菜品数据和创建测试订单整个开发过程都会舒服很多。4. 核心业务功能实现与权限控制4.1 用视图函数处理订单创建Admin后台适合管理人员日常维护但服务员点餐不可能用Admin去操作得写一套面向业务人员的操作页面。我以最核心的“创建订单”为例讲讲视图函数的实现思路。订单创建的过程涉及到多张表的数据联动要创建订单主记录、要批量创建订单明细、要更新桌台状态为占用、要扣减菜品库存。这个过程必须保证数据一致性否则容易出现“订单建了但库存没扣”的严重问题。我使用Django的**事务Transaction**来确保所有操作要么全部成功要么全部回滚。# order/views.py from decimal import Decimal from django.db import transaction from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.utils import timezone from .models import Order, OrderItem from dish.models import Dish from table.models import DiningTable login_required def create_order(request): if request.method POST: table_id request.POST.get(table_id) dish_ids request.POST.getlist(dish_id) quantities request.POST.getlist(quantity) remark request.POST.get(remark, ) # 基础校验 if not table_id or not dish_ids: return render(request, order/create_order.html, {error: 请选择桌台和菜品}) # 生成订单号时分秒 随机数避免并发冲突 order_no timezone.now().strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999)) try: with transaction.atomic(): # 1. 创建订单主记录 order Order.objects.create( order_noorder_no, table_idtable_id, waiterrequest.user, remarkremark, status1 # 直接进入制作中状态 ) # 2. 批量创建订单明细同时计算总金额 total Decimal(0.00) for dish_id, qty in zip(dish_ids, quantities): quantity int(qty) if quantity 0: continue dish Dish.objects.select_for_update().get(pkdish_id) # 库存检查 if dish.stock quantity: raise ValueError(f菜品 {dish.name} 库存不足) OrderItem.objects.create( orderorder, dishdish, dish_namedish.name, pricedish.price, quantityquantity ) # 扣减库存 dish.stock - quantity dish.save() total dish.price * quantity # 3. 更新订单金额 order.total_amount total order.save() # 4. 更新桌台状态 DiningTable.objects.filter(pktable_id).update(status1) # 5. 请求厨房打印小票伪代码实际可接入消息队列 # kitchen_print(order) return redirect(order_success, order_idorder.id) except ValueError as e: return render(request, order/create_order.html, {error: str(e)}) # GET请求渲染下单页面 tables DiningTable.objects.filter(status0) # 只看空闲桌台 dishes Dish.objects.filter(is_availableTrue, stock__gt0) return render(request, order/create_order.html, { tables: tables, dishes: dishes, })这里有几个细节值得拎出来说。第一order_no订单号的生成。点餐高峰期可能出现两桌同时下单的情况这时如果订单号只用到秒级时间戳就很容易重复。我加了随机数后缀保证唯一性。更稳妥的做法是用uuid4的短形式但为了方便后厨看单保持纯数字的可读性也很重要。第二select_for_update()行级锁。扣库存这条操作在并发场景下一定要加锁。如果没有锁两个服务员同时给不同桌台点同一个菜品可能都读到了库存为1然后都扣减最后库存变成-1。select_for_update会在数据库层面锁定这一行直到事务结束才释放从根本上解决超卖问题。第三哪些状态该落到数据库。我订单创建后直接把status设为1制作中而不是0待支付。为什么因为现实场景里服务员下单后客人基本不会取消少一个状态就少一次状态流转的出错机会。这也是做系统的一个经验流程越简单越不容易出错。4.2 订单查询与状态流转订单列表页是餐厅后厨的“工作台”要求一眼能看到所有在做的订单。这里我用Django的Queryset熟练拼接条件动态实现多条件查询配合分页做到性能与体验的平衡。# order/views.py from django.core.paginator import Paginator from django.db.models import Q login_required def order_list(request): # 筛选条件 keyword request.GET.get(keyword, ) # 按订单号搜索 status request.GET.get(status, ) # 按状态筛选 date_start request.GET.get(date_start, ) # 开始日期 date_end request.GET.get(date_end, ) # 结束日期 orders Order.objects.select_related(table, waiter).order_by(-created_at) if keyword: orders orders.filter(order_no__icontainskeyword) if status ! and status is not None: orders orders.filter(statusstatus) if date_start: orders orders.filter(created_at__date__gtedate_start) if date_end: orders orders.filter(created_at__date__ltedate_end) # 分页每页20条 paginator Paginator(orders, 20) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, order/order_list.html, { page_obj: page_obj, keyword: keyword, status: status, date_start: date_start, date_end: date_end, status_choices: Order.ORDER_STATUS, })select_related(table, waiter)这种预加载优化很有必要。如果不加每渲染一条订单就去数据库查一次桌台和用户信息N1查询问题直接让页面卡死。加了之后Django会用一次JOIN把关联数据全查出来性能天壤之别。状态流转这里有个容易踩的坑是随手用.delete()方法。QuerySet.delete()是批量删除会直接调SQL的DELETE不走模型的delete方法。而instance.delete()是删除单个模型实例会触发信号和级联逻辑。著名的坑就是Order.objects.filter(status4).delete()删掉了一堆已取消订单结果订单明细还在因为外键指的是具体对象。我处理订单状态变更一律使用update()方法改状态字段绝不用删除。# 取消订单的正确方式改状态而不是删记录 def cancel_order(request, order_id): if request.method POST: Order.objects.filter(pkorder_id).update(status4, finished_attimezone.now()) return redirect(order_list)4.3 角色权限控制餐厅后台的权限控制需求非常清晰店长能看营收报表、能管理菜品服务员只能操作点餐和桌台后厨只能看到制作中的订单。Django自带一套完善的权限系统直接用就行。我推荐用Group用户组做权限管理而不是给一个个用户单独授权。先创建“店长”“服务员”“后厨”三个组再把权限绑定到组上最后把用户拉进组里。这样新员工入职直接进组就有相应权限离职出组就断权非常清爽。在settings.py里配置登录重定向和权限装饰器业务视图加一行装饰器就能开工。# settings.py LOGIN_URL /account/login/ LOGIN_REDIRECT_URL /order/order_list/ LOGOUT_REDIRECT_URL /account/login/ # order/views.py from django.contrib.auth.decorators import permission_required permission_required(order.view_order, raise_exceptionTrue) def order_list(request): ... permission_required(order.add_order, raise_exceptionTrue) def create_order(request): ...权限控制有两点要注意一是视图层和模板层要双重判断视图用装饰器挡住非法请求模板用{% if perms.order.view_order %}控制菜单显示两层都做用户才不会看到“无权访问”的白屏错误。二是超管账号is_superuser天然拥有所有权限测试时别用超管账号测权限逻辑会被误导以为权限没生效。5. 统计报表与数据可视化5.1 按日/月统计营业额老板打开后台第一个想看的绝对不是“最近新增了哪个菜品”而是“今天赚了多少钱”。统计报表就是做老板视角的数据面板。营业额统计的核心逻辑是筛选已完成status3且完成日期在目标时间范围内的订单聚合求和。Django的ORM提供了aggregate和annotate两个聚合工具前者返回单个汇总值后者给每行加统计字段两者配合相当好用。# stats/views.py from django.db.models import Sum, Count from django.db.models.functions import TruncMonth from django.utils import timezone from order.models import Order login_required def daily_stats(request): 按日统计营业额 today timezone.now().date() stats ( Order.objects .filter(status3, finished_at__datetoday) .aggregate( total_revenueSum(total_amount), total_ordersCount(id), ) ) # 菜品销量排行TOP10 from order.models import OrderItem top_dishes ( OrderItem.objects .filter(order__status3, order__finished_at__datetoday) .values(dish_name) .annotate(total_qtySum(quantity)) .order_by(-total_qty)[:10] ) return render(request, stats/daily.html, { today: today, total_revenue: stats[total_revenue] or 0, total_orders: stats[total_orders] or 0, top_dishes: top_dishes, })stats[total_revenue] or 0这行不能省因为如果没有已完成订单Sum的返回值是None直接传给模板会显示“None”用户看到的是个莫名其妙的结果。5.2 月报与曲线图月度报表我习惯用TruncMonth把日期截断到月再按月份分组。为了实现“没有订单的月份也显示0”这种完整报表我通常是查出来之后在Python端按月补零而不是靠SQL硬算。图表展示这块不想引太重的库我直接在模板里接ECharts的CDN。后端把统计数据转换成JSON格式模板里用JavaScript接收并渲染。这一块和前后端分离没冲突只是在前端页面里用了图表库Django照样负责模板渲染和数据提供。import json from django.db.models.functions import TruncMonth def monthly_stats(request): 按月统计最近6个月营业额 months_data ( Order.objects .filter(status3, finished_at__gtetimezone.now() - timezone.timedelta(days180)) .annotate(monthTruncMonth(finished_at)) .values(month) .annotate(totalSum(total_amount)) .order_by(month) ) # 转成图表需要的格式 categories [item[month].strftime(%Y年%m月) for item in months_data] values [float(item[total]) for item in months_data] return render(request, stats/monthly.html, { chart_data: json.dumps({categories: categories, values: values}) })后端输出JSON前端ECharts接收渲染这样一个轻量级报表功能就完成了。注意要把Decimal类型转成float不然json.dumps会直接报错。6. Django与MySQL的坑与部署实战6.1 mysqlclient的安装“三步走”很多人卡在pip install mysqlclient这一步报错报得怀疑人生。这个包需要编译依赖系统的MySQL开发库和C编译器。在宝塔面板的CentOS环境下我建议按这个顺序处理# 1. 安装系统依赖宝塔面板可以用软件商店里的“系统工具”装 yum install -y gcc gcc-c python3-devel mysql-devel # 2. 进入虚拟环境安装 pip install mysqlclient2.0.3 # 3. 如果上面的mysql-devel版本和MySQL版本不匹配尝试指定mysql_config路径 # MYSQLCLIENT_CFLAGS-I/www/server/mysql/include MYSQLCLIENT_LDFLAGS-L/www/server/mysql/lib pip install mysqlclient装好mysqlclient之后记得在__init__.py里声明PyMySQL作为后备方案宝塔自带的MySQL客户端兼容性问题较多PyMySQL更稳定# restaurant_manage/__init__.py import pymysql pymysql.install_as_MySQLdb()6.2 常见问题排查速查表我整理了一下这个项目开发过程中最常见的报错和解决方案报错信息原因分析解决办法Error loading MySQLdb module数据库驱动没装好重装mysqlclient或在__init__.py里导入pymysqlAccess denied for user xxx数据库账号权限不足在MySQL里授权GRANT ALL PRIVILEGES ON restaurant_db.* TO restaurant_userlocalhost;Table restaurant_db.auth_user doesnt exist迁移没跑或者迁移顺序错了执行python manage.py migrate先迁移auth再迁移业务表(1040, Too many connections)连接数耗尽检查settings.py里CONN_MAX_AGE是否设太大调整MySQL的max_connectionsOperationalError: (2006, Server has gone away)MySQL连接超时被断开在settings.py里加CONN_MAX_AGE: 60或关闭后自动重连TimeoutError: [WinError 10060]服务器防火墙/安全组没开放端口宝塔面板放行8000端口云服务商安全组同步放行6.3 宝塔部署DjangoNginx Gunicorn Supervisor到部署环节了。宝塔面板部署Django我用的是Nginx Gunicorn Supervisor这套组合。一些教程用uWSGI但我个人更推荐Gunicorn纯Python实现配置简单配合Nginx做静态文件处理和反向代理非常稳。第一步收集静态文件先把Django的静态文件收集到一个统一目录不然Nginx没法托管它们。# settings.py STATIC_ROOT /www/wwwroot/restaurant_manage/static STATIC_URL /static/然后执行mkdir -p /www/wwwroot/restaurant_manage/static python manage.py collectstatic第二步安装Gunicorn并测试启动pip install gunicorn # 到项目目录下测试启动 cd /www/wwwroot/restaurant_manage gunicorn restaurant_manage.wsgi:application -b 0.0.0.0:8000如果这个命令能正常启动说明Django项目本身没问题后面就是配置Nginx和Supervisor的事了。第三步用Supervisor守护Gunicorn进程Supervisor的作用是让Gunicorn变成常驻服务万一进程挂了能自动拉起。在宝塔软件商店装好Supervisor管理器后创建配置; /etc/supervisord.d/restaurant_manage.ini [program:restaurant_manage] directory/www/wwwroot/restaurant_manage command/www/wwwroot/restaurant_manage/venv/bin/gunicorn restaurant_manage.wsgi:application -b 127.0.0.1:8000 --workers 3 autostarttrue autorestarttrue stderr_logfile/www/wwwroot/restaurant_manage/logs/gunicorn.error.log stdout_logfile/www/wwwroot/restaurant_manage/logs/gunicorn.access.logworkers数量参考公式2 * CPU核心数 1。餐厅后台并发量不大3个worker足够了。设太多反而会因为内存不够导致频繁重启。第四步配置Nginx反代在宝塔面板添加站点Nginx配置如下server { listen 80; server_name your-domain.com; location /static/ { alias /www/wwwroot/restaurant_manage/static/; } location /media/ { alias /www/wwwroot/restaurant_manage/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } access_log /www/wwwlogs/restaurant_manage.access.log; error_log /www/wwwlogs/restaurant_manage.error.log; }这里有个安全细节Gunicorn只监听127.0.0.1不对外直接暴露。外部请求必须经过Nginx转发这样可以省掉一层防火墙配置也更安全。第五步媒体文件的特殊处理Django的ImageField上传的图片默认存到media目录Nginx也要托管它。在settings.py里加MEDIA_URL /media/ MEDIA_ROOT /www/wwwroot/restaurant_manage/media注意在开发模式里Django自己就能处理media文件但生产环境必须交给Nginx这类Web服务器否则图片加载极慢甚至无法显示。6.4 部署后的安全检查清单项目上线后有几件事必须立刻做否则就是裸奔关闭DEBUG调试模式DEBUG False并把ALLOWED_HOSTS设置成你的域名或IP。配置SECRET_KEY不要把settings里的SECRET_KEY公布到Git仓库应该用环境变量方式加载。开启HTTPS宝塔一键申请Let‘s Encrypt免费证书然后把Nginx里80端口的请求301重定向到443。备份数据库宝塔计划任务里配置每天凌晨自动备份MySQL数据库到OSS或本地磁盘保留最近7天版本。餐厅的订单数据丢了就是事故备份策略一定要有。7. 项目心得与扩展建议这个餐厅后台管理系统我从开发到部署大约用了两周时间。做完之后我最大的体会是Django根本不需要“炫技”老老实实把模型设计好、把Admin用好、把权限控制好就已经能达到生产可用水平。很多初学者一上来就想上Vue、上Redis、上Celery结果数据库模型都是歪的业务逻辑一复杂直接崩盘本末倒置了。有几个点我想再强调一遍都是实际项目中换来的教训第一状态字段别用字符串。我见过有人用status models.CharField(max_length10)存“已支付”“未支付”查数据库一眼能看懂但写查询条件时要拼中文容易出错且效率低。用IntegerField存数字配合get_status_display()方法在模板里显示中文既高效又干净。第二Admin后台是生产力不是玩具。初始化数据、跑通流程、甚至给老板演示原型靠Admin就够了。等业务验证没问题再针对高频操作写页面优化体验这个顺序别搞反。第三权限粒度宁粗勿细。餐厅后台的角色就那么几个分得太细反而增加维护成本。Django自带的add/change/delete/view四层权限配合group使用已经能满足90%的场景。别自己造RBAC轮子除非业务真的复杂到那个程度。关于这个项目后续能怎么扩展我根据自己经验提几个方向接入微信小程序点餐后端用Django REST Framework把API层抽出来前端小程序直接对接复用现有订单和菜品模型。增加消息队列餐厅高峰期下单频繁订单创建后的“后厨小票打印”可以丢给Celery异步处理避免请求阻塞。数据大屏餐厅收银台旁边放个大屏显示实时营业额和菜品销量排行ECharts WebSocket实时推送效果很提气。最后说个小事。部署完那天晚上店长跟我说后台挺好用就是登录页面有点慢。我查了一下是页面加载了Google字体和部分远程CDN资源国内网络访问慢。后来把所有静态资源全部本地化登录页秒开。做国内业务系统资源本地化这个细节别忘了。这个项目本身算不上高深但它把Django的核心能力用得很彻底模型设计、Admin定制、ORM查询、权限控制、事务处理、部署上线。能把这一套串下来你对Django的掌握程度就足够应付绝大多数业务后台的需求了。如果大家在做类似项目时遇到具体报错也可以按我上面整理的排查思路去定位大部分问题都是环境或状态流的问题数据库一查就真相大白。