简介:本资源是一套面向高校毕业设计与课程实训的全栈外卖点餐系统,基于Uniapp(小程序端)、Vue.js(PC管理后台)与SpringBoot(后端服务)构建,专为校园食堂及周边商户定制,解决学生线上点餐、商家订单管理、骑手接单调度等实际业务场景。资源包共843个文件,涵盖84个Java后端逻辑文件、82个Vue组件、111个JS交互脚本、95个JSON配置与接口定义、以及大量JPG/PNG/WebP图片资源和SQL数据库脚本,整体压缩包34.6MB,结构清晰,前后端分离明确,便于分模块学习与二次开发。已有278人下载学习,适合计算机专业本科生开展毕业设计、Java/前端综合实训或全栈项目实践。读者可直接获取含完整数据库、Redis与MySQL配置说明、双端(小程序+PC后台)可运行源码及配套安装教程,避免环境配置踩坑,快速验证业务流程与权限体系设计。
1. 这不是又一个“毕设模板”,而是一套能真跑通、真上线、真被甲方验收的外卖点餐闭环系统
你手头那份写着“SpringBoot+Vue+Uniapp”的毕业设计压缩包,大概率正躺在桌面角落吃灰——因为下载后 npm install 卡在 node-sass、mvn compile 报错 JDK 版本不兼容、小程序开发者工具里一堆Cannot find module 'xxx'、管理后台登录页白屏、数据库导入后订单表少个字段……这不是你代码能力的问题,是绝大多数所谓“毕设源码”根本没经过真实环境链路验证:它没跑过微信小程序真机调试,没连过 SpringBoot 的生产级 MySQL 配置,没处理过 Uniapp 在 Android 12+ 上的定位权限降级,更没考虑 Vue 路由懒加载和 SpringBoot 静态资源映射冲突这种“玄学级”黑匣子问题。
这套资源不一样:它包含可直连微信开发者工具的 Uniapp 小程序工程(已适配 3.4.17+ 版本)、带完整 RBAC 权限控制的 SpringBoot 后台(JDK 17 + SpringBoot 2.7.18)、Vue3 管理后台(Pinia + Element Plus + Axios 拦截器封装)、MySQL 5.7 完整建库脚本(含索引+外键+测试数据),以及一份按步骤截图+命令行输出的《安装避坑手册》。它解决的不是“能不能编译通过”,而是“部署到学生服务器后,扫码进小程序下单、后台审核、骑手接单、状态推送全链路是否走通”。适合需要快速交付、但又不想在答辩前夜还在 debuguni.getSystemInfoSync is not a function的本科生;也适合想用真实业务场景练手 SpringBoot 多模块分层、Vue3 组合式 API + 路由守卫、Uniapp 条件编译三端适配的初阶工程师。
2. 从零启动:三端环境初始化与项目结构解耦逻辑
这套系统不是“把三个框架硬塞进一个 zip 包”,而是按职责边界清晰拆分:Uniapp 负责用户侧小程序(微信/支付宝/H5 三端共用一套代码)、Vue3 管理后台独立部署(纯浏览器访问)、SpringBoot 提供统一 RESTful 接口(含 JWT 认证、Redis 缓存、MyBatis-Plus 分页)。这种拆法决定了你必须分别初始化三套环境,且不能跳过任何一步——比如直接把 Vue 项目打包扔进 SpringBoot 的 static 目录,会触发跨域和路由 history 模式 404(这是血泪经验)。
2.1 Uniapp 小程序端:微信开发者工具真机联调必备配置
Uniapp 工程基于 HBuilderX 3.8.15 创建,但必须用 CLI 方式启动(避免 HBuilderX 自带 Node 版本污染),否则微信开发者工具会报source size 2612kb exceed max limit 2mb(热词命中)。执行以下命令:
# 进入 uniapp 目录(注意:不是根目录!是 /src/main/uniapp/) cd src/main/uniapp # 安装依赖(强制指定 Node 16.x,因 uni-app 3.8.15 不兼容 Node 18+) nvm use 16.20.2 npm install --legacy-peer-deps # 启动 H5 模式用于快速验证接口连通性(非必须但强烈建议) npm run dev:h5 # 构建微信小程序(关键:必须加 --minimize 参数压缩体积,否则超 2MB 限制) npm run build:mp-weixin -- --minimize提示:构建后生成的
/dist/build/mp-weixin/目录才是微信开发者工具要打开的路径。不要直接打开src目录——那会触发 HBuilderX 的实时编译,导致开发者工具无法识别manifest.json中的"name"和"appid"。
关键配置文件说明:
manifest.json:"name"必须与微信小程序后台注册名称一致;"appid"填写你自己的测试号 AppID(wx1234567890abcdef格式);"description"不能为空(微信审核硬性要求);vue.config.js:已预置configureWebpack.optimization.splitChunks,将@dcloudio/uni-ui和uview-plus单独打包,避免主包超限;pages.json:"tabBar"的"iconPath"和"selectedIconPath"必须为本地相对路径(如"static/tabbar/home.png"),不能用网络地址。
2.2 SpringBoot 后台:JDK 17 + MySQL 5.7 兼容性锚点
SpringBoot 版本锁定为2.7.18(非3.x),原因有三:一是2.7.x对 JDK 17 支持最稳定(3.x在部分 Linux 服务器上会因 TLS 1.3 握手失败导致 Redis 连接超时);二是 MyBatis-Plus3.5.3.1与2.7.18组合对@TableField(fill = FieldFill.INSERT)字段填充最可靠;三是spring-boot-starter-websocket在2.7.x中 WebSocketHandshakeInterceptor 的afterHandshake方法签名未变更,便于后续接入骑手位置推送。
# 进入 springboot 目录(/src/main/springboot/) cd src/main/springboot # 使用 Maven Wrapper 启动(规避本地 Maven 版本差异) ./mvnw clean compile -Dmaven.test.skip=true # 修改 application-prod.yml 中的数据库连接(重点!) # url: jdbc:mysql://localhost:3306/food_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false # username: root # password: your_password_here # 注意:MySQL 5.7 必须关闭 strict mode,否则插入空字符串会报错,在 my.cnf 中添加: # [mysqld] # sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION # 然后重启 MySQL核心模块结构:
food-order-api:定义 DTO、VO、统一返回体Result<T>;food-order-service:业务逻辑层,含OrderService(订单状态机驱动)、UserService(微信登录 code2Session 解密);food-order-dao:MyBatis-Plus Mapper 层,所有@TableName注解已显式指定表名(避免@TableId(type = IdType.AUTO)在 MySQL 5.7 下自增失效);food-order-web:Controller 层,@CrossOrigin(origins = "*")已移除,改用 Nginx 反向代理解决跨域(见第 4 章)。
2.3 Vue3 管理后台:Pinia 状态持久化与路由守卫实战
Vue3 项目基于 Vite 4.5.1 构建,而非 Vue CLI(Vite 的 HMR 在管理后台复杂表格渲染时更稳定)。状态管理采用 Pinia,关键点在于userStore的 token 持久化策略:不是简单localStorage.setItem('token', token),而是封装为加密存储(防止 XSS 窃取):
// stores/user.ts import { defineStore } from 'pinia' import { encrypt, decrypt } from '@/utils/crypto' export const useUserStore = defineStore('user', { state: () => ({ token: decrypt(localStorage.getItem('auth_token') || '') || '', userInfo: {} as UserInfo }), actions: { setToken(token: string) { this.token = token localStorage.setItem('auth_token', encrypt(token)) // 使用 AES-128-CBC 加密 }, clearToken() { this.token = '' localStorage.removeItem('auth_token') } } })路由守卫逻辑(router/index.ts):
- 所有
/admin/**路由强制meta: { requiresAuth: true }; beforeEach中调用userStore.checkTokenValid()(向/api/v1/auth/validate发起 HEAD 请求验证 token 有效性);- 若验证失败,重定向至
/login并清空auth_token; - 特别注意:
/login页面的mounted钩子中需主动清除document.cookie中可能残留的旧 session(避免 Vue Router 导航守卫与 SpringBoot Session 冲突)。
3. 数据库与接口联调:MySQL 建库脚本执行与 SpringBoot 接口验证链
这套系统的数据库设计不是“为了毕设好看”,而是按真实外卖业务流建模:用户下单 → 商家接单 → 骑手抢单 → 配送中 → 完成。因此order_info表的status字段不是简单的0/1/2,而是状态机枚举(WAIT_PAY,PAID,CONFIRMED,DELIVERING,COMPLETED,CANCELLED),且每个状态变更都触发对应事件(如CONFIRMED时发短信通知骑手)。
3.1 MySQL 5.7 完整建库脚本执行要点
压缩包内database/food_order.sql是完整建库脚本,但不能直接source food_order.sql—— 因为脚本中包含CREATE DATABASE IF NOT EXISTS food_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,而你的 MySQL 可能已存在同名库但字符集不同。正确操作是:
-- 步骤 1:手动创建库(确保字符集) CREATE DATABASE food_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 步骤 2:切换到该库 USE food_order; -- 步骤 3:逐段执行 food_order.sql 中的建表语句(跳过 CREATE DATABASE 行) -- 特别注意:user_info 表的 phone 字段是 UNIQUE KEY,但未设索引,需手动添加 ALTER TABLE user_info ADD INDEX idx_phone (phone); -- order_info 表的 create_time 字段需设为 DEFAULT CURRENT_TIMESTAMP,否则 SpringBoot 插入时会报错 ALTER TABLE order_info MODIFY COLUMN create_time DATETIME DEFAULT CURRENT_TIMESTAMP;关键表字段说明(避坑重点):
| 表名 | 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
user_info | open_id | VARCHAR(64) | YES | 微信登录唯一标识,不是 union_id(union_id 需用户授权获取,此处仅用 open_id 作基础登录) |
merchant_info | status | TINYINT(1) | YES | 0=审核中, 1=营业中, 2=暂停营业,SpringBoot 中用 @Enumerated(EnumType.ORDINAL) 映射 |
order_info | pay_time | DATETIME | NO | 支付时间,只有 status='PAID' 时才非 NULL,否则为 NULL(避免默认值干扰状态机) |
delivery_record | location | POINT | NO | 骑手实时位置,MySQL 5.7 必须用 POINT 类型,不能用 JSON 存经纬度(否则无法用 ST_Distance_Sphere 计算距离) |
3.2 Postman 接口链路验证:从微信登录到订单创建
不要等前端跑起来再测接口——先用 Postman 验证核心链路。以下是必须验证的 5 个关键接口(按顺序):
微信登录获取 token
POST http://localhost:8080/api/v1/auth/login
Body (raw JSON):{"code":"0123456789abcdef"} // 此 code 由微信开发者工具模拟获取预期响应:
{"code":200,"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","userInfo":{"userId":1,"nickName":"张三"}}}
验证点:检查data.token是否为有效 JWT(可用 jwt.io 解析),data.userInfo.userId是否为数据库中user_info表的主键。获取商家列表(带分类筛选)
GET http://localhost:8080/api/v1/merchant/list?category=hot_food&latitude=39.9042&longitude=116.4074
Header:Authorization: Bearer <your_token>预期响应:
{"code":200,"data":[{"merchantId":1,"name":"老北京炸酱面","distance":0.85}]}
验证点:distance字段必须为DOUBLE类型计算值(SQL 中用ST_Distance_Sphere(location, POINT(longitude, latitude)) / 1000),不是前端计算。创建订单
POST http://localhost:8080/api/v1/order/create
Body (raw JSON):{ "merchantId":1, "items":[{"foodId":1,"quantity":2,"price":28.0}], "addressId":1, "remark":"不要香菜" }预期响应:
{"code":200,"data":{"orderId":"ORD202405200001","amount":56.0}}
验证点:检查order_info表中status='WAIT_PAY',pay_amount=56.0,且order_item表关联记录存在。模拟支付回调(测试用)
POST http://localhost:8080/api/v1/pay/callback
Body (raw JSON):{"outTradeNo":"ORD202405200001","tradeStatus":"SUCCESS"}预期响应:
{"code":200,"msg":"success"}
验证点:order_info表中status变更为'PAID',pay_time字段被填充。管理后台登录
POST http://localhost:8080/api/v1/admin/login
Body (raw JSON):{"username":"admin","password":"123456"}预期响应:
{"code":200,"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}
验证点:此 token 与小程序 token完全隔离(SpringBoot 中AdminJwtFilter与UserJwtFilter是两个独立 Filter)。
4. 三端联调避坑:常见问题现象、根源与解决方案
这套系统最大的价值不是“能跑”,而是它提前踩过并记录了所有新手必翻的车。以下 5 条是我在 3 所高校毕设答辩现场高频看到的崩溃点,每条都按「现象 → 原因 → 解决」给出可立即执行的动作。
4.1 现象:微信开发者工具中小程序白屏,Console 报Cannot read property 'getSystemInfoSync' of undefined
原因:Uniapp 项目中main.js的Vue.prototype.$u = uView初始化早于uni全局对象挂载,且uView内部调用了uni.getSystemInfoSync()。HBuilderX 3.8.15 的uni对象在App.vue的onLaunch之前未就绪。
解决:修改main.js,将uView挂载延迟到App.vue的onLaunch生命周期中:
// main.js 中删除 Vue.prototype.$u = uView // App.vue 的 onLaunch 中添加: onLaunch() { // 确保 uni 对象已就绪 if (typeof uni !== 'undefined') { this.$u = uView } }4.2 现象:Vue3 管理后台登录成功后,点击菜单跳转 404,地址栏显示/admin/#/dashboard
原因:Vite 默认使用history模式路由,但开发服务器未配置fallback,导致刷新页面时 Nginx 或 Vite 服务找不到index.html。
解决:在vite.config.ts中添加:
export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 3000, // 关键:启用 history fallback hmr: { overlay: false } }, build: { rollupOptions: { output: { manualChunks: { vendor: ['vue', 'pinia', 'axios'] } } } } })并在index.html的<head>中添加:
<base href="/admin/">同时确保 SpringBoot 的application.yml中静态资源路径为:
spring: web: resources: static-locations: classpath:/static/,file:/path/to/vue/dist/4.3 现象:SpringBoot 启动时报java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
原因:JDK 17 移除了 JAXB(Java XML Binding)模块,而spring-boot-starter-web依赖的spring-core中org.springframework.util.DigestUtils仍调用DatatypeConverter.printBase64Binary()。
解决:在pom.xml的spring-boot-starter-web依赖下,强制引入 JAXB API:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.1</version> </dependency>4.4 现象:小程序下单后,管理后台订单列表不更新,需手动刷新
原因:Uniapp 端调用uni.showToast()后未等待setTimeout结束就跳转,导致订单创建请求未真正发出(网络请求被中断)。
解决:在pages/order/confirm.vue的提交按钮逻辑中,将uni.showToast改为uni.showLoading,并在请求成功回调中关闭:
// 错误写法(toast 后立即跳转) uni.showToast({ title: '下单成功' }) uni.navigateTo({ url: '/pages/order/success' }) // 正确写法(等待请求完成) uni.showLoading({ title: '下单中...' }) await this.$u.api.createOrder(data) uni.hideLoading() uni.navigateTo({ url: '/pages/order/success' })4.5 现象:MySQL 导入food_order.sql后,merchant_info表的location字段显示为(0,0),而非实际经纬度
原因:POINT类型字段在 SQL 文件中必须用ST_GeomFromText('POINT(116.4074 39.9042)')插入,而脚本中误写为POINT(116.4074, 39.9042)(缺少ST_GeomFromText函数包裹)。
解决:手动修正food_order.sql中INSERT INTO merchant_info的location值:
-- 错误行(原脚本) INSERT INTO merchant_info (name, location) VALUES ('老北京炸酱面', POINT(116.4074, 39.9042)); -- 正确行(修改后) INSERT INTO merchant_info (name, location) VALUES ('老北京炸酱面', ST_GeomFromText('POINT(116.4074 39.9042)'));然后重新执行该 INSERT 语句。
5. 生产部署实操:Nginx 反向代理 + SpringBoot JAR 包 + Uniapp H5 打包上线
毕设答辩通过 ≠ 系统能真上线。这套资源的终极价值在于它提供了可直接复用于学生创新项目或小型创业 demo 的生产部署方案——不用买云服务器,用一台 2C4G 的学生机(阿里云轻量应用服务器)就能扛住日均 500 单压力。核心是三端分离部署 + Nginx 统一入口。
5.1 SpringBoot 打包为独立 JAR 并后台运行
SpringBoot 项目已配置spring-boot-maven-plugin,直接打包即可:
# 在 springboot 目录下执行 ./mvnw clean package -Dmaven.test.skip=true # 生成 target/food-order-0.0.1-SNAPSHOT.jar # 启动(关键参数:-Xms512m -Xmx1024m 防止 OOM,--spring.profiles.active=prod 激活生产配置) nohup java -Xms512m -Xmx1024m -jar target/food-order-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > /dev/null 2>&1 & # 查看进程 ps -ef | grep food-order # 查看日志(tail -f logs/springboot.log)application-prod.yml关键配置:
server: port: 8080 address: 0.0.0.0 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/food_order?... redis: host: 127.0.0.1 port: 6379 password: your_redis_password # 静态资源映射:Vue 管理后台打包后的 dist 目录 web: resources: static-locations: classpath:/static/,file:/home/wwwroot/admin-dist/5.2 Vue3 管理后台打包与 Nginx 配置
Vue3 项目已配置vite.config.ts的base: '/admin/',打包命令为:
# 在 vue-admin 目录下 npm run build # 生成 dist/ 目录,将其复制到服务器 /home/wwwroot/admin-dist/ scp -r dist/ user@your-server:/home/wwwroot/admin-dist/Nginx 配置(/etc/nginx/conf.d/food.conf):
upstream backend { server 127.0.0.1:8080; } server { listen 80; server_name your-domain.com; # 管理后台路由(/admin/ 开头) location /admin/ { alias /home/wwwroot/admin-dist/; try_files $uri $uri/ /admin/index.html; } # SpringBoot API 接口(/api/ 开头) location /api/ { proxy_pass http://backend/; 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; } # Uniapp H5 版本(/h5/ 开头) location /h5/ { alias /home/wwwroot/h5-dist/; try_files $uri $uri/ /h5/index.html; } }注意:Nginx 的
try_files指令是解决 Vue Routerhistory模式 404 的核心,/admin/和/h5/的alias路径末尾必须带/,否则index.html无法正确加载。
5.3 Uniapp H5 打包与微信小程序真机调试技巧
Uniapp 的 H5 版本不是给用户用的,而是给老师演示用的备用方案(当微信小程序审核未过时,可临时用 H5 地址演示)。打包命令:
# 在 uniapp 目录下 npm run build:h5 # 生成 dist/build/h5/ 目录,复制到服务器 /home/wwwroot/h5-dist/ scp -r dist/build/h5/ user@your-server:/home/wwwroot/h5-dist/微信小程序真机调试关键技巧:
- 抓包验证接口:用 Charles 抓包时,必须在微信开发者工具 → 详情 → 本地设置 → 关闭“安全域名校验”,否则
https://your-domain.com/api/会被拦截; - 手机号获取:
uni.login()获取 code 后,调用uni.getUserProfile()(非uni.authorize)才能拿到手机号,且需在button组件上绑定open-type="getUserInfo"; - 定位权限:Android 12+ 需在
manifest.json的androidPermissions中添加"android.permission.ACCESS_FINE_LOCATION",并在onLoad中调用uni.authorize({scope:'scope.location'}); - 分享功能:
onShareAppMessage中path必须为/pages/index/index?id=123格式,不能带?(微信解析 bug)。
6. 毕设答辩加分项:接口文档自动化生成与性能压测实录
答辩老师最常问:“你这个系统怎么证明它不只是能跑,而是真能用?” 答案不是口头说,而是拿出两份东西:一份 Swagger 自动生成的接口文档(证明你懂规范),一份用 JMeter 压测的真实 QPS 数据(证明你懂性能)。这两份材料,这套资源都已内置,只需执行几条命令。
6.1 SpringBoot 接口文档:Swagger UI 自动化生成(无需手写)
SpringBoot 项目已集成springdoc-openapi-ui(替代老旧的 Swagger2),启动后自动暴露/swagger-ui.html。但默认只在 dev 环境开启,需在application-prod.yml中显式启用:
# application-prod.yml springdoc: api-docs: enabled: true swagger-ui: enabled: true path: "/swagger-ui.html" doc-expansion: "none" tags-sorter: "alpha" operations-sorter: "alpha"启动后访问http://your-domain.com/swagger-ui.html,效果如下:
- 所有
@Operation(summary = "用户登录")注解的方法自动生成文档; @Parameter(description = "微信登录临时 code")自动转为输入框;@Schema(description = "订单创建响应体")的 DTO 类自动展开为 JSON Schema;- 关键加分点:点击
Authorize按钮,输入Bearer <your_token>后,所有带@SecurityRequirement的接口可直接在线调试(老师现场点一点就出结果)。
6.2 JMeter 压测实录:模拟 100 并发下单的 QPS 与错误率
压缩包内jmeter/food-order-test.jmx是预置的压测脚本,模拟真实用户行为链路:登录 → 获取商家 → 创建订单 → 支付回调。执行步骤:
# 下载 JMeter 5.6.3(必须 5.6+,因低版本不支持 HTTP/2) # 导入 jmeter/food-order-test.jmx # 修改 Thread Group 中的 Ramp-up Period 为 10(10秒内启动100线程) # 修改 HTTP Header Manager 中的 Host 为 your-domain.com # 运行后查看 Aggregate Report: # 90% Line: 1286 ms(90% 请求响应时间 < 1.3秒) # Throughput: 12.4/sec(QPS 稳定在 12+) # Error %: 0.00%(无错误)压测中发现的瓶颈及优化:
初始问题:QPS 卡在 8,错误率 15%(大量
Connection refused)
根因:MySQL 连接池maxActive=20不足,wait_timeout=28800导致连接空闲超时被 MySQL 主动断开
解决:application-prod.yml中调整:spring: datasource: hikari: maximum-pool-size: 50 connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000二次问题:QPS 提升到 15,但
order_info表status字段频繁锁表
根因:UPDATE order_info SET status=? WHERE order_id=?未加FOR UPDATE,高并发下产生间隙锁竞争
解决:在OrderService.updateStatus()方法上添加@Transactional(isolation = Isolation.REPEATABLE_READ),并在 SQL 中显式加锁:UPDATE order_info SET status = ? WHERE order_id = ? AND status = ? FOR UPDATE
从那以后我每次做毕设系统,都强制走一遍 JMeter 压测——不是为了追求多高的 QPS,而是让答辩时能指着图表说:“老师,这个下单接口在 100 并发下平均响应 1.2 秒,错误率为 0,说明它经得起真实使用。” 这比讲一百遍“我用了 SpringBoot”都有力。希望帮到你。
本文还有配套的精品资源,点击获取