简介:这是一套面向计算机专业本科生的毕业设计级外卖点餐系统源码,基于Python+Django后端与Vue前端实现前后端分离架构,适用于课程设计、毕设开发及Web全栈能力实训。资源共421个文件,压缩包大小25.25MB,涵盖28个Python脚本(含Django模型、视图与API逻辑)、43个Vue组件(实现用户端/商家端/后台管理模块)、49个TypeScript文件(强化类型安全与工程化开发)、166个JPEG及32个PNG图片(菜品与界面素材)、38个SVG图标(响应式UI资源)以及关键SQL数据库文件(python_food.sql)和完整README说明文档。项目采用标准分层结构:server目录承载Django后端,web目录存放Vue前端,配置文件(.gitignore、.eslintignore等)齐全,便于快速部署与二次开发。目前已有506人学习下载,可直接用于毕设答辩、代码复现、技术栈整合实践或企业原型参考。
1. 这不是又一个“Hello World”Demo:Django+Vue外卖系统源码,专为毕业设计打磨的可运行闭环
你手头这份外卖点餐系统源码,不是网上随手搜到的半成品模板,也不是只跑通登录页就戛然而止的演示项目。它是一个真实压缩过、解压即可见结构、启动后能完成「用户注册→浏览餐厅→加购下单→订单状态流转→管理员后台审核」全链路的毕业设计级工程。418个文件不是堆砌,而是按职责切分:28个Python脚本里藏着Django的Model定义、视图逻辑与API路由;43个Vue组件不是孤立.vue文件,而是从HeaderBar.vue到OrderDetail.vue逐层嵌套的业务模块;49个TypeScript文件说明它没用Vue 2的Options API凑数,而是采用Composition API +<script setup>语法糖,配合Volar插件做类型推导——这意味着你在答辩时展示代码,评委一眼就能看出你对现代前端工程的理解深度。它适合两类人:一是大三下学期刚学完Django ORM和Vue响应式原理,需要一个“不跳步、不省略、不魔改”的参考项目来串联知识;二是指导老师,能直接用这套结构讲清前后端分离中接口契约怎么定、跨域怎么配、静态资源怎么托管。别被.gitignore和一堆图片文件迷惑——它们是刻意保留的真实开发痕迹,不是冗余。
2. Django后端:从数据库建模到RESTful API的硬核落地
2.1 数据模型设计:为什么Restaurant和Dish必须用ForeignKey而非JSONField?
外卖系统的核心是“餐厅-菜品-订单”三级关系,但很多初学者会把菜品直接存成Restaurant模型里的JSON字段(如menu = models.JSONField()),看似省事,实则埋下隐患:无法按菜品价格范围筛选、不能对单个菜品做点赞统计、订单关联时无法利用数据库外键约束保证数据一致性。本项目严格采用关系型建模:
# server/app/models.py class Restaurant(models.Model): name = models.CharField(max_length=100) address = models.TextField() rating = models.DecimalField(max_digits=3, decimal_places=2, default=0.0) class Dish(models.Model): name = models.CharField(max_length=100) price = models.DecimalField(max_digits=8, decimal_places=2) restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE, related_name='dishes') image = models.ImageField(upload_to='dishes/') class Order(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) restaurant = models.ForeignKey(Restaurant, on_delete=models.SET_NULL, null=True) status = models.CharField(max_length=20, choices=[ ('pending', '待接单'), ('confirmed', '已接单'), ('delivered', '已送达') ])提示:
on_delete=models.CASCADE表示删除餐厅时自动删其所有菜品,而on_delete=models.SET_NULL用于订单关联餐厅——餐厅停业时订单仍需保留历史记录,只是餐厅字段置空。这种细粒度控制在毕业答辩中是加分项,证明你理解业务语义而非机械写ORM。
2.2 REST API实现:用Django REST Framework暴露标准接口
单纯用Django View写JSON返回太原始。本项目采用django-rest-framework(DRF)构建API,关键在于Serializer的字段控制和Viewset的权限设计:
# server/app/serializers.py class DishSerializer(serializers.ModelSerializer): class Meta: model = Dish fields = ['id', 'name', 'price', 'image', 'restaurant_id'] # 显式声明字段,避免暴露created_at等内部字段 # server/app/views.py from rest_framework import viewsets, permissions from rest_framework.decorators import action from rest_framework.response import Response class DishViewSet(viewsets.ReadOnlyModelViewSet): queryset = Dish.objects.select_related('restaurant').all() serializer_class = DishSerializer permission_classes = [permissions.IsAuthenticatedOrReadOnly] # 未登录用户可查,不可改 @action(detail=False, methods=['get']) def by_restaurant(self, request): restaurant_id = request.query_params.get('restaurant_id') if not restaurant_id: return Response({'error': 'restaurant_id required'}, status=400) dishes = self.queryset.filter(restaurant_id=restaurant_id) serializer = self.get_serializer(dishes, many=True) return Response(serializer.data)启动服务后访问http://localhost:8000/api/dishes/by_restaurant/?restaurant_id=1即可获取某餐厅全部菜品。注意select_related('restaurant')——这是N+1查询的经典优化,避免每查一个菜品都触发一次餐厅查询。毕业设计中若出现性能问题,评委常会问“你怎么解决N+1”,此处就是标准答案。
2.3 数据库迁移与初始化:python_food.sql不是摆设,而是可复现的基线
项目附带的python_food.sql文件不是备份,而是经过mysqldump导出的完整表结构+初始数据(含测试餐厅、菜品、用户)。执行步骤如下:
# 1. 创建数据库(MySQL) mysql -u root -p -e "CREATE DATABASE python_food DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 导入SQL(确保server/settings.py中DATABASES配置指向该库) mysql -u root -p python_food < python_food.sql # 3. 同步Django迁移(验证模型与SQL一致) cd server && python manage.py migrate --fake-initial注意:
--fake-initial参数仅在首次导入SQL后使用,它告诉Django“这些表已存在,跳过创建,只记录迁移已执行”。若漏掉此步,migrate会报错“table already exists”。这是毕业设计部署时最常踩的坑——学生常以为migrate万能,却忽略已有数据的场景。
3. Vue前端:TypeScript驱动的组件化架构与状态管理实战
3.1 项目结构解析:web/src/views/与web/src/components/的职责边界
前端代码位于web/目录,采用Vue 3 + TypeScript + Vite构建。关键结构如下:
web/ ├── src/ │ ├── assets/ # 静态资源(SVG、WOFF字体、JPG/PNG/JPEG图片) │ ├── components/ # 可复用原子组件(Button.vue, LoadingSpinner.vue) │ ├── views/ # 页面级组件(HomeView.vue, OrderListView.vue) │ ├── stores/ # Pinia状态管理(userStore.ts, cartStore.ts) │ └── router/ # 路由配置(index.ts,含路由守卫)区别在于:components/中的CartIcon.vue只负责渲染购物车图标和角标数字,不处理加购逻辑;而views/OrderListView.vue则调用cartStore.addDish()并监听cartStore.items变化。这种分离让代码可测试性大幅提升——你可以单独为CartIcon.vue写单元测试,无需启动整个应用。
3.2 TypeScript类型定义:从API响应到Vuex替代方案Pinia
TypeScript不是装饰,而是保障。以订单列表为例,先定义类型:
// web/src/types/order.ts export interface Dish { id: number; name: string; price: number; image: string; restaurant_id: number; } export interface OrderItem { dish: Dish; quantity: number; } export interface Order { id: number; status: 'pending' | 'confirmed' | 'delivered'; items: OrderItem[]; total_price: number; created_at: string; }再在Pinia store中使用:
// web/src/stores/orderStore.ts import { defineStore } from 'pinia' import { Order } from '@/types/order' export const useOrderStore = defineStore('order', { state: () => ({ orders: [] as Order[], loading: false }), actions: { async fetchOrders() { this.loading = true try { const res = await fetch('/api/orders/') // 注意:此处走Django开发服务器代理 this.orders = await res.json() as Order[] // 类型断言确保TS校验 } finally { this.loading = false } } } })提示:
as Order[]不是绕过类型检查,而是告诉TS“我确信API返回的是这个结构”。若API实际返回{data: [...]},此处就会报错,迫使你修正fetchOrders逻辑——这正是TypeScript的价值:编译期暴露问题,而非运行时报Cannot read property 'map' of undefined。
3.3 跨域调试:Vite代理配置解决开发阶段CORS痛点
前后端分离必然面对跨域。Django默认拒绝非同源请求,而Vite开发服务器(http://localhost:5173)与Django(http://localhost:8000)端口不同。解决方案不是关Django的CSRF,而是配置Vite代理:
// web/vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })这样前端代码中fetch('/api/orders/')会被Vite重写为fetch('http://localhost:8000/api/orders/'),浏览器看到的仍是同源请求。部署时则需Nginx反向代理,此配置仅用于开发——毕业设计答辩演示时,评委不会要求你现场配Nginx,但会问“开发时怎么解决跨域”,此即标准回答。
4. 全栈联调:从环境搭建到关键接口验证的完整路径
4.1 环境准备:Python 3.9+、Node.js 18+、MySQL 8.0的最小可行组合
不要试图用最新版Python 3.12或Node.js 20——本项目经测试兼容性如下:
| 组件 | 推荐版本 | 理由 |
|---|---|---|
| Python | 3.9.18 | Django 4.2 LTS官方支持的最低Python版本,避免async语法兼容问题 |
| Node.js | 18.17.0 | Vite 4.x与Vue 3.3的稳定组合,npm 9.x可正确解析package.json中exports字段 |
| MySQL | 8.0.33 | python_food.sql中使用了utf8mb4_0900_as_cs排序规则,低版本不支持 |
安装命令(Linux/macOS):
# Python虚拟环境(避免污染全局) python3.9 -m venv venv source venv/bin/activate pip install -r server/requirements.txt # 包含django==4.2.7, djangorestframework==3.14.0 # Node.js依赖 cd web && npm install注意:
requirements.txt中mysqlclient==2.2.4需提前安装MySQL开发头文件(Ubuntu:sudo apt-get install libmysqlclient-dev;macOS:brew install mysql-client),否则pip install会失败。这是学生最容易卡住的环节,务必提前验证。
4.2 启动双服务:Django后端与Vue前端的协同验证
启动顺序决定成败:
# 终端1:启动Django(保持运行) cd server && python manage.py runserver 8000 # 终端2:启动Vue(自动打开http://localhost:5173) cd web && npm run dev验证是否成功:
- 访问
http://localhost:8000/admin/,用python manage.py createsuperuser创建的账号登录,确认管理员后台可操作餐厅、菜品数据; - 访问
http://localhost:5173/,观察Network面板,应看到/api/restaurants/返回200且响应体为JSON数组; - 在Vue页面点击“加入购物车”,打开浏览器Console,应看到
cartStore.addItem()被调用且cartStore.items.length递增。
若Django返回403 Forbidden,检查settings.py中ALLOWED_HOSTS = ['localhost', '127.0.0.1']是否包含前端域名;若Vue页面空白,检查Vite控制台是否有Failed to resolve component错误——大概率是components/中某个.vue文件名大小写不匹配(如HeaderBar.vue被误写为headerbar.vue)。
4.3 关键接口压力测试:用curl验证订单创建的原子性
毕业设计常被质疑“高并发下单会不会超卖”。本项目虽未实现Redis库存扣减,但Django ORM的select_for_update()已预留扩展点。先用curl模拟真实下单:
# 模拟用户1下单(需先登录获取sessionid) curl -X POST http://localhost:8000/api/orders/ \ -H "Cookie: sessionid=abc123..." \ -H "Content-Type: application/json" \ -d '{ "restaurant_id": 1, "items": [ {"dish_id": 5, "quantity": 2}, {"dish_id": 7, "quantity": 1} ] }'后端视图中应有类似逻辑:
# server/app/views.py def create_order(request): with transaction.atomic(): # 开启事务 order = Order.objects.create(...) for item in request.data['items']: # 此处可加 select_for_update() 锁定菜品库存 dish = Dish.objects.select_for_update().get(id=item['dish_id']) if dish.stock < item['quantity']: raise ValidationError('库存不足') # 扣减库存...提示:
transaction.atomic()确保订单创建与库存扣减要么全成功,要么全回滚。答辩时若被问“如何防止超卖”,此段代码+事务注释就是核心答案,不必强行上Redis。
5. 毕业设计交付技巧:从代码注释到答辩PPT的实战细节
5.1 代码注释规范:用Google Style Docstring让评委快速定位价值点
Django模型和Vue组件的注释不是装饰,而是答辩时的提词器。例如Dish模型:
class Dish(models.Model): """ 菜品模型 核心字段: - name: 菜品名称(最大100字符,前端展示用) - price: 价格(精确到分,decimal类型避免浮点误差) - restaurant: 外键关联餐厅,on_delete=CASCADE确保餐厅删除时菜品同步清理 业务约束: - 单个菜品价格不得低于0.01元(通过Model.clean()校验) - 图片上传路径为'dishes/',由Django自动处理存储 关联查询优化: - 使用select_related('restaurant')预加载餐厅信息,减少SQL查询次数 """ name = models.CharField(max_length=100) # ...其余字段Vue组件同理,在OrderListView.vue顶部添加:
<!-- 页面功能:展示当前用户全部订单,支持按状态筛选 技术亮点: - 使用Pinia持久化订单状态,页面刷新不丢失 - 调用Django REST API /api/orders/?status=pending 获取待处理订单 - 响应式设计适配移动端,CSS使用Tailwind实用类 -->评委翻看代码时,3秒内就能抓住你的技术决策点,而非在无注释的代码海中迷失。
5.2 答辩PPT结构:用“问题-方案-证据”三段式替代功能罗列
避免制作“系统包含登录、注册、点餐、支付”这类流水账PPT。改为:
| 章节 | 内容要点 | 证据支撑 |
|---|---|---|
| 问题 | 传统单体架构外卖系统难以维护,前后端耦合导致迭代慢 | 展示旧项目截图:PHP混写HTML+SQL,修改一个按钮需重启整个服务 |
| 方案 | 采用Django+Vue分离架构,API契约先行 | 展示api_spec.yaml(OpenAPI 3.0格式),标注/api/dishes/接口的request/response schema |
| 证据 | 全链路可验证:从数据库SQL到Vue组件props传递 | 动态演示:修改Django模型字段→生成新migration→前端自动适配新字段(TypeScript报错提示) |
最后一张PPT放git log --oneline -10截图,显示最近10次提交,证明你持续迭代而非最后三天突击——这是导师最看重的过程证据。
5.3 部署备选方案:宝塔面板一键部署的实操参数表
若答辩要求演示线上环境,宝塔面板是最稳妥选择。关键配置参数如下:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Python项目根目录 | /www/wwwroot/food-system/server | 必须指向Django的manage.py所在目录 |
| Python版本 | 3.9 | 宝塔内置,无需额外编译 |
| 启动命令 | gunicorn server.wsgi:application -b 127.0.0.1:8000 --daemon | 使用gunicorn替代runserver,--daemon后台运行 |
| Nginx反向代理 | location /api/ { proxy_pass http://127.0.0.1:8000/; } | 将/api路径全部转发给Django |
| Vue静态文件 | /www/wwwroot/food-system/web/dist | 构建后的dist目录,Nginx直接serve |
执行npm run build生成dist后,将web/dist上传至宝塔指定目录,再重启Nginx即可。整个过程无需接触Linux命令行,符合毕业设计“可落地”要求。
本文还有配套的精品资源,点击获取