简介:一套基于Python Django框架的食堂外卖系统源码,面向毕业设计、课程实训以及Web开发初学者,围绕用户下单、支付、订单状态管理等真实场景构建,帮助理解Django项目从建模到上线的完整开发链条。压缩包共737个文件,内含41个Python文件用于实现模型与视图逻辑,45个Vue组件和164个JS文件构建前端交互,41个HTML页面配合53个CSS完成页面展示,另有SQL数据库脚本、requirements依赖清单、启动批处理以及多份.bak备份配置,压缩后约15.43MB,整体目录结构按功能划分,便于前后端对照阅读。项目覆盖Django的主要知识点,包括MVT分层设计、ORM数据建模、URL路由、表单验证、模板渲染、用户认证与权限管理,并涉及支付接口集成、静态资源处理及常见部署配置;前端还附带SVG图标、GIF动图、字体文件和图片素材,可直接用于界面完善。已有214人浏览学习,适合作为毕业设计参考或Django综合练习材料,既能练习后端业务逻辑,也能观察前后端协作方式。
1. 这个 Django 食堂外卖系统源码解决的不只是"点餐"问题
拿到一个名为"Python基于Django的食堂外卖系统源码.zip"的压缩包,很多人习惯先解压、用IDE打开一遍代码,结果一头扎进几百个文件里。其实这套系统真正要解决的问题是"如何在校园或园区场景里,把点餐、支付、取餐这条链路用Web方式跑通"。它不只是一个demo,而是一套包含用户端、商家端和管理后台的完整Django项目。对Python工程师来说,它的价值在于让你看到:一个多App的Django项目如何拆分业务模块,ORM如何建模订单与库存,以及反向URL解析怎样把前后端粘合起来。如果你正准备给小型食堂做信息化,或者正在做基于Django的毕业设计,这套源码就是最好的参考起点。接下来我会按照"拆结构 → 搭环境 → 读业务 → 做后台"的顺序,带你把这套源码吃透。
2. 拆解源码:Django 食堂外卖系统的项目结构与数据模型
拿到源码包,先不要急着runserver。你要做的是先理解它的目录结构。Django 项目天生有"项目配置"和"应用"两层界限,一个成熟的食堂外卖系统往往会拆出账号、商品、购物车、订单、支付等独立 App。这一步读懂了,后面二次开发就不会迷路。
2.1 一眼看懂 manage.py 与 app 划分:从入口到业务模块
以常见结构为例:
. ├── manage.py ├── config/ # 项目配置(settings/urls/wsgi) │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── accounts/ # 用户注册登录,扩展User模型 │ ├── canteen/ # 食堂与档口管理 │ ├── menu/ # 菜品、分类、库存 │ ├── cart/ # 购物车 │ ├── order/ # 订单、订单项、状态流转 │ └── payment/ # 支付回调(通常先写占位) ├── static/ ├── media/ ├── requirements.txt └── db.sqlite3 # 部分源码自带SQLite数据库manage.py是 Django 的命令行入口,所有python manage.py ...命令都从这里进入。如果你把业务代码写在apps/下面,说明源码作者用了 App 目录聚合策略,这在大型项目里很常见,目的是防止每个功能都堆在项目根目录。你要做的第一件事就是在终端里执行python manage.py help,看看项目有哪些自定义命令(比如init_data),这在源码包里常被用来初始化食堂和菜品数据。
在 Windows 上想看目录树,可以运行tree /F;在 Linux/macOS 上运行tree -L 2。看到结构后,打开config/settings.py,重点看INSTALLED_APPS列表里有没有django_extensions、rest_framework这样的第三方库,这决定了你后续要装哪些额外依赖。
2.2 核心数据模型:用户扩展、菜品分类、购物车与订单
食堂外卖系统最核心的是"用户-菜品-订单"三角关系。由于 Django 自带的User模型字段有限,源码通常会创建一个Profile类通过一对一来扩展用户信息,比如学号、工号、余额。菜品和分类是典型的多对一:一个分类下有多个菜品,一个菜品只属于一个分类。购物车项则同时外键到用户和菜品,订单与订单项是主从关系。
from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') student_id = models.CharField(max_length=20, blank=True) balance = models.DecimalField(max_digits=8, decimal_places=2, default=0) class Category(models.Model): name = models.CharField(max_length=50) sort = models.IntegerField(default=0) class Dish(models.Model): name = models.CharField(max_length=100) category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='dishes') price = models.DecimalField(max_digits=6, decimal_places=2) stock = models.PositiveIntegerField(default=0) image = models.ImageField(upload_to='dishes/', blank=True) class CartItem(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='cart_items') dish = models.ForeignKey(Dish, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1) class Order(models.Model): STATUS_CHOICES = ( ('pending', '待支付'), ('paid', '已支付'), ('preparing', '备餐中'), ('delivering', '配送中'), ('completed', '已完成'), ('cancelled', '已取消'), ) user = models.ForeignKey(User, on_delete=models.PROTECT, related_name='orders') created_at = models.DateTimeField(auto_now_add=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') total_amount = models.DecimalField(max_digits=8, decimal_places=2) address = models.CharField(max_length=200) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') dish = models.ForeignKey(Dish, on_delete=models.PROTECT) price = models.DecimalField(max_digits=6, decimal_places=2) quantity = models.PositiveIntegerField()上面的模型有几个关键设计:on_delete=models.PROTECT用于菜品被订单引用时禁止删除,这在食堂菜单管理中非常重要,避免历史订单变成"无源之水";购物车没有单独的主表,直接由CartItem以用户为粒度查询,简单够用;订单状态用字符串常量表示,比用数字更易读。你在二次开发时,如果要加"取消原因",只需在Order里增加一个cancel_reason字段即可。
| 模型 | 关键字段 | 关联关系 |
|---|---|---|
| Profile | user(OneToOne), balance | 扩展内置 User |
| Dish | name, category(FK), stock | 多对一 Category |
| CartItem | user(FK), dish(FK), quantity | 用户与菜品多对多关系简化为购物车项 |
| Order | user(FK), status, total | 一用户多订单 |
| OrderItem | order(FK), dish(FK), price | 订单与菜品的多对多通过订单项实现 |
在这个设计中,OrderItem充当了关联表,它同时保存了下单时的菜品价格快照。如果菜单价格调整,历史订单的金额不会跟着变,这是订单系统必须有的特性。理解了这些关系,再去读源码里的views.py,你会发现逻辑都是围绕这些模型展开的。
2.3 用迁移文件同步数据库表结构
理解模型后,下一步是通过 Django 的迁移系统把表建出来。源码压缩包里一般已经携带了每个 App 的migrations目录,里面是形如0001_initial.py的迁移文件。只要你本机安装了 Django,并且设置好了数据库连接,直接执行:
python manage.py makemigrations --check python manage.py migrate--check只检查模型与迁移文件是否有差异,不生成文件,适合用来确认源码里的迁移文件是否完整。如果提示No changes detected,说明模型与迁移一致,可以放心migrate。如果提示缺少迁移,说明源码包可能被删过文件,这时需要手动补makemigrations。
准备好数据库表后,你会看到django_migrations表记录了所有已执行的迁移文件名,Django 靠它知道哪些迁移已经应用。如果你在项目中期换过数据库,就不要直接删迁移文件,否则会出现InconsistentMigrationHistory报错。另一个常见错误是django.db.utils.OperationalError: no such table: menu_dish,这通常是因为你跳过了migrate直接runserver,或者migrate时选了错误的数据库。遇到这种情况,先检查settings.py里DATABASES的指向,再重新执行迁移。
3. 本地跑通源码:Python 环境、依赖安装与 settings 配置
源码项目的 README 可能写得不够详细,但requirements.txt一定是最后的救命稻草。这一章照着做,能在一台干净机器上把系统跑起来。
3.1 虚拟环境与 requirements.txt:一次性装齐依赖
永远不要用全局 Python 去装项目依赖,否则一个项目升级依赖,另一个项目就崩了。先建虚拟环境:
python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt这里假设你已正确安装 Python 3.10+。如果你还没有 Python,可以从官网下载安装包,注意勾选"Add Python to PATH"。装完依赖后,用pip list对比requirements.txt,确认没有缺包。requirements.txt里常见的内容包括:
Django==4.2.10 Pillow==10.2.0 mysqlclient==2.2.0 django-crispy-forms==2.1Django版本要锁死,因为项目可能用到某些只在 4.x 存在的 API。Pillow是处理菜品图片上传的必备库。mysqlclient是 MySQL 驱动,比pymysql稳定,但安装时容易编译失败,见下一节。django-crispy-forms用于渲染 Bootstrap 风格的表单,源码里如果用了它,你在模板中会看到{% load crispy_forms_tags %}。
我常见的问题是 Windows 用户装mysqlclient失败导致整句pip install终止,这时可以用pip install pymysql并在项目的__init__.py里做适配。但更好的办法是:先安装pymysql,再重新执行pip install -r requirements.txt,这样其他依赖也能正常装上。
3.2 切换 MySQL 数据库:解决 mysqlclient 安装问题
源码默认可能使用 SQLite(db.sqlite3),但生产环境你一定想换成 MySQL。在settings.py中找到DATABASES:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'canteen_db', 'USER': 'root', 'PASSWORD': 'yourpassword', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", }, } }注意ENGINE的写法是django.db.backends.mysql,Django 通过这个配置决定使用哪个数据库适配层。如果mysqlclient装不上,可以在项目同名目录的__init__.py里写入:
import pymysql pymysql.install_as_MySQLdb()这样 Django 会把pymysql当作 MySQLdb 使用。但这只是临时方案,pymysql在 Python 3.12 上会有兼容问题,建议有条件还是装mysqlclient。在 Ubuntu/Debian 上,先执行sudo apt install default-libmysqlclient-dev build-essential再pip install mysqlclient;在 CentOS 上是sudo yum install mysql-devel gcc-c++。如果你用的是宝塔面板,可以在"软件商店"里直接安装 MySQL,并在"Python项目管理器"里设置依赖安装,宝塔会自动处理编译环境,省去很多麻烦。
数据库本身也需要先创建好:
CREATE DATABASE canteen_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;utf8mb4是必须的,否则菜名里有 emoji 或生僻字会报Incorrect string value错误。创建完数据库后,再回到项目执行python manage.py migrate。
3.3 创建数据库表、初始化数据与超级管理员
如果源码包里附带fixtures目录(比如data.json),里面通常是食堂、档口、菜品的初始数据。加载它:
python manage.py migrate python manage.py loaddata apps/menu/fixtures/initial_data.jsonloaddata会按 JSON/XML 文件里的模型名和主键插入数据。加载前确认文件里没有重复主键,否则会报IntegrityError。然后创建管理员:
python manage.py createsuperuser按提示输入用户名、邮箱、密码。这一步会写入auth_user表,供你之后登录 Django Admin 后台。
最后启动开发服务器:
python manage.py runserver 0.0.0.0:8000你可以在浏览器访问http://127.0.0.1:8000/看首页,访问/admin/进入管理后台。如果页面样式全丢了,十有八九是STATICFILES_DIRS或MEDIA_URL配置不对。打开settings.py检查:
STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static'] MEDIA_URL = '/images/' MEDIA_ROOT = BASE_DIR / 'media'runserver在调试模式下会自动处理静态文件,但只在DEBUG=True时生效。把DEBUG=True改成False后,静态文件需要由 Nginx 或 WSGI 中间件托管,这是部署时最常见的坑。
4. 核心交易流程的 Django 实现:购物车、订单与反向解析
读完模型和环境,接下来要看源码里最值钱的业务代码——用户怎么从加购到下单再到支付。这一章我会按常见的实现思路讲解,如果你手上的源码实现略有出入,逻辑也逃不出这几步。
4.1 用事务保证下单扣库存的原子性
食堂外卖系统里,用户点击"提交订单"会同时做几件事:读取购物车、生成订单、扣减菜品库存、清空购物车。任何一步失败,都不能留下"订单生成但库存没扣"的脏数据。Django 提供transaction.atomic()来包裹操作。
from django.db import transaction from django.shortcuts import get_object_or_404, redirect from .models import Order, OrderItem, CartItem, Dish @transaction.atomic def create_order(request): cart_items = CartItem.objects.select_related('dish').filter(user=request.user) if not cart_items.exists(): return redirect('cart:detail') total = sum(item.dish.price * item.quantity for item in cart_items) order = Order.objects.create(user=request.user, total_amount=total, status='pending') for item in cart_items: dish = Dish.objects.select_for_update().get(pk=item.dish_id) if dish.stock < item.quantity: raise ValueError(f"菜品 {dish.name} 库存不足") dish.stock -= item.quantity dish.save() OrderItem.objects.create(order=order, dish=dish, price=dish.price, quantity=item.quantity) cart_items.delete() return redirect('order:detail', order_id=order.id)这里有几个关键点:select_related('dish')在查询购物车时就用 SQL 的 JOIN 把菜品信息一次性取出来,避免循环中逐条查库;select_for_update()对菜品行加锁,在数据库层面防止并发下单时库存被超卖;整个函数被@transaction.atomic装饰,任何异常都会回滚订单和库存操作。注意raise ValueError在事务里会触发回滚,但不会吞掉异常,你需要在上层视图或中间件里捕获。
参数order_id在redirect中传递给下一个视图,这依赖 URL 反向解析,见 4.3 节。如果你看到源码里不是用@transaction.atomic装饰器,而是在函数内部调用with transaction.atomic():,效果是一样的,只是作用域不同。前者包裹整个函数,后者只包裹代码块,推荐使用后者,即便于控制事务粒度,也容易阅读。
4.2 订单状态流转与"删除对象"的常见写法
订单不是创建后就完事了,常见状态机是:待支付 → 已支付 → 备餐中 → 配送中 → 已完成,或者用户在未支付时取消。源码里一般设计一个status字段配合save()来更新。以用户取消订单为例:
from django.contrib import messages def cancel_order(request, order_id): order = get_object_or_404(Order, pk=order_id, user=request.user) if order.status not in ('pending', 'paid'): messages.error(request, '当前状态不允许取消') return redirect('order:detail', order_id=order.id) order.status = 'cancelled' order.save() # 归还库存 for item in order.items.all(): dish = item.dish dish.stock += item.quantity dish.save() messages.success(request, '订单已取消') return redirect('order:list')这里要特别强调 Django 删除对象的两种写法。初学者容易把order.delete()当成通用操作,但在订单业务里delete()会把历史订单从库里彻底删除,后续对账和统计就没了。正确的做法是把状态变为cancelled,用"逻辑删除"替代物理删除。如果确实要清理数据,可以调用order.delete(),但要注意它返回一个元组(deleted_count, {model: count}),并且 Django 2.0 以后on_delete=models.CASCADE关联的数据也会一并删除。想要只删除当前行,用Order.objects.filter(pk=order_id).delete()也可以,但它们的 SQL 转换不同:前者先查出所有关联对象,后者直接执行DELETE FROM加上条件。笔者建议业务数据一律不要物理删除,用状态字段替代。
django执行查询-删除对象这个话题常被搜索,其实核心就是两点:QuerySet.delete()返回删除计数,且会级联删除;Model.delete()也会触发信号,但不会像QuerySet.delete()那样对每个对象发送pre_delete信号。如果你在模型里定义了save()的重写或信号,删除时就要特别小心。
4.3 用 reverse 解决订单支付回跳 URL 与 messages 传参
Django 项目中硬编码 URL 是维护灾难。源码里如果看到视图函数里写/order/3/这种路径,说明作者偷懒了;正确的做法是用reverse()生成 URL。reverse的本质是去urls.py里根据name反查匹配规则,比如:
from django.urls import reverse url = reverse('order:detail', args=[order.id])如果项目用了上面这种app_name命名空间,URL 配置通常是:
# urls.py from django.urls import path from . import views app_name = 'order' urlpatterns = [ path('<int:order_id>/', views.order_detail, name='detail'), path('cancel/<int:order_id>/', views.cancel_order, name='cancel'), ]这样即使你调整了 URL 中的前缀,视图里的reverse('order:detail', args=[order_id])依然能生成正确路径。与之相关的是django.urls.resolve的反向操作,它把request.path解析为匹配的视图函数和参数。在调试权限或中间件时,resolve(request.path)非常有用:
from django.urls import resolve match = resolve('/order/5/') print(match.func.__name__) # order_detail print(match.kwargs) # {'order_id': 5}在redirect中传参,很多人会写redirect('/order/' + str(order_id) + '/'),但更优雅的是redirect(reverse('order:detail', args=[order_id]))。如果想同时给用户提示,配合 Django 的messages框架:先messages.success(request, '下单成功'),再redirect,模板里用{% for message in messages %}展示即可。messages的实现依赖 session,记得检查settings.py的INSTALLED_APPS是否包含django.contrib.messages,以及MIDDLEWARE里的SessionMiddleware位置。
如果你的源码是前后端分离项目(比如 Django + Vue),那么这里不需要reverse,而是返回 JSON,由前端路由跳转。但传统 Django 模板项目用reverse是标配。遇到NoReverseMatch错误时,优先检查命名空间、name是否拼写正确,以及 URL 正则是否需要参数。
5. 进阶:用 Django Admin 管理食堂数据,并用 show_urls 验证源码路由
这一章是实战技巧,让你在二次开发时能更快地操作后台、梳理路由,并在上线前做最后验证。
5.1 自定义 Admin 后台:快速上架菜品与处理订单
Django Admin 是整套系统里性价比最高的后台。你只需要在apps/menu/admin.py注册模型,并定义展示列:
from django.contrib import admin from .models import Dish, Category @admin.register(Dish) class DishAdmin(admin.ModelAdmin): list_display = ('name', 'category', 'price', 'stock') list_filter = ('category',) search_fields = ('name',) list_editable = ('price', 'stock')这样食堂工作人员可以直接在/admin/menu/dish/页面修改库存与价格,无需编写前端页面。list_editable让列表页直接可编辑,适合批量调价。订单模型也可以照此定制,加入status的下拉筛选和created_at只读字段。记住一个原则:Admin 只给内部人员用,不要暴露给普通用户,普通用户端一定要单独写视图。
5.2 用 django-extensions 的 show_urls 梳理所有业务端点
面对一个陌生源码包,最快熟悉路由的方式是安装django-extensions:
pip install django-extensions把django_extensions加入INSTALLED_APPS后,运行:
python manage.py show_urls | grep order输出会类似:
/apps/order/<int:order_id>/ apps.order.views.order_detail order:detail /apps/order/cancel/<int:order_id>/ apps.order.views.cancel_order order:cancel你一眼就能看到每个 URL 对应的视图函数和名称,甚至能发现源码里是否有隐藏 App。这个命令是反向解析的调试利器:如果reverse('order:detail')报NoReverseMatch,先运行show_urls确认 URL 配置里是否真的存在这个 name,以及需要多少参数。
5.3 部署前最后检查:静态文件与 DEBUG=False
准备上线时,把settings.py的DEBUG改为False,然后执行:
python manage.py collectstatic --noinput该命令将所有 App 和STATICFILES_DIRS下的静态文件复制到STATIC_ROOT,否则 Nginx 只能拿到空目录。验证方法是启动一个临时服务:
python manage.py runserver 0.0.0.0:8000 --insecure加上--insecure后 Django 会临时托管静态文件,方便你快速检查页面是否变样。但正式环境绝不要用这个参数,而应由 Nginx 配置location /static/ { alias /path/to/staticfiles/; }。最后用python manage.py check --deploy检查是否有安全警告,它会提示SECRET_KEY是否暴露、ALLOWED_HOSTS是否配置、以及是否启用了 HSTS 等。
python manage.py check --deploy对于食堂外卖系统,你还需要确认MEDIA_ROOT在 Nginx 中也配置了别名,这样用户上传的菜品图片才能正常访问。完成以上检查后,就可以用 gunicorn 启动服务,并在/admin/登录后台看数据是否正常。
本文还有配套的精品资源,点击获取