简介:这份文档面向计算机专业毕业设计学生与Java Web初学者,围绕校园食堂订餐系统的设计与实现展开,针对疫情下食堂拥挤、学生外卖饮食不健康、商家销售统计困难等现实问题,给出了一套可参考的完整课题方案。资源包共1个doc文件,约3.32MB,内容涵盖系统总体功能设计、Spring Boot后台与uni-app前台的架构选型、MySQL数据存储方案,以及手机端点餐、订单提交与限时修改撤销、一卡通在线支付、菜品销售统计等核心模块的实现思路,并附系统测试与调试过程记录。目前已有9254人学习下载,说明该选题在毕业设计场景中具有较高的参考价值。读者可从中获取需求分析、技术选型、功能模块划分与论文写作结构的完整范本,适合需要快速搭建订餐类项目或撰写同类毕设论文的同学对照借鉴。
1. 校园食堂订餐系统:从排队拥挤到线上预订,这套 Spring Boot 方案要解决什么
每到中午十一点半,食堂窗口前的队伍能拐两个弯,学生排二十分钟只为打一份饭,商家忙到串单漏单,管理员手里还攥着一沓手写统计表——这是很多高校食堂的真实写照。校园食堂订餐系统要做的,不是把菜单搬到网页上那么简单,而是把「选菜、下单、支付、出餐、统计」这条链路整体搬到线上,让学生错峰取餐、商家按单备料、管理员有数据可看。这套基于 Spring Boot 与 uni-app 的方案,适合正在做毕业设计或课程设计的同学直接参考结构,也适合需要交付一套可运行订餐系统的开发者按模块落地。下面从需求、架构、数据、实现到验收,一层层拆开讲。
2. 需求分析与技术选型:三个角色、六类功能怎么定下来
2.1 角色划分与功能边界
这套系统的角色只有三个:学生/教职工(下称用户)、商家、食堂管理员。角色少不代表功能简单,关键在于把每个角色的动作边界划清楚,否则后期权限判断会写成一团乱麻。
用户侧的核心动作是「看菜—下单—支付—查单」。看菜要能按食堂、按窗口、按菜品分类筛选;下单要能选数量、填备注、选取餐时间段;支付走校园一卡通;查单要能看到订单状态流转,并且在规定时间窗口内还能改单或撤单。商家侧的核心动作是「维护菜品—接单—改状态—看统计」。菜品维护包括上架下架、库存数量、单价、原材料说明;接单后要把订单从「待接单」推到「制作中」「待取餐」「已完成」;统计要能按菜品、按时间段导出销售数据。管理员侧则是「管商家—管食堂—看全局」,新增或停用商家账号、维护食堂与窗口信息、处理异常订单。
把这三条线画出来之后,功能清单自然就出来了:餐厅管理、菜品管理、订单管理、菜品统计、我的订单、用户订餐,正好对应需求分析里的六个模块。这里有个血泪经验——功能边界一定要在需求阶段写死,我见过太多项目做到一半发现「商家要不要能改价」「用户撤单后库存怎么回滚」这类问题没定义,返工成本极高。
2.2 技术选型:为什么是 Spring Boot + uni-app + MySQL
后台选 Spring Boot,理由很实在:毕设和课设的时间窗口通常只有两三个月,Spring Boot 的自动配置和内嵌 Tomcat 能把环境搭建时间压到最低,一个main方法就能跑起来,不用在 XML 配置上耗掉一周。配合 MyBatis 或 MyBatis-Plus 做数据访问,分页、条件查询这些高频操作都有现成封装。
前端选 uni-app 而不是纯 Vue 页面,是因为需求里明确要求「手机端随时随地选菜」。uni-app 一套代码能编译到 H5、小程序和 App,答辩时用手机扫码演示比开电脑浏览器有说服力得多。后台管理页面则用 Vue + Element UI 那套常规组合,表格、表单、弹窗都是现成组件。
数据库用 MySQL 8.0,订单、菜品、用户这些结构化数据用关系型库最稳妥,事务支持也成熟。开发工具按原文用 IntelliJ IDEA 和 WebStorm,硬件 4 核 1.6G 主频、4G 内存起步就够跑开发和本地测试。
| 层次 | 技术选型 | 选它的理由 |
|---|---|---|
| 后端框架 | Spring Boot | 自动配置、内嵌容器、上手快 |
| 数据访问 | MyBatis / MyBatis-Plus | SQL 可控,分页封装成熟 |
| 前端(用户端) | uni-app | 一套代码多端编译,手机演示方便 |
| 前端(管理端) | Vue + Element UI | 表格表单组件齐全,开发效率高 |
| 数据库 | MySQL 8.0 | 事务可靠,结构化数据适配好 |
| 架构 | B/S | 免安装,浏览器/手机直接访问 |
提示:技术选型不要贪多。毕设阶段把 Spring Boot、MyBatis、MySQL 这三样吃透,比堆一堆中间件更能保证系统跑通、答辩讲清。
2.3 性能与非功能需求
需求分析里提到的及时性、准确性、灵活性、可靠性,落到实现上就是几条硬指标:订单状态变更后用户端刷新要能立刻看到,这要求接口响应控制在秒级;订单金额、库存扣减必须准确,不能出现超卖;查询要支持多条件组合,不能只给一个全量列表;不同角色访问不同数据,登录态和权限校验不能省。
这些指标不需要复杂的架构去撑,单机部署加合理的索引和事务就能满足校园场景的并发量。真正容易翻车的是「及时性」——如果订单状态靠用户手动刷新,体验会很差,通常的做法是前端在订单详情页做轮询,或者用 WebSocket 推送状态变更,毕设阶段轮询就够用,实现简单也好讲。
3. 总体架构与数据库设计:三层结构如何撑起六个模块
3.1 三层架构与模块职责划分
系统整体走经典的 B/S 三层结构:表现层(uni-app 用户端 + Vue 管理端)、业务逻辑层(Spring Boot 的 Controller-Service-Mapper)、数据层(MySQL)。表现层只负责展示和收集输入,业务规则全部压在 Service 层,数据访问收敛到 Mapper 层。这样分层的好处是,订单状态流转、库存扣减这些逻辑只写一遍,用户端和商家端调用同一个 Service 方法,不会出现两边规则不一致的情况。
六个功能模块的职责这样分:餐厅管理归管理员,负责商家账号和食堂窗口的增删查;菜品管理归商家,负责菜品 CRUD 和上下架;订单管理归商家,负责接单和状态推进;菜品统计归商家,负责销售数据聚合与导出;我的订单归用户,负责查单、撤单、删单;用户订餐是用户侧主流程,串起选菜、下单、支付。模块之间通过订单表关联,菜品表被订单明细引用,用户表被订单和权限引用。
3.2 核心数据表设计
数据库设计遵循一条原则:先画 E-R 图定实体关系,再落表。核心实体有用户、商家、食堂窗口、菜品、订单、订单明细。用户和订单是一对多,商家和菜品是一对多,订单和订单明细是一对多,菜品和订单明细通过明细表关联。下面给出几张关键表的结构,字段名和类型按实际开发习惯来。
-- 菜品表:商家维护,订单明细引用 CREATE TABLE dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT, merchant_id BIGINT NOT NULL COMMENT '所属商家', canteen_id BIGINT NOT NULL COMMENT '所属食堂窗口', name VARCHAR(64) NOT NULL COMMENT '菜品名称', category VARCHAR(32) COMMENT '分类:主食/荤菜/素菜/汤', price DECIMAL(10,2) NOT NULL COMMENT '单价', stock INT NOT NULL DEFAULT 0 COMMENT '可售库存', material VARCHAR(255) COMMENT '原材料说明', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单表:状态机是核心 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, pickup_slot VARCHAR(32) COMMENT '取餐时间段', status TINYINT DEFAULT 0 COMMENT '0待支付 1待接单 2制作中 3待取餐 4已完成 5已取消', pay_time DATETIME COMMENT '支付时间', 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(64) NOT NULL COMMENT '下单时名称快照', price DECIMAL(10,2) NOT NULL COMMENT '下单时单价快照', quantity INT NOT NULL );订单明细里存dish_name和price快照,是个容易被忽略但很关键的设计。菜品价格后期可能调整,如果订单只存dish_id,历史订单金额会跟着变,对账就乱了。存快照等于给订单留了「后悔药」,统计和退款都以快照为准。
3.3 订单状态机设计
订单状态是整个系统的主线,状态流转必须定义清楚,否则商家和用户看到的状态会对不上。这套方案的状态流转是:用户下单生成「待支付」,支付成功变「待接单」,商家接单变「制作中」,出餐变「待取餐」,用户取餐后变「已完成」;用户在「待支付」和「待接单」阶段可以撤单,撤单后库存回滚。
// 订单状态流转校验,放在 Service 层统一拦截非法跳转 public void changeStatus(Long orderId, int targetStatus) { Orders order = orderMapper.selectById(orderId); int current = order.getStatus(); // 只允许相邻状态推进,防止商家跳步或用户乱改 boolean valid = (current == 0 && targetStatus == 1) || (current == 1 && targetStatus == 2) || (current == 2 && targetStatus == 3) || (current == 3 && targetStatus == 4) || ((current == 0 || current == 1) && targetStatus == 5); if (!valid) { throw new BizException("当前状态不允许该操作"); } order.setStatus(targetStatus); orderMapper.updateById(order); }这段校验的意义在于把状态规则收口到一个方法里,用户撤单、商家接单、系统自动完成都走它,避免每个接口各写一套判断导致规则漂移。参数targetStatus对应上面注释里的状态码,非法跳转直接抛业务异常,前端拿到统一错误提示。
4. 核心模块实现:登录、订餐、支付与统计怎么落地
4.1 公共模块:统一返回、分页与登录鉴权
动手写业务之前,先把公共模块搭好,后面每个模块都能省事。统一返回格式用一个Result类包住code、msg、data,前端只认这一种结构,错误处理也统一。分页用 MyBatis-Plus 的Page对象,传页码和每页条数即可。登录鉴权用 JWT,用户登录后签发 token,前端每次请求带在 header 里,后端拦截器校验并解析出用户身份和角色。
// 统一返回结构,所有接口都返回它 public class Result<T> { private int code; // 200成功,其他为业务错误码 private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }登录接口拿到账号密码后,先查用户表比对密码(实际项目里密码要加盐哈希存储,毕设也别存明文),校验通过生成 JWT,把userId和role写进载荷。拦截器里解析 token 后放进ThreadLocal,Service 层随时能取当前用户,不用每个方法都传参。角色判断放在拦截器或注解上,商家接口只允许role=merchant访问,管理员接口只允许role=admin,越权请求直接返回 403。
4.2 用户订餐主流程:选菜、下单与库存扣减
用户订餐是系统最核心的链路,顺序是:浏览菜品列表 → 加入购物车(或直接下单)→ 确认订单 → 支付 → 生成待接单订单。这里最关键的是库存扣减的时机和方式。常见误用是「下单就扣库存」,结果用户不支付,库存被占死;正确做法是支付成功回调里扣库存,并且用数据库行锁或乐观锁防止超卖。
// 支付成功后扣减库存,用乐观锁版本号防并发超卖 @Transactional public void deductStock(Long dishId, int quantity) { int affected = dishMapper.deductStock(dishId, quantity); if (affected == 0) { // 影响行数为0说明库存不足或并发冲突,整单回滚 throw new BizException("菜品库存不足,请重新选择"); } }对应的 Mapper SQL 写成UPDATE dish SET stock = stock - #{qty} WHERE id = #{id} AND stock >= #{qty},把库存判断和扣减放在一条语句里,靠数据库的原子性保证不超卖。@Transactional保证订单状态更新和库存扣减要么都成功要么都回滚。参数quantity来自订单明细,扣减前要校验明细里的菜品都属于同一商家,避免跨商家订单混在一起。
下单接口还要处理取餐时间段。用户在确认页选一个时间段(比如 11:30-12:00),系统按时间段统计已下单量,超过窗口承载量就提示换时段。这个逻辑不复杂,但能体现「错峰就餐」的设计意图,答辩时是个加分点。
4.3 一卡通支付与订单状态推进
支付模块在毕设里通常不接真实支付渠道,而是模拟一卡通扣款:用户确认支付后,调用一个模拟支付接口,校验一卡通余额是否充足,充足则扣款并回调订单服务把状态从「待支付」推到「待接单」。真实项目里这一步会对接学校一卡通中心的接口,毕设阶段用模拟实现完全够用,关键是把「支付回调驱动状态变更」这个模式讲清楚。
// 模拟一卡通支付:校验余额 -> 扣款 -> 回调订单状态 public Result pay(Long orderId, String cardNo) { Orders order = orderMapper.selectById(orderId); if (order.getStatus() != 0) { return Result.fail("订单状态异常,无法支付"); } BigDecimal balance = cardService.getBalance(cardNo); if (balance.compareTo(order.getTotalAmount()) < 0) { return Result.fail("一卡通余额不足"); } cardService.deduct(cardNo, order.getTotalAmount()); orderService.changeStatus(orderId, 1); // 推到待接单 return Result.ok("支付成功"); }支付成功后,商家端订单列表就能看到新订单,商家点「接单」推进到「制作中」,出餐后点「出餐」推进到「待取餐」,用户取餐后系统或商家点「完成」。每一步都走 3.3 里的状态校验方法,保证流转合法。用户撤单只在「待支付」和「待接单」阶段开放,撤单时把库存加回去,形成闭环。
4.4 菜品统计:按菜品与时间段聚合销售数据
统计模块是商家最关心的功能,也是答辩时容易被追问的点。需求是「查询菜品统计列表并导出」,实现上就是按菜品分组聚合订单明细的销量和金额,支持按日期范围过滤。
-- 按菜品统计销量与销售额,支持时间段过滤 SELECT oi.dish_id, oi.dish_name, SUM(oi.quantity) AS total_qty, SUM(oi.price * oi.quantity) AS total_amount FROM order_item oi JOIN orders o ON oi.order_id = o.id WHERE o.status = 4 -- 只统计已完成订单 AND o.create_time BETWEEN #{start} AND #{end} GROUP BY oi.dish_id, oi.dish_name ORDER BY total_qty DESC;这条 SQL 只统计「已完成」订单,避免把撤单和未支付的数据算进去,统计口径要和商家对账口径一致。导出功能用 EasyExcel 或 POI 把查询结果写成 Excel,字段就是菜品名、销量、销售额,商家拿去就能估算第二天备料量。参数start和end由前端日期选择器传入,后端做格式校验,防止 SQL 注入。
5. 避坑与常见问题:这几处最容易翻车
5.1 库存超卖:并发下扣成负数
现象是压测或多人同时下单时,某菜品库存变成负数,订单却生成成功。原因在于「先查库存再扣减」分了两步,两个请求同时查到库存为 1,都判断充足,然后各自扣减。解决办法是把判断和扣减合并成一条带条件的 UPDATE,靠数据库行锁保证原子性,影响行数为 0 就抛异常回滚整单,如 4.2 所示。
5.2 订单状态乱跳:商家和用户看到的不一致
现象是用户撤单后商家还能接单,或者订单没支付就显示「制作中」。根因是状态变更散落在多个接口里,各写各的判断。解决办法是收口到一个changeStatus方法,所有状态推进都走它,非法跳转统一抛异常,前端根据返回码提示。这条规则写进 Service 层,比在每个 Controller 里补判断可靠得多。
5.3 金额对不上:菜品改价后历史订单跟着变
现象是商家调整菜品价格后,用户查历史订单发现金额变了,对账对不上。原因是订单只存了dish_id,展示时实时关联菜品表取价格。解决办法是下单时把菜品名称和单价快照写进订单明细表,统计和展示都以快照为准,菜品表改价不影响历史订单。
5.4 登录态失效:token 过期后接口报错不友好
现象是用户停留久了再操作,接口直接抛 500 或空白页。原因是拦截器解析 token 失败后没有统一处理。解决办法是拦截器捕获 token 异常,返回明确的 401 状态码和「登录已过期」提示,前端拦截 401 后跳转登录页,而不是让用户对着报错发呆。
5.5 统计口径不一致:商家看到的销量和实际不符
现象是统计报表里的销量比商家自己记的多或少。原因通常是统计时没过滤订单状态,把待支付、已取消的订单也算进去了。解决办法是统计 SQL 固定加status = 4(已完成)条件,并在页面上标注统计口径,避免商家误解。
6. 验收与答辩:怎么证明这套系统真的能用
验收这件事,我一般分三步走:功能走查、异常路径测试、数据核对。功能走查按角色来,用户端把「选菜—下单—支付—查单—撤单」完整走一遍,商家端把「上架—接单—出餐—统计导出」走一遍,管理员端把「新增商家—停用商家」走一遍,每一步截图留档。异常路径重点测三处:库存不足时下单是否被拦、未支付订单能否被商家接单、越权访问商家接口是否返回 403。数据核对则是下单前后对比菜品库存和订单金额,确认扣减和快照都对。
答辩时老师最爱问的无非几个:库存怎么防超卖、订单状态怎么保证不乱、统计口径是什么、权限怎么控制。提前把这几处的代码和 SQL 准备好,能当场翻出来讲,比背 PPT 管用。下面这张表可以当自测清单用。
| 验收项 | 操作 | 预期结果 |
|---|---|---|
| 下单扣库存 | 库存 1 时两人同时下单 | 仅一人成功,库存不为负 |
| 状态流转 | 未支付订单点接单 | 提示状态异常,拒绝操作 |
| 撤单回滚 | 待接单订单撤单 | 状态变已取消,库存加回 |
| 权限隔离 | 用户 token 调商家接口 | 返回 403 |
| 统计口径 | 含撤单订单的日期范围统计 | 只统计已完成订单 |
| 金额快照 | 改菜品价后查历史订单 | 历史金额不变 |
答辩演示有个小技巧:提前准备好两个账号(用户和商家)和几条测试数据,现场用手机扫码演示用户下单,再切到电脑演示商家接单和统计导出,整个链路一气呵成,比逐个页面点过去有说服力。演示前一定要把数据重置到干净状态,别让上一位同学留下的脏数据把流程卡住。
从那以后我每次带项目,验收前都强制自己按角色把主流程和异常路径各走一遍,宁可多花半小时,也不在答辩现场翻车。希望这套结构和清单能帮到你,照着改改就能变成你自己的方案。
本文还有配套的精品资源,点击获取