news 2026/10/28 19:14:57

Spring Boot + uni-app 校园订餐系统实战:从架构设计到订单状态流转

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot + uni-app 校园订餐系统实战:从架构设计到订单状态流转

简介:这份文档面向计算机专业毕业生与Java Web初学者,围绕校园食堂订餐系统的毕业设计展开,针对疫情下食堂拥挤、学生外卖饮食不健康、商家销售统计困难等现实痛点,给出了一套可参考的完整课题方案。资源包共1个doc文件,约3.32MB,内容涵盖系统总体功能设计、Spring Boot后台与uni-app前台的架构选型、MySQL数据存储方案,以及手机端点餐、订单提交与限时修改撤销、一卡通在线支付、菜品销售统计等核心模块的实现思路,并附有系统测试与调试过程记录。目前已有9254人学习下载,说明该选题在毕业设计场景中具有较高的参考价值。读者可从中获取需求分析、技术选型、功能模块划分与论文写作框架等完整素材,适合作为同类订餐系统开发与答辩准备时的对照范本。

1. 从食堂排队到手机下单:这套 Spring Boot 订餐系统到底解决了什么

中午十二点下课铃一响,几千人同时涌向食堂,窗口前排起的长队能拐三个弯——这是很多高校后勤最头疼的场景。这份毕业设计资源给出的答案很直接:把选菜、下单、支付、统计整条链路搬到手机上,让学生错峰点单、到点取餐,商家按预售量备料。系统后台用 Spring Boot 搭 REST 接口,前台用 uni-app 一套代码同时覆盖 Android、iOS、H5 和小程序,数据库选 MySQL,整体是标准的 B/S 架构。它适合两类人:一是拿它当毕业设计底稿、需要一套能跑通全流程代码的学生;二是想照着这套结构改造成自己单位内部订餐工具的开发。功能边界很清楚——餐厅管理、菜品管理、订单管理、菜品统计、我的订单、在线支付六块,没有花哨的推荐算法,但每一块都能落到表结构和接口上。

2. 技术选型与整体架构:为什么是 Spring Boot + uni-app + MySQL

2.1 后端为什么选 Spring Boot 而不是传统 SSM

这个项目的业务量级不大,但角色不少:食堂管理员、商家、学生/教职工三类人,各自的操作权限和数据范围完全不同。传统 SSM 要写一堆 XML 映射和 Tomcat 部署配置,对一个人独立开发的毕业设计来说,配置成本会吃掉大量写业务逻辑的时间。Spring Boot 的自动装配把数据源、事务、JSON 序列化这些默认配好,开发者只需要在application.yml里声明连接信息,剩下的精力放在 Controller 和 Service 上。

我一般会把项目按职责切成四层,这也是这类系统最常见的做法:

canteen-order/ ├── src/main/java/com/canteen/ │ ├── controller/ # 接口层:接收请求、参数校验、返回统一格式 │ ├── service/ # 业务层:订单状态流转、库存扣减、统计聚合 │ ├── mapper/ # 持久层:MyBatis 接口 + XML │ ├── entity/ # 实体类:与数据表一一对应 │ └── common/ # 公共模块:分页、统一返回、异常处理 ├── src/main/resources/ │ ├── application.yml # 数据源、端口、文件上传路径 │ └── mapper/ # 各模块 SQL 映射文件 └── pom.xml

分层的好处是改需求时定位快。比如“订单提交后 15 分钟内可撤销”这条规则,只动 Service 层,Controller 和 Mapper 不用碰。论文里提到的“公共模块类”其实就是这套分层里的common包,包含数据库连接、分页工具、统一返回格式和表格导出工具,后面第 3 章会逐个拆。

2.2 前端为什么用 uni-app 而不是纯 Vue

论文摘要里写得很明确:前台要跨 Android、iOS、H5、小程序。如果分别用原生 Android 和 iOS 各写一套,工作量翻倍还不止。uni-app 基于 Vue 语法,编译时输出各端代码,一套页面逻辑复用,这对独立开发者几乎是唯一现实的选择。后台管理端则用 WebStorm 配合 Vue 开发,跑在浏览器里,和移动端共用同一套后端接口。

这里有个容易忽略的点:移动端和后台管理端的接口虽然同源,但返回字段的裁剪需求不同。学生端查菜品只需要名称、价格、图片、余量;商家端还要看成本、原材料、历史销量。常见做法是在 Controller 里用不同的 VO(视图对象)承接,而不是把整张表字段一股脑返回。这样既减少移动端流量,也避免把成本价这类敏感字段暴露给学生端。

2.3 数据库与运行环境的最低配置

论文给出的软硬件环境可以直接照抄作为起步配置:CPU 四核 1.6G 以上、内存 4G 以上、磁盘 1TB;操作系统 Windows 10,Web 服务 Tomcat 7.0,数据库 MySQL 8.0。实际开发时 Tomcat 可以省掉,Spring Boot 内置了容器,打成 jar 直接java -jar启动更省事。MySQL 8.0 要注意驱动版本,mysql-connector-java用 8.x 系列,连接串里加上serverTimezone=Asia/Shanghai,否则时间字段会差 8 小时,订单的“可撤销时间窗口”会算错——这个坑后面第 5 章会细说。

3. 核心模块拆解:从登录鉴权到订单状态流转

3.1 公共模块:统一返回格式与分页,先把地基打平

论文第 4 章开头讲“公共模块类”,这是整个项目最值得先看的部分,因为后面每个接口都依赖它。统一返回格式的作用是让前端不用为每个接口写不同的解析逻辑,成功、失败、未登录都走同一个结构。

// common/Result.java —— 全局统一返回体 public class Result<T> { private Integer code; // 200 成功,401 未登录,500 业务异常 private String message; // 给前端直接展示的提示语 private T data; // 泛型承载具体业务数据 public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "操作成功"; r.data = data; return r; } public static <T> Result<T> fail(Integer code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }

code用数字而不是布尔值,是为了区分“未登录”和“业务失败”两种场景:前端拦截器看到 401 就跳登录页,看到 500 只弹提示不跳转。data用泛型,分页查询时塞PageInfo,单条查询时塞实体对象,前端拿到的结构始终是{code, message, data}三层。

分页工具类通常基于 PageHelper 实现,在查询前一行代码开启分页:

// service/DishServiceImpl.java —— 菜品分页查询 public PageInfo<Dish> pageQuery(Integer pageNum, Integer pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); // 必须紧挨着查询语句 List<Dish> list = dishMapper.selectByKeyword(keyword); return new PageInfo<>(list); // 自动封装总数、页码 }

PageHelper.startPage有个硬性要求:它只对紧随其后的第一条 MyBatis 查询生效。如果中间插了一句别的查询,分页会作用到错误的语句上,表现为“总数对但列表数据不对”。这是新手最容易翻车的地方之一,写的时候把startPage和查询语句贴在一起,中间不留任何逻辑。

3.2 登录模块:三种角色如何共用一套鉴权

系统里有食堂管理员、商家、学生三类角色,登录入口可以共用一个,靠用户表里的role字段区分。登录成功后后端签发 token,前端存本地,后续请求在请求头里带上。论文里登录模块的实现思路是账号密码校验加角色判断,这里补一个可落地的 token 方案:

// controller/LoginController.java —— 登录并签发 token @PostMapping("/login") public Result<Map<String, Object>> login(@RequestBody LoginDTO dto) { User user = userService.checkPassword(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.fail(500, "账号或密码错误"); } // token 里只放用户 id 和角色,不放敏感信息 String token = JwtUtil.create(user.getId(), user.getRole()); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("role", user.getRole()); // 前端据此决定跳商家端还是学生端 return Result.success(data); }

checkPassword里要做的是先按用户名查记录,再把前端传来的明文密码做一次 MD5 加盐比对,不能直接拿明文去数据库里where password = ?。JwtUtil.create的过期时间建议设 2 小时,食堂场景下学生不会长时间挂着页面,过期后重新登录的成本很低,换来的是 token 泄露后的风险窗口更小。

角色鉴权放在拦截器里做,而不是每个接口里写 if-else:

// common/AuthInterceptor.java —— 按路径前缀校验角色 public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { String uri = req.getRequestURI(); String token = req.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token)) { resp.setStatus(401); return false; } String role = JwtUtil.getRole(token); // /api/admin/** 只允许食堂管理员,/api/merchant/** 只允许商家 if (uri.startsWith("/api/admin") && !"ADMIN".equals(role)) { resp.setStatus(403); return false; } return true; }

路径前缀和角色的映射关系写死在拦截器里,比在数据库里配一张权限表简单得多,对毕业设计这个规模完全够用。学生端的接口前缀是/api/student/**,登录后即可访问,不需要额外角色判断。

3.3 订单模块:提交、撤销与状态机

订单是整个系统最核心也最容易出 bug 的模块。论文要求“一定时间范围内可修改和撤销订单”,这意味着订单不是简单的增删改查,而是一个带时间约束的状态流转。我一般会把状态定义成枚举,避免用魔法数字:

// entity/OrderStatus.java —— 订单状态枚举 public enum OrderStatus { WAIT_PAY(0, "待支付"), PAID(1, "已支付"), PREPARING(2, "备餐中"), FINISHED(3, "已完成"), CANCELLED(4, "已取消"); // 构造、getter 省略 }

提交订单时要做三件事:校验菜品余量、扣减库存、写入订单主表和明细表。这三步必须在同一个事务里,否则会出现“库存扣了但订单没写进去”的脏数据:

// service/OrderServiceImpl.java —— 提交订单(核心事务) @Transactional(rollbackFor = Exception.class) public Long submitOrder(Long userId, List<OrderItemDTO> items) { BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : items) { // 用行锁查库存,防止并发超卖 Dish dish = dishMapper.selectForUpdate(item.getDishId()); if (dish.getStock() < item.getNum()) { throw new BizException("菜品[" + dish.getName() + "]余量不足"); } dishMapper.decreaseStock(item.getDishId(), item.getNum()); total = total.add(dish.getPrice().multiply(new BigDecimal(item.getNum()))); } Order order = new Order(); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(OrderStatus.WAIT_PAY.getCode()); order.setCreateTime(new Date()); orderMapper.insert(order); // 主表 orderItemMapper.insertBatch(order.getId(), items); // 明细表 return order.getId(); }

selectForUpdate对应 SQL 里的select ... for update,在事务内锁住这一行,第二个并发请求会等待而不是读到旧库存。这是防超卖最直接的手段,代价是并发量高时会有锁等待,但食堂场景的峰值也就几百人同时下单,完全扛得住。

撤销订单的约束是“下单后 15 分钟内且未支付”:

// service/OrderServiceImpl.java —— 撤销订单 public void cancelOrder(Long orderId, Long userId) { Order order = orderMapper.selectById(orderId); if (!order.getUserId().equals(userId)) { throw new BizException("无权操作他人订单"); } long minutes = (System.currentTimeMillis() - order.getCreateTime().getTime()) / 60000; if (minutes > 15) { throw new BizException("已超过15分钟,无法撤销"); } if (order.getStatus() != OrderStatus.WAIT_PAY.getCode()) { throw new BizException("已支付订单请联系商家处理"); } orderMapper.updateStatus(orderId, OrderStatus.CANCELLED.getCode()); // 库存回滚,和提交时的扣减对称 orderItemMapper.selectByOrderId(orderId) .forEach(i -> dishMapper.increaseStock(i.getDishId(), i.getNum())); }

时间窗口用createTime算差值,而不是在数据库里存一个“截止时间”字段,好处是规则改了只改代码不改表。库存回滚必须和状态更新在同一个事务里,否则会出现“订单取消了但库存没回来”的情况,第二天商家备料就会少算。

3.4 菜品统计:按销量聚合给商家备料依据

统计模块的需求是“商家根据之前销售情况预估一天所需原材料”。落地时不需要复杂的报表引擎,一条分组聚合 SQL 加一个导出接口就够:

-- mapper/OrderItemMapper.xml —— 按菜品统计销量 SELECT d.id, d.name, d.unit, SUM(oi.num) AS total_num, SUM(oi.num * d.price) AS total_amount FROM order_item oi JOIN dish d ON oi.dish_id = d.id JOIN orders o ON oi.order_id = o.id WHERE o.status IN (1, 2, 3) -- 只统计已支付及之后的订单 AND o.create_time BETWEEN #{start} AND #{end} GROUP BY d.id, d.name, d.unit ORDER BY total_num DESC;

status IN (1,2,3)这个条件很关键:待支付和已取消的订单不能计入销量,否则统计结果虚高,商家按这个数备料会浪费。导出功能用 POI 把查询结果写成 Excel,论文里提到的“表格工具类”就是封装了创建工作簿、设置表头、写数据这几步,调用方只需要传List和列名数组。

4. 本地跑起来:从建库到前后端联调的完整步骤

4.1 数据库初始化与配置修改

拿到源码后第一步不是急着启动,而是先把库建好。按论文第 3 章的 E-R 设计,核心表至少有六张:用户表、餐厅表、菜品表、订单主表、订单明细表、支付记录表。建库语句大致如下:

CREATE DATABASE canteen_order DEFAULT CHARACTER SET utf8mb4; USE canteen_order; CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL, -- MD5 加盐后的值 `role` VARCHAR(20) NOT NULL, -- ADMIN / MERCHANT / STUDENT `real_name` VARCHAR(50), `card_no` VARCHAR(30) -- 一卡通号,学生角色使用 ) ENGINE=InnoDB; CREATE TABLE `dish` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `merchant_id` BIGINT NOT NULL, `name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `unit` VARCHAR(10), `image_url` VARCHAR(255), `status` TINYINT DEFAULT 1 -- 1 上架 0 下架 ) ENGINE=InnoDB;

utf8mb4而不是utf8,是因为菜品名里可能出现生僻字或特殊符号,utf8三字节存不下会报错。金额字段用DECIMAL(10,2)不用FLOAT,浮点数做累加会出现0.1 + 0.2 = 0.30000000000000004这类精度问题,订单总价对不上账。

建完表后改application.yml:

spring: datasource: url: jdbc:mysql://localhost:3306/canteen_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver server: port: 8080

serverTimezone必须显式指定,MySQL 8.0 驱动默认按 UTC 解析时间,不设的话订单创建时间会比实际早 8 小时,撤销窗口的判断直接失效。

4.2 后端启动与接口自测

配置改完,在项目根目录执行mvn clean package,成功后target目录下会生成 jar 包,java -jar xxx.jar启动。看到控制台打印Started Application in x seconds就说明起来了。先用 Postman 或浏览器测登录接口:

curl -X POST http://localhost:8080/api/login \ -H "Content-Type: application/json" \ -d '{"username":"student01","password":"123456"}'

返回体里拿到 token 后,后续请求在 Header 里加Authorization: token值。如果返回 401,先检查拦截器有没有把登录接口本身也拦了——登录接口的路径必须加入白名单,否则永远登不进去。这是联调阶段最高频的问题,没有之一。

4.3 前端 uni-app 的编译与接口地址切换

前端用 HBuilderX 打开 uni-app 工程,找到请求封装文件,把baseUrl改成后端实际地址。开发阶段跑 H5 最方便,浏览器里直接调试;要出小程序包就在 HBuilderX 里选对应平台编译。注意 H5 端存在跨域,后端要加一个全局 CORS 配置:

// common/CorsConfig.java —— 开发阶段放开跨域 @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true); } }

allowedOriginPatterns是 Spring 5.3 之后推荐写法,配allowedOrigins("*")加allowCredentials(true)会直接抛异常。上线前要把*换成具体域名,开发阶段图省事可以放开。

5. 避坑与排查:五个真实会卡住你的问题

5.1 订单时间差 8 小时,撤销窗口形同虚设

现象:刚下的订单点撤销,提示“已超过 15 分钟”。原因:MySQL 驱动时区没配,createTime存进去是 UTC 时间,取出来和本地时间比较差了 8 小时。解决:连接串加serverTimezone=Asia/Shanghai,同时确认数据库服务器时区与开发机一致。改完后已有数据要手动修正,否则历史订单仍然算错。

5.2 并发下单出现负库存

现象:两个学生同时买最后一份菜,两边都下单成功,库存变成 -1。原因:先查库存再扣减,两条语句之间没有锁,两个事务读到的都是 1。解决:扣减 SQL 改成update dish set stock = stock - #{num} where id = #{id} and stock >= #{num},靠数据库行锁保证原子性,返回影响行数为 0 就抛“余量不足”。这比selectForUpdate更轻量,适合扣减场景。

5.3 分页查询总数正确但列表为空

现象:菜品列表第一页有数据,翻到第二页空白,但总数显示还有 20 条。原因:PageHelper.startPage之后插入了别的查询语句,分页参数被那条语句消费掉了。解决:确保startPage紧挨着目标查询,中间不夹任何 Mapper 调用;排查时在startPage下一行打日志确认。

5.4 小程序端图片不显示

现象:H5 端菜品图片正常,编译到小程序后全部裂图。原因:图片地址存的是相对路径或localhost,小程序运行环境访问不到开发机。解决:图片统一存到后端静态资源目录,数据库存完整 URL;开发阶段用局域网 IP 代替localhost,手机和电脑连同一个网络才能访问。

5.5 支付回调重复触发导致重复加库存

现象:一卡通支付成功后,订单状态被改了两次,库存多加了一份。原因:支付平台的回调可能重试,接口没有做幂等。解决:回调入口先按订单号查支付记录,已存在就直接返回成功不再处理;或者用订单状态做前置判断,只有WAIT_PAY状态才允许流转到PAID。幂等这件事在支付链路上是硬要求,不能靠“应该不会重复”侥幸。

6. 二次开发改造:把毕业设计变成能真正上线的工具

这套代码作为毕设是完整的,但要真放到食堂用,有几处值得动手改。第一处是鉴权从手写 JWT 换成 Spring Security,角色权限用注解@PreAuthorize("hasRole('MERCHANT')")声明,比拦截器里写路径前缀更不容易漏。第二处是订单状态流转加一张状态变更日志表,每次改状态记一条,出问题时能查“谁在什么时候把订单从备餐中改成了已完成”,这是线上系统必备的黑匣子。

第三处改造收益最大:把菜品库存的扣减从“下单即扣”改成“支付成功才扣”。现在的逻辑是提交订单就锁库存,学生下单不付款,菜就被占着,高峰期会造成虚假售罄。改法是提交时只校验不扣减,支付回调里再执行扣减,同时加一个定时任务清理超过 15 分钟未支付的订单并释放占用。这个改动涉及订单、支付、定时任务三个模块,正好是练手事务和调度的好场景。

验证改造是否成功,我习惯用一组固定用例跑一遍:两个账号同时抢最后一份菜,看是否只有一个成功;下单后等 16 分钟再撤销,看是否被拒;支付回调手动重放三次,看库存是否只扣一次。这三条过了,核心链路基本就稳了。

改造点改动范围验证方式
鉴权换 Spring Security拦截器、用户模块三角色分别访问越权接口应返回 403
订单状态日志订单 Service、新增日志表每次状态变更后查日志表有对应记录
支付后才扣库存订单、支付、定时任务未支付订单 15 分钟后库存自动释放

从那以后我每次接这类订餐项目,都会先把“时间、库存、幂等”这三件事的用例写成清单,跑通了再往下写业务,不然越往后改代价越大。这套源码的结构清晰、模块边界干净,拿来当二次开发的起点是合适的,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 19:14:53

微信小程序医院预约挂号系统源码解析:从科室浏览到预约落库

简介&#xff1a;这份资源是面向计算机专业毕业设计场景的微信小程序医院预约挂号系统配套文档&#xff0c;适合正在准备Java方向毕设的本科生或需要参考完整项目方案的自学者。文档围绕微信小程序前端、Spring Boot与SSM后端、MySQL数据库的技术组合展开&#xff0c;涵盖项目背…

作者头像 李华
网站建设 2026/10/28 19:13:07

生成式AI计入企业预算:40%工时与任务拆解落地路径

简介&#xff1a;《生成式人工智能.pdf》是一份面向企业管理者、技术从业者及AI入门读者的行业报告型资料&#xff0c;聚焦ChatGPT背后的大语言模型与生成式AI技术&#xff0c;帮助读者理解这一轮技术变革的来龙去脉、企业落地方式及潜在影响。压缩包内仅含1个PDF文件&#xff…

作者头像 李华
网站建设 2026/10/28 19:11:30

星盘接口开发文档:次限盘接口指南

星盘接口开发文档&#xff1a;次限盘接口指南 1. 引言 本文档详细介绍了占星系统的次限盘接口的使用方法&#xff0c;包括请求参数详解、响应数据结构、错误处理机制以及最佳实践建议。 2. 接口基础信息 接口名称: 次限盘 请求方式: POSTContent-Type: application/x-www-form-…

作者头像 李华
网站建设 2026/10/28 19:08:57

从零开始自学C语言

自我介绍我是一名刚刚进入大一的学生 &#xff0c;面对突如其来的自由&#xff0c;没有了明确的高考目标&#xff0c;我一瞬间就失去了所有的方向感&#xff0c;现在在读的专业也不是我所想要的&#xff0c;也曾陷入过深深的迷茫。相比起智能制造&#xff0c;我更热爱在屏幕前敲…

作者头像 李华