Django食堂外卖系统开发全解析:从ORM模型到Vue集成与部署

发布时间:2026/9/16 11:19:09
Django食堂外卖系统开发全解析:从ORM模型到Vue集成与部署
简介这是一份面向毕业设计及教学实践的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_length11, blankTrue) address models.CharField(max_length255, blankTrue) class Dish(models.Model): name models.CharField(max_length50) price models.DecimalField(max_digits7, decimal_places2) category models.CharField(max_length20, choices[ (rice, 米饭套餐), (noodle, 面食), (drink, 饮品), ]) stock models.PositiveIntegerField(default0) on_sale models.BooleanField(defaultTrue) class Order(models.Model): STATUS_CHOICES [ (unpaid, 待支付), (paid, 已支付), (preparing, 制作中), (done, 待取餐), (finished, 已完成), ] user models.ForeignKey(User, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) total models.DecimalField(max_digits9, decimal_places2) status models.CharField(max_length10, choicesSTATUS_CHOICES, defaultunpaid) class OrderItem(models.Model): order models.ForeignKey(Order, related_nameitems, on_deletemodels.CASCADE) dish models.ForeignKey(Dish, on_deletemodels.PROTECT) quantity models.PositiveIntegerField(default1) price models.DecimalField(max_digits7, decimal_places2)User继承AbstractUser之后Django 的 admin 和认证接口都能直接识别。OrderItem.price保存的是下单那一刻的菜品价格这比关联Dish.price更可靠——菜品改价后历史订单不会受影响。on_deletemodels.PROTECT是一个容易忽略的细节它表示如果菜品已经被某个订单引用就不允许直接删除该菜品这避免了后台误删导致订单明细悬空。如果在你的源码里看到on_delete缺失请补上否则新版本迁移会直接报错。2.3 迁移命令与数据库切换陷阱模型定义完成后执行两条命令让数据库表真正落盘python manage.py makemigrations python manage.py migratemakemigrations会扫描每个 app 下的模型生成带序号的文件放进migrations/目录记录本次变更。migrate则是把这些变更应用到数据库。如果你运行后看到No changes detected说明模型所在的 app 没有被注册到INSTALLED_APPS或者你还没执行startapp创建应用。这个项目里如果只有一个 app建议把所有模型放到一个canteen或ordersapp 里方便管理。开发时默认用 SQLite零配置直接跑。但很多同学的数据库设计文档里要求用 MySQL切换时注意配置项SQLiteMySQLENGINEdjango.db.backends.sqlite3django.db.backends.mysqlNAMEdb.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, namedish-list), path(orders/, views.OrderCreateView.as_view(), nameorder-create), path(orders/int:pk/, views.OrderDetailView.as_view(), nameorder-detail), ]int:pk是路径转换器它会强制 URL 里的这部分必须是整数并把值作为参数传给视图。如果用户访问orders/abcDjango 直接返回 404而不是让视图去处理错误。name参数是给这个 URL 起名字在模板里用{% url order-create %}或者视图里用reverse(order-detail, args[1])反向解析时都离不开它。如果你在项目里看到大量硬编码的/orders/字符串建议改成反向解析这样重构路由时不用全项目去找。3.2 函数视图与类视图什么时候该选谁订单流程里最难写的两个接口是“创建订单”和“订单详情”。函数视图写起来简洁但 GET 和 POST 逻辑混在一起时容易变得一坨。类视图可以用一个类分别定义get和post方法结构更清晰。下面的代码展示了两种风格的差异# 函数视图 def dish_list(request): dishes Dish.objects.filter(on_saleTrue) 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, flatTrue).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 FrameworkDRF可以直接继承generics.ListCreateAPIView配合序列化器甚至能省掉手写 JSON 解析的步骤。但需要注意如果项目本身没有引入 DRF不要为了用类视图而强行加依赖手写View已经够用。3.3 表单校验与支付回调的写法差异Django 自带的表单系统适合处理传统 POST 表单比如修改密码、填写配送地址。在食堂外卖里下单表单项可能包括隐藏的菜品 ID 列表和地址用forms.Form能集中做数据清洗from django import forms class CheckoutForm(forms.Form): items forms.CharField(widgetforms.HiddenInput) address forms.CharField(max_length255) 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 cleanedclean_items是字段校验钩子form.is_valid()调用时自动执行返回清洗后的数据。这套机制对新手很友好因为错误提示可以逐个字段返回。但支付回调不一样第三方支付平台回传的是 JSON 或 XML 放在请求体里request.POST根本读不到。我见过很多同学把支付回调写成了普通表单视图导致回调一直失败。正确的写法是用json.loads(request.body)读取原始数据并且把视图用csrf_exempt装饰因为支付服务器不会携带 Django 的 CSRF Tokenimport 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.bak、settings.py.bak、index.html.bak还有UpdatePassword.vue.bak、IndexMain.vue.bak、IndexHeader.vue.bak。.bak后缀通常是开发者修改前留下的备份。把这些后缀去掉就得到一套完整的源码。其中.vue文件的存在说明原项目不是纯 Django 模板而是引入了 Vue 来构建交互模块。IndexMain.vue、IndexHeader.vue这类命名很像是后台管理界面的布局组件大概率是给食堂管理员用的菜品管理、订单处理页面。恢复文件时要注意备份和当前文件可能并存比如同时有urls.py和urls.py.bak这时优先用.bak覆盖回来因为备份往往是可运行的稳定版本。然后用文本编辑器打开settings.py检查INSTALLED_APPS里是否注册了 Vue 相关的打包输出目录。4.2 方式一Django模板内嵌Vue组件最简单的方式是直接在 Django 模板中通过 CDN 引入 Vue然后在某个div上挂载实例。适合购物车、菜品搜索这类局部交互。示例如下div idapp div v-fordish in dishes span${ dish.name }/span button clickaddToCart(dish)加入购物车/button /div /div script srchttps://cdn.jsdelivr.net/npm/vue2/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_saleTrue) 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:8080Django 跑在localhost:8000浏览器会拦截跨域请求。我一般在 settings.py 里加django-cors-headersINSTALLED_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 idapp 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 python3.8 conda activate django_meal pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple如果pip install时因为网络超时中断加上--default-timeout100增加超时时间。这一步是环境问题的高发区十有八九是依赖版本与 Python 版本不兼容。5.2 数据库迁移与创建后台账号万事俱备后执行迁移python manage.py migrate python manage.py createsuperusercreatesuperuser会交互式提示输入用户名、邮箱、密码。这个超级用户就是食堂管理员登录/admin/后可以管理菜品分类、上下架菜品、查看订单。如果migrate报错多半是settings.py里的数据库配置和你本地实际不符检查DATABASES配置项。5.3 本地运行与功能验收启动服务python manage.py runserver 0.0.0.0:8000浏览器访问http://127.0.0.1:8000按这几个业务流验收用户注册并登录进入食堂主页看到菜品列表。将菜品加入购物车提交订单此时订单状态为待支付。在后台管理中将该订单标记为已支付模拟支付流程。用户端刷新订单详情确认状态变为已支付或制作中。如果页面样式全无检查settings.py中DEBUGTrue时 Django 是否能自动提供给静态文件以及STATIC_URL是否以/static/结尾。5.4 部署到服务器Gunicorn Nginx 的最小方案毕业设计演示时把项目部署到云服务器能加不少印象分。用宝塔面板可以降低操作门槛但内核步骤是一样的。首先用 Gunicorn 启动 Djangogunicorn canteen.wsgi:application -w 2 -b 0.0.0.0:8000canteen.wsgi.application是项目生成的 WSGI 入口-w 2表示两个 worker 进程-b绑定监听地址。然后配置 Nginx 反向代理把 80 端口转发到 8000并把静态文件目录指向 Django 的STATIC_ROOTlocation /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.pyDEBUG False ALLOWED_HOSTS [你的域名, 服务器IP]DEBUGFalse后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/, datajson.dumps({ order_id: 1, amount: 25.00, sign: fake }), content_typeapplication/json) print(resp.status_code, resp.content)把order_id换成真实订单 ID观察响应。如果能返回{code: 0}并修改订单状态说明回调接口无误。这个技巧在答辩现场演示时非常有用只需要提前准备一段模拟数据不用依赖外部网络。之后你再把签名校验逻辑补全就能无缝切换到真实支付平台。本文还有配套的精品资源点击获取