简介:这份资源是面向计算机专业毕业生与Java Web初学者的一份完整毕业设计文档,围绕基于Spring Boot的校园食堂订餐系统展开,针对疫情下食堂拥挤、学生外卖饮食不健康、商家销售统计困难等现实问题给出可落地的技术方案。文档系统梳理了系统的总体功能设计,涵盖手机端菜品浏览、订单提交与限时修改撤销、校园一卡通在线支付以及菜品销售统计等模块,并说明了后台采用Spring Boot轻量框架与B/S架构、前台基于可跨Android、iOS、H5及小程序的uni-app开发、数据层使用MySQL存储的完整技术选型,同时记录了系统测试与调试过程。资源包共1个doc文件,约3.32MB,内容为论文正文,包含中英文摘要、关键词与系统设计论述,适合作为毕业设计选题参考、论文写作模板或项目搭建思路借鉴。目前已有9268人学习下载,对需要完成同类课题或学习Spring Boot与vue全栈开发实践的读者具有较高参考价值。
1. 从一份毕业设计文档说起:这套校园食堂订餐系统到底能跑出什么
去年帮一个学弟看毕设,他丢过来一份.doc,标题是《基于 Spring Boot 的校园食堂订餐系统的设计与实现》。我本来以为又是那种“增删改查套壳”的模板货,结果翻到第 4 章详细设计,发现里面把订单状态机、一卡通支付回调、菜品销量统计这几块讲得还挺细,数据库表也画了 E-R 图。这份文档本身不是可运行代码,而是一份完整的系统设计与实现说明——它把需求分析、总体设计、数据库设计、模块实现、测试用例全串了一遍,适合两类人:一是要照着它把系统真正写出来的学生,二是想拿它当骨架做二次开发、换成自己业务场景的开发者。它解决的核心问题很具体:食堂高峰期排队拥挤、外卖不健康、商家看不到销量数据。技术栈是 Spring Boot + uni-app + MySQL,B/S 架构,后台管理加手机端订餐两条线。下面我就按“这系统长什么样 → 怎么把它跑起来 → 怎么改”的顺序,把这份文档拆开讲。
2. 系统架构与模块划分:三个角色、两条主线怎么落地
2.1 角色与权限模型:食堂管理员、商家、学生各管什么
这份设计里角色划分得很清楚,不是那种“一个 admin 走天下”的糊弄做法。食堂管理员管餐厅(也就是商家)的增删查,商家管自己的菜品、订单、统计,学生和教职工管下单和查自己的订单。三者的权限边界在数据库层面就分开了,常见做法是用户表里放一个role字段,登录后由后端返回对应菜单,前端 uni-app 根据角色渲染不同 tab。
我一般会把权限做成“角色 + 资源”的二维表,而不是在代码里写一堆if (role == 'admin')。文档里虽然没贴完整的权限表结构,但从“系统对食堂管理员、商家、消费人群访问数据库设定不同权限”这句能看出设计意图是对的。落地时建议加一张sys_role和sys_role_menu,后面加角色不用改代码。
-- 角色表:区分食堂管理员、商家、学生/教职工 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(32) NOT NULL UNIQUE COMMENT 'ADMIN/MERCHANT/STUDENT', role_name VARCHAR(64) NOT NULL ); -- 用户表挂角色,一个用户一个角色够用,别过度设计 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL COMMENT 'BCrypt 加密存储', role_id BIGINT NOT NULL, card_no VARCHAR(32) COMMENT '一卡通卡号,学生角色才有', status TINYINT DEFAULT 1 COMMENT '1正常 0禁用' );上面这段的关键点是card_no单独挂在用户表上,因为一卡通支付要拿卡号去调校园支付网关。password用 BCrypt 而不是 MD5,这是血泪经验——毕设里用 MD5 的,答辩时被问“盐在哪”基本答不上来。status字段是给管理员封禁账号留的口子,别等到上线才发现没地方禁用用户。
2.2 功能模块拆解:餐厅、菜品、订单、统计四块怎么串
文档第 2 章把功能需求列了七项,归拢下来其实是四个核心模块加两个辅助模块。餐厅管理是管理员维护商家信息;菜品管理是商家维护菜品(品种、数量、单价、原材料、供应量);订单管理是商家查订单、改状态、导出;菜品统计是商家看销量、导报表。辅助的是“我的订单”和用户订餐流程。
这四块的数据流是串起来的:商家先建菜品 → 学生浏览菜品加购物车 → 提交订单生成订单主表和明细表 → 支付回调改订单状态 → 统计模块按订单明细聚合销量。所以数据库设计上,订单明细表必须冗余菜品名称和单价,不能只存菜品 ID。为什么?因为菜品价格会变,统计历史销量时如果 join 菜品表拿现价,算出来的营业额是错的。这是很多毕设翻车的地方。
-- 订单主表 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务单号,防重复提交', user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已接单 3已完成 4已取消', pay_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单明细:冗余菜名和单价,统计才准 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, dish_name VARCHAR(128) NOT NULL COMMENT '下单时快照', unit_price DECIMAL(10,2) NOT NULL COMMENT '下单时快照', quantity INT NOT NULL );order_no用“日期 + 商家 ID + 随机数”生成,别用自增 ID 直接当单号,否则用户能猜出一天卖了多少单。status用数字枚举而不是字符串,查询快、占空间小,前端做状态映射就行。明细表冗余dish_name和unit_price是这份设计里最值得抄的一个点,统计模块直接SUM(unit_price * quantity)就能出营业额,不用回查菜品表。
2.3 B/S 架构下的前后端分工:Spring Boot 管什么,uni-app 管什么
文档明确写了后台用 Spring Boot、前台用 uni-app 跨 Android/iOS/H5/小程序。这个组合在毕设里算务实的选择:后端一套 REST 接口,前端一套代码多端发布,不用为安卓和 iOS 各写一遍。分工上,Spring Boot 负责鉴权、业务逻辑、支付回调、统计聚合;uni-app 负责页面渲染、购物车本地缓存、调起支付。
接口风格建议统一成/api/{模块}/{动作},返回体固定成{ code, msg, data }。文档第 4 章提到“返回信息格式”作为公共模块类,说明作者也是这么想的。下面是一个统一返回体的写法,配合全局异常处理,前端只认code == 200就行。
// 统一返回体,前端只判断 code public class R<T> { private int code; private String msg; private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static <T> R<T> fail(String msg) { R<T> r = new R<>(); r.code = 500; r.msg = msg; return r; } }code用 200/500 这种 HTTP 风格的值,前端拦截器里统一处理:200 取data,500 弹msg,401 跳登录。别每个接口返回不同结构,否则 uni-app 那边要写一堆判断,维护起来是灾难。data用泛型是为了分页和列表都能复用同一个包装类,文档里“分页显示”和“返回信息格式”两个公共类其实就是干这个的。
3. 把项目跑起来:环境、建表、启动、联调四步走
3.1 环境准备:JDK、MySQL、Node 版本怎么选
文档给的软件环境是 Windows 10 + Tomcat 7 + MySQL 8.0,开发工具 IntelliJ IDEA 2019.3.1 和 WebStorm 2020.1。这里要提醒一句:Spring Boot 内置了 Tomcat,不需要单独装 Tomcat 7,文档里写 Tomcat 7 大概率是沿用老模板。实际跑起来你只需要 JDK 8 或 11、MySQL 8.0、Node 14 以上(uni-app 编译需要)。
| 组件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | Spring Boot 2.x 都兼容,别上 17 除非确认依赖 |
| MySQL | 8.0 | 文档指定,注意时区参数 |
| Maven | 3.6+ | 拉依赖用 |
| Node | 14/16 | uni-app 对高版本 Node 偶有兼容问题 |
| HBuilderX | 最新 | 跑 uni-app 最省事 |
MySQL 8.0 连接串一定要带serverTimezone=Asia/Shanghai,不然create_time会差 8 小时,订单时间全乱。这是新手最容易踩的坑,后面避坑章节还会细说。
3.2 数据库初始化:建库、导表、灌测试数据
文档第 3 章有 E-R 图和数据表创建,但没给完整 SQL 脚本。按它的表结构,你需要建库后依次建sys_user、sys_role、restaurant、dish、orders、order_item六张核心表。建库语句注意字符集用utf8mb4,不然菜品名里的生僻字或 emoji 会存成问号。
CREATE DATABASE canteen DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE canteen; -- 餐厅表:管理员维护 CREATE TABLE restaurant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, location VARCHAR(128) COMMENT '食堂楼层位置', status TINYINT DEFAULT 1 ); -- 菜品表:商家维护,supply 是供应量 CREATE TABLE dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT, restaurant_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, material VARCHAR(255) COMMENT '原材料', supply INT DEFAULT 0 COMMENT '当日供应量', status TINYINT DEFAULT 1 COMMENT '1上架 0下架' );灌测试数据时至少造一个管理员、两个商家、三个学生,每个商家挂五道菜。别用真实姓名和学号,用test01这种。supply字段是文档里“供应量”的落地,下单时要扣减,扣到 0 就自动下架,这个逻辑放在 Service 层用乐观锁或UPDATE ... WHERE supply >= ?保证不超卖。
3.3 后端启动:application.yml 配置与常见启动失败
Spring Boot 的配置文件里,数据源、端口、支付回调地址三块必须改。端口默认 8080,如果本机被占就改 8081。支付回调地址在本地联调时填内网穿透的地址,没有的话先注释掉支付相关 Bean,别让启动卡在支付网关初始化上。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/canteen?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这行很关键,数据库的create_time能自动映射到 Java 的createTime,不然每个字段都要写@Results,烦死人。启动失败最常见的原因是 MySQL 没启动、密码错、或者driver-class-name用了旧的com.mysql.jdbc.Driver(MySQL 8 要用cj那个)。看控制台第一行报错就能定位,别急着翻代码。
3.4 前端联调:uni-app 改 baseUrl、跑 H5 还是小程序
uni-app 项目拉下来后,先找common/request.js或类似文件,把baseUrl改成http://localhost:8080。跑 H5 最省事,HBuilderX 里点“运行到浏览器”就行;跑微信小程序需要在小程序开发者工具里配好 AppID,并且后端要允许跨域(H5 才需要跨域,小程序不需要)。
// uni-app 请求封装,统一带 token const BASE_URL = 'http://localhost:8080'; export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'token': uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { uni.reLaunch({ url: '/pages/login/login' }); } else { uni.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail: reject }); }); }这段封装把 token 注入、错误提示、401 跳登录全包了,页面里只管调request({ url: '/api/dish/list' })。uni.getStorageSync('token')在登录成功后写入,退出时清掉。H5 联调如果报跨域,后端加一个CorsConfig允许localhost就行,别在前端瞎折腾代理。
4. 核心模块代码拆解:订单状态机、支付回调、销量统计
4.1 订单模块:提交、改单、撤单的时间窗口怎么控
文档说“一定时间范围修改和撤销订单”,这个“一定时间”落地时就是一个截止时间字段。常见做法是下单时记录create_time,改单/撤单时判断now - create_time < 15 分钟且状态还是“待支付”或“已支付未接单”。超过窗口就只能联系商家,系统不给改。
public R<String> cancelOrder(Long orderId, Long userId) { Orders order = orderMapper.selectById(orderId); if (order == null || !order.getUserId().equals(userId)) { return R.fail("订单不存在"); } // 只有待支付/已支付未接单可撤 if (order.getStatus() != 0 && order.getStatus() != 1) { return R.fail("当前状态不可撤销"); } // 15 分钟窗口 long minutes = Duration.between(order.getCreateTime(), LocalDateTime.now()).toMinutes(); if (minutes > 15) { return R.fail("已超过可撤销时间"); } order.setStatus(4); orderMapper.updateById(order); // 回滚库存 dishService.restoreStock(orderId); return R.ok("已撤销"); }status判断放在时间判断前面,先挡掉已完成/已取消的订单,再算时间,逻辑更清晰。restoreStock要把明细里的数量加回dish.supply,不然撤单后库存对不上。这里有个细节:撤单和支付回调可能并发,建议给订单行加version字段做乐观锁,或者用UPDATE orders SET status=4 WHERE id=? AND status IN (0,1)这种条件更新,避免重复撤单。
4.2 一卡通支付:回调验签与幂等处理
线上支付这块文档只说了“具有线上学生一卡通支付功能”,没给实现。按校园场景的常见做法,是调学校一卡通中心的支付接口,拿到支付链接或二维码,用户支付后一卡通中心回调你的notify接口。回调里必须做两件事:验签和幂等。
@PostMapping("/pay/notify") public String payNotify(@RequestBody Map<String, String> params) { // 1. 验签,防止伪造回调 if (!payService.verifySign(params)) { return "fail"; } String orderNo = params.get("orderNo"); // 2. 幂等:已处理过的直接返回 success Orders order = orderMapper.selectByOrderNo(orderNo); if (order.getStatus() == 1) { return "success"; } // 3. 条件更新,防并发重复处理 int rows = orderMapper.updateStatusByNo(orderNo, 1); if (rows == 0) { return "success"; } // 4. 扣库存、记流水 dishService.deductStock(orderNo); return "success"; }验签用一卡通中心给的密钥做 HMAC,别省这一步,否则别人构造一个 POST 就能把订单改成已支付。幂等靠“状态判断 + 条件更新”双保险,updateStatusByNo的 SQL 是UPDATE orders SET status=1 WHERE order_no=? AND status=0,返回 0 行说明已经被处理过,直接返回 success 让回调方停止重试。扣库存放在状态更新之后,保证只有真正流转到已支付的订单才扣。
4.3 销量统计:按菜品聚合与导出报表
统计模块是商家最关心的,文档说“查询菜品统计列表信息和导出”。核心 SQL 就是按dish_id聚合订单明细,过滤掉已取消的订单,按销量倒序。
SELECT oi.dish_id, oi.dish_name, SUM(oi.quantity) AS total_qty, SUM(oi.unit_price * oi.quantity) AS total_amount FROM order_item oi JOIN orders o ON oi.order_id = o.id WHERE o.merchant_id = #{merchantId} AND o.status IN (1, 2, 3) AND o.create_time BETWEEN #{start} AND #{end} GROUP BY oi.dish_id, oi.dish_name ORDER BY total_qty DESC;status IN (1,2,3)排除了待支付和已取消,只统计真实成交。BETWEEN的时间范围由前端传,默认查当天。导出用 EasyExcel 或 POI 把结果写成 xlsx,字段就是菜名、销量、金额三列,别搞太花哨。这个查询在订单量大了以后会慢,给orders(merchant_id, create_time)加联合索引就能扛住校园量级。
5. 避坑与排查:这几个坑我替你先踩了
5.1 时间差 8 小时:MySQL 时区没配对
现象是订单列表里create_time显示的时间比实际晚 8 小时,统计按天查的时候数据对不上。原因是 MySQL 8.0 默认时区是 UTC,JDBC 连接没指定时区。解决办法是在连接串加serverTimezone=Asia/Shanghai,同时确认 MySQL 服务器time_zone是+08:00。改完重启后端,历史数据如果已经错了,得手动UPDATE修正,没有后悔药。
5.2 订单重复提交:用户手抖点两下生成两单
现象是同一个用户短时间内出现两条一模一样的订单。原因是前端按钮没防抖,后端也没做幂等。解决分两层:前端提交后立即禁用按钮;后端用order_no唯一索引兜底,生成规则里带用户 ID 和时间戳,重复插入直接抛唯一键冲突,捕获后返回“请勿重复提交”。别只靠前端,接口被直接调的时候前端防抖没用。
5.3 库存超卖:并发下单把供应量扣成负数
现象是dish.supply出现负数,或者两个用户都下单成功但实际只剩一份。原因是“先查库存再扣减”不是原子操作。解决办法是用条件更新:UPDATE dish SET supply = supply - ? WHERE id = ? AND supply >= ?,返回影响行数为 0 就说明库存不足,回滚订单。这是最轻量的方案,不用引入分布式锁,校园量级完全够用。
5.4 跨域报错:H5 调后端接口被浏览器拦
现象是 H5 页面请求后端报Access-Control-Allow-Origin相关错误,小程序却正常。原因是浏览器同源策略,小程序不受限。解决办法是后端加全局 CORS 配置,允许前端开发地址,并且allowCredentials和allowedOrigins不能同时用*。上线后把允许的域名收窄,别一直开着*。
5.5 支付回调收不到:本地没有公网地址
现象是本地联调时支付完成后订单状态一直不变。原因是一卡通中心的回调打不到localhost。解决办法是联调阶段先用一个“模拟回调”的测试接口手动触发,或者用内网穿透工具把本地端口映射出去(注意只用于开发环境)。上线时回调地址必须是公网 HTTPS,并且要在支付平台后台配好,配错了回调永远进不来。
6. 二次开发怎么改:从毕设骨架到能用的订餐系统
这份文档的骨架是够用的,但真要拿去给一个小食堂用,有几处必须改。第一处是鉴权,毕设里常见的是登录后返回一个简单 token 存 Redis,但没做过期续期。我一般会换成 JWT + 刷新 token,access token 两小时过期,refresh token 七天,前端拦截器里自动续,用户无感知。第二处是菜品图片,文档里菜品只有文字信息,实际运营没图没人点,加一个image_url字段接对象存储就行,别把图片存数据库。
第三处是统计维度,现在只按菜品聚合,商家其实还想看“哪个时间段销量高”,好在orders.create_time已经有了,加一个按小时分组的查询就能出热力图数据。第四处是订单状态机,文档里状态是 0 到 4,但没定义流转规则,二次开发时建议画一张状态迁移表,明确“谁能把哪个状态改成哪个状态”,比如只有商家能把 1 改成 2,只有系统能把 0 改成 1,学生只能把 0/1 改成 4。这张表贴在 Service 层做校验,比散落在各个方法里靠谱。
验证改造是否成功,我习惯用三个用例走一遍:并发下单同一道只剩一份的菜,只能成功一单;支付回调重复发三次,订单只流转一次;撤单后库存回到原值。这三个过了,核心链路基本就稳了。从那以后我每次接这类订餐项目,都强制先把状态迁移表和幂等方案写出来再动手写代码,不然改到后面全是补丁。希望帮到你。
本文还有配套的精品资源,点击获取