Django食堂外卖系统开发全解析:从ORM模型到Vue集成与部署
2026/9/16 11:19:07 网站建设 项目流程

简介:这是一份面向毕业设计及教学实践的Python Web项目源码,基于Django框架实现食堂外卖订餐流程,覆盖用户下单、支付、订单状态查看等核心功能。资源共737个文件,包含45个vue、41个html、41个py、53个css及164个js等前端页面与后端逻辑文件,并带有36个pyc、2个sql等数据库与运行辅助文件,包体约15.43MB,结构完整便于直接对照学习。项目从Python基础、Django MVT设计模式、ORM模型、URL路由、表单处理到用户认证与权限管理均有体现,同时涉及第三方支付集成、模板渲染与系统部署等关键知识点。已有214人下载学习,适合希望系统掌握Web全流程开发、快速完成课程设计或毕业设计的初学者与中级开发者。通过研读源码并运行调试,可理解真实业务模块的拆分方式、数据表设计与前后端协作模式,并借此梳理从开发到部署的完整工程思路。

1. 从需求到落地:这套Django外卖系统到底解决了什么

拿到一份打着Python基于Django的食堂外卖系统源码.zip的压缩包,第一反应不是解压,而是先想清楚它要解决的业务问题。食堂外卖和普通外卖最大的区别在于“固定食堂、固定餐线、订单自提”,用户在线上看到当日菜品,加购下单,支付后到窗口取餐。这个项目用 Django 完整实现了这条链路:用户注册登录、菜品列表、购物车、订单状态流转、后台管理,前端还掺入了 Vue 组件来提升交互密度。压缩包里那一批.bak备份文件说明项目经历过多次改动,把这些文件恢复后就能得到一个可运行的 Django Web 项目。对于正在做毕业设计的学生,它是最好的“参考实现”,既有 ORM 多表关联,又有支付回调模拟、前后端混编等实战细节。下面我会按模型、路由、视图、前端、部署这条主线拆开讲,每步都能在本地复现。

2. Django模型与ORM:把食堂菜单、订单、支付映射成数据表

2.1 为什么ORM是关键决策

在这个外卖系统里,表与表的关系是非常典型的 1:N 和 N:N:一个用户有多个订单,一个订单包含多个菜品,一个菜品属于一个分类。如果用原生 SQL 手写建表和关联查询,代码量会翻倍,而且后期改字段要到处找ALTER TABLE。Django 内置的 ORM 用 Python 类描述表结构,通过迁移机制把类的变化同步到数据库,这让“用户下单”这类业务逻辑能直接操作对象而不是拼接 SQL 字符串。对毕业设计答辩而言,用 ORM 也更容易解释清楚数据模型,因为models.py本身就等于一份可读的数据库设计文档。

2.2 核心模型定义与关系拆解

恢复源码后,我建议你先看models.py,它定义了整个系统的骨架。按照业务场景,至少需要四张核心表:用户表、菜品表、订单表、订单明细表。用户表直接继承 Django 的AbstractUser,这样能免费获得登录、会话、密码哈希等能力。订单和菜品之间通过OrderItem做中间表,订单只保存总金额和状态,明细保存每个菜品的快照价格。下面这段代码是按外卖系统最常见的方式缩写的:

from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone = models.CharField(max_length=11, blank=True) address = models.CharField(max_length=255, blank=True) class Dish(models.Model): name = models.CharField(max_length=50) price = models.DecimalField(max_digits=7, decimal_places=2) category = models.CharField(max_length=20, choices=[ ('rice', '米饭套餐'), ('noodle', '面食'), ('drink', '饮品'), ]) stock = models.PositiveIntegerField(default=0) on_sale = models.BooleanField(default=True) class Order(models.Model): STATUS_CHOICES = [ ('unpaid', '待支付'), ('paid', '已支付'), ('preparing', '制作中'), ('done', '待取餐'), ('finished', '已完成'), ] user = models.ForeignKey(User, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) total = models.DecimalField(max_digits=9, decimal_places=2) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='unpaid') class OrderItem(models.Model): order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) dish = models.ForeignKey(Dish, on_delete=models.PROTECT) quantity = models.PositiveIntegerField(default=1) price = models.DecimalField(max_digits=7, decimal_places=2)

User继承AbstractUser之后,Django 的 admin 和认证接口都能直接识别。OrderItem.price保存的是下单那一刻的菜品价格,这比关联Dish.price更可靠——菜品改价后历史订单不会受影响。on_delete=models.PROTECT是一个容易忽略的细节,它表示如果菜品已经被某个订单引用,就不允许直接删除该菜品,这避免了后台误删导致订单明细悬空。如果在你的源码里看到on_delete缺失,请补上,否则新版本迁移会直接报错。

2.3 迁移命令与数据库切换陷阱

模型定义完成后,执行两条命令让数据库表真正落盘:

python manage.py makemigrations python manage.py migrate

makemigrations会扫描每个 app 下的模型,生成带序号的文件放进migrations/目录,记录本次变更。migrate则是把这些变更应用到数据库。如果你运行后看到No changes detected,说明模型所在的 app 没有被注册到INSTALLED_APPS,或者你还没执行startapp创建应用。这个项目里如果只有一个 app,建议把所有模型放到一个canteenordersapp 里,方便管理。

开发时默认用 SQLite,零配置直接跑。但很多同学的数据库设计文档里要求用 MySQL,切换时注意:

配置项SQLiteMySQL
ENGINEdjango.db.backends.sqlite3django.db.backends.mysql
NAMEdb.sqlite3库名
USER不需要数据库用户名
PASSWORD不需要数据库密码
HOST不需要服务器地址
PORT不需要3306

切换后经常报Did you install mysqlclient?。Windows 下安装mysqlclient需要编译环境,非常容易失败。我一般改用pymysql,并在项目根目录的__init__.py里加上两行:

import pymysql pymysql.install_as_MySQLdb()

这样 Django 就能把mysqlclient调用转给 PyMySQL 处理。但要注意,PyMySQL 只能用于开发和小并发场景,真实生产环境中还是推荐用系统自带的 MySQLdb 编译包。

3. 路由、视图与表单:把HTTP请求变成业务动作

3.1 URLconf组织方式:从根路由到应用路由

Django 的 URL 设计非常直观,每个 app 都可以有独立的urls.py,再通过根路由include进来。这个外卖系统一般会有一个ordersapp 负责订单流程,一个usersapp 负责登录注册。根urls.py的结构类似:

from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('orders.urls')), path('accounts/', include('django.contrib.auth.urls')), ]

path('api/', include('orders.urls'))的意思是,所有以api/开头的请求,都交给orders/urls.py继续匹配。然后orders/urls.py里定义具体的路径:

from django.urls import path from . import views urlpatterns = [ path('dishes/', views.dish_list, name='dish-list'), path('orders/', views.OrderCreateView.as_view(), name='order-create'), path('orders/<int:pk>/', views.OrderDetailView.as_view(), name='order-detail'), ]

<int:pk>是路径转换器,它会强制 URL 里的这部分必须是整数,并把值作为参数传给视图。如果用户访问orders/abc,Django 直接返回 404,而不是让视图去处理错误。name参数是给这个 URL 起名字,在模板里用{% url 'order-create' %}或者视图里用reverse('order-detail', args=[1])反向解析时都离不开它。如果你在项目里看到大量硬编码的/orders/字符串,建议改成反向解析,这样重构路由时不用全项目去找。

3.2 函数视图与类视图:什么时候该选谁

订单流程里最难写的两个接口是“创建订单”和“订单详情”。函数视图写起来简洁,但 GET 和 POST 逻辑混在一起时容易变得一坨。类视图可以用一个类分别定义getpost方法,结构更清晰。下面的代码展示了两种风格的差异:

# 函数视图 def dish_list(request): dishes = Dish.objects.filter(on_sale=True) return render(request, 'dishes.html', {'dishes': dishes}) # 类视图 from django.views import View from django.http import JsonResponse class OrderCreateView(View): def get(self, request): # 返回下单页面所需的菜品分类 categories = Dish.objects.values_list('category', flat=True).distinct() return JsonResponse({'categories': list(categories)}) def post(self, request): # 解析购物车数据,创建订单,返回支付参数 data = json.loads(request.body) # ... 业务逻辑 return JsonResponse({'order_id': order.id, 'pay_url': pay_url})

类视图把请求方法拆解后用同一个 URL 入口暴露出去,前端只需向/api/orders/发 GET 或 POST。如果你用了 Django REST Framework(DRF),可以直接继承generics.ListCreateAPIView,配合序列化器甚至能省掉手写 JSON 解析的步骤。但需要注意,如果项目本身没有引入 DRF,不要为了用类视图而强行加依赖,手写View已经够用。

3.3 表单校验与支付回调的写法差异

Django 自带的表单系统适合处理传统 POST 表单,比如修改密码、填写配送地址。在食堂外卖里,下单表单项可能包括隐藏的菜品 ID 列表和地址,用forms.Form能集中做数据清洗:

from django import forms class CheckoutForm(forms.Form): items = forms.CharField(widget=forms.HiddenInput) address = forms.CharField(max_length=255) def clean_items(self): raw = self.cleaned_data['items'] ids = raw.split(',') if not ids or ids[0] == '': raise forms.ValidationError('购物车为空') try: cleaned = [int(i) for i in ids] except ValueError: raise forms.ValidationError('菜品ID格式错误') return cleaned

clean_items是字段校验钩子,form.is_valid()调用时自动执行,返回清洗后的数据。这套机制对新手很友好,因为错误提示可以逐个字段返回。但支付回调不一样,第三方支付平台回传的是 JSON 或 XML 放在请求体里,request.POST根本读不到。我见过很多同学把支付回调写成了普通表单视图,导致回调一直失败。正确的写法是用json.loads(request.body)读取原始数据,并且把视图用@csrf_exempt装饰,因为支付服务器不会携带 Django 的 CSRF Token:

import json from django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse @csrf_exempt def payment_callback(request): data = json.loads(request.body) # 验证签名、金额、订单号 order_id = data.get('order_id') # 更新订单状态为已支付 return JsonResponse({'code': 0, 'message': 'ok'})

这里删除 CSRF 防护后,安全性完全依赖签名验证,千万不能省掉签名校验步骤,否则任何人发一个伪造 POST 就能把订单改成已支付。

4. Vue前端与Django的三种集成方式(从.bak文件看项目结构)

4.1 压缩包里的一堆.bak文件说明了什么

打开压缩包,你会看到urls.py.baksettings.py.bakindex.html.bak,还有UpdatePassword.vue.bakIndexMain.vue.bakIndexHeader.vue.bak.bak后缀通常是开发者修改前留下的备份。把这些后缀去掉,就得到一套完整的源码。其中.vue文件的存在说明原项目不是纯 Django 模板,而是引入了 Vue 来构建交互模块。IndexMain.vueIndexHeader.vue这类命名很像是后台管理界面的布局组件,大概率是给食堂管理员用的菜品管理、订单处理页面。

恢复文件时要注意,备份和当前文件可能并存,比如同时有urls.pyurls.py.bak,这时优先用.bak覆盖回来,因为备份往往是可运行的稳定版本。然后用文本编辑器打开settings.py,检查INSTALLED_APPS里是否注册了 Vue 相关的打包输出目录。

4.2 方式一:Django模板内嵌Vue组件

最简单的方式是直接在 Django 模板中通过 CDN 引入 Vue,然后在某个div上挂载实例。适合购物车、菜品搜索这类局部交互。示例如下:

<div id="app"> <div v-for="dish in dishes"> <span>${ dish.name }</span> <button @click="addToCart(dish)">加入购物车</button> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> <script> new Vue({ el: '#app', delimiters: ['${', '}'], data: { dishes: {{ dishes_json|safe }} }, methods: { addToCart(dish) { // 发送加入购物车请求 } } }); </script>

注意这里我把 Vue 的delimiters改成了${ },这样可以避开 Django 模板的{{ }}。如果不改,Vue 会尝试解析 Django 已经渲染过的变量,造成页面显示异常。另一个办法是使用{% verbatim %}把 Vue 表达式包起来,但那样会失去 Django 向 Vue 传参的便利。推荐优先使用delimiters方案,一劳永逸。

4.3 方式二:前后端分离,Django只提供JSON API

如果项目里那些.vue文件是独立编译的,那就意味着前后端完全分离。Django 后端只写 API,比如用 DRF 实现菜品列表接口:

from rest_framework import generics from .models import Dish from .serializers import DishSerializer class DishListAPI(generics.ListAPIView): queryset = Dish.objects.filter(on_sale=True) serializer_class = DishSerializer

前端 Vue 组件通过 axios 请求接口:

export default { data() { return { dishes: [] }; }, mounted() { axios.get('/api/dishes/').then(res => { this.dishes = res.data; }).catch(error => { console.log(error); }); } }

分离模式下最常踩的坑是跨域。前端开发服务器跑在localhost:8080,Django 跑在localhost:8000,浏览器会拦截跨域请求。我一般在 settings.py 里加django-cors-headers

INSTALLED_APPS = [ 'corsheaders', ... ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ... ] CORS_ALLOW_ALL_ORIGINS = True # 仅开发阶段

生产环境必须把CORS_ALLOW_ALL_ORIGINS改成CORS_ALLOWED_ORIGINS的具体域名列表,否则任何人都能跨域调用你的接口,存在安全风险。

4.4 方式三:混合模式,Django渲染框架,Vue负责交互

从压缩包同时有index.html.bak和多个.vue.bak来看,原项目更可能用的是混合模式:Django 模板输出页面主体,Vue 组件嵌入到模板中的特定区域。这种模式兼顾了 Django 的服务端渲染优势(利于 SEO 和后台管理)和 Vue 的响应式交互。实现时,Vue 代码会打包成 JS 文件放进 Django 的static目录,模板里手动引用:

{% load static %} <div id="app"> <BreadCrumbs></BreadCrumbs> <IndexMain></IndexMain> </div> <script src="{% static 'js/chunk-vendors.js' %}"></script> <script src="{% static 'js/app.js' %}"></script>

混合模式的隐患在于 Django 静态文件的 resolve 顺序,如果STATICFILES_DIRS没有配置成 Vue 打包输出目录,访问页面时会 404。你需要检查:

STATICFILES_DIRS = [ BASE_DIR / 'frontend' / 'dist', ]

把构建好的 Vue 产物目录加进去。另外,Vue Router 如果开启 history 模式,刷新页面时 Django 需要把未知路径全部重定向到首页,否则二级路由一点直达时会 404。在urls.py末尾加一个兜底视图就够了:

from django.contrib import admin from django.urls import re_path urlpatterns += [ re_path(r'^.*$', views.spa_fallback), ]

5. 让毕业设计跑起来:环境搭建、部署与验收清单

5.1 恢复.bak文件并检查运行环境

先把所有.bak重命名去掉后缀,Windows 下用ren *.bak *或批处理。项目里自带的安装.bat通常写了两条命令:

pip install -r requirements.txt python manage.py runserver

直接双击运行前,先确认 Python 环境。我推荐用 Anaconda 新建 Python 3.8 环境,避免系统 Python 包冲突:

conda create -n django_meal python=3.8 conda activate django_meal pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

如果pip install时因为网络超时中断,加上--default-timeout=100增加超时时间。这一步是环境问题的高发区,十有八九是依赖版本与 Python 版本不兼容。

5.2 数据库迁移与创建后台账号

万事俱备后,执行迁移:

python manage.py migrate python manage.py createsuperuser

createsuperuser会交互式提示输入用户名、邮箱、密码。这个超级用户就是食堂管理员,登录/admin/后可以管理菜品分类、上下架菜品、查看订单。如果migrate报错,多半是settings.py里的数据库配置和你本地实际不符,检查DATABASES配置项。

5.3 本地运行与功能验收

启动服务:

python manage.py runserver 0.0.0.0:8000

浏览器访问http://127.0.0.1:8000,按这几个业务流验收:

  1. 用户注册并登录,进入食堂主页,看到菜品列表。
  2. 将菜品加入购物车,提交订单,此时订单状态为待支付。
  3. 在后台管理中将该订单标记为已支付,模拟支付流程。
  4. 用户端刷新订单详情,确认状态变为已支付或制作中。

如果页面样式全无,检查settings.pyDEBUG=True时 Django 是否能自动提供给静态文件,以及STATIC_URL是否以/static/结尾。

5.4 部署到服务器:Gunicorn + Nginx 的最小方案

毕业设计演示时,把项目部署到云服务器能加不少印象分。用宝塔面板可以降低操作门槛,但内核步骤是一样的。首先用 Gunicorn 启动 Django:

gunicorn canteen.wsgi:application -w 2 -b 0.0.0.0:8000

canteen.wsgi.application是项目生成的 WSGI 入口;-w 2表示两个 worker 进程;-b绑定监听地址。然后配置 Nginx 反向代理,把 80 端口转发到 8000,并把静态文件目录指向 Django 的STATIC_ROOT

location /static/ { alias /www/wwwroot/your_project/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

部署前务必修改settings.py

DEBUG = False ALLOWED_HOSTS = ['你的域名', '服务器IP']

DEBUG=False后,Django 不再托管静态文件,如果没有 Nginx 的location /static/配置,管理后台和前端都是光秃秃的。这是最常见的线上部署问题。

5.5 验证支付回调的快捷技巧

下单功能容易演示,支付回调却不好模拟。不用真实支付平台时,可以在 Django shell 里手动触发回调逻辑:

python manage.py shell

输入:

from django.test import Client import json c = Client() resp = c.post('/api/payment_callback/', data=json.dumps({ 'order_id': 1, 'amount': '25.00', 'sign': 'fake' }), content_type='application/json') print(resp.status_code, resp.content)

order_id换成真实订单 ID,观察响应。如果能返回{'code': 0}并修改订单状态,说明回调接口无误。这个技巧在答辩现场演示时非常有用,只需要提前准备一段模拟数据,不用依赖外部网络。之后你再把签名校验逻辑补全,就能无缝切换到真实支付平台。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询