简介:这份资源是面向计算机专业学生与Java Web开发学习者的毕业设计完整文档,聚焦疫情背景下校园食堂就餐拥挤、外卖不健康、点餐不便等现实痛点,给出了一套可落地的订餐系统设计方案。文档围绕Spring Boot轻量框架与B/S架构展开,前台采用可跨Android、iOS、H5及小程序的uni-app,后台结合MySQL存储,完整覆盖手机端选菜、订单提交与限时修改撤销、一卡通在线支付、菜品销售统计等核心模块,并附系统测试与调试过程。资源包共1个doc文件,约3.32MB,内容为论文全文,含中英文摘要、系统总体设计、功能实现与总结展望,结构完整、论述详实。目前已有9268人学习下载,适合需要参考选题思路、功能模块划分、技术选型论证与论文写作框架的读者,可据此快速梳理毕设脉络、对照实现要点并查漏补缺。
1. 从一份毕业设计文档说起:校园食堂订餐系统到底解决了什么问题
每年毕业季,后台总有人问我要一份能跑起来的 Spring Boot 项目参考。这次拆的是一份完整的毕业设计文档——《基于 Spring Boot 的校园食堂订餐系统的设计与实现》。它不是那种只停留在需求分析层面的空壳论文,而是把需求、架构、数据库、核心模块代码、测试用例都写全了的一套落地材料。系统面向校园场景,学生用手机端选菜下单,商家在后台维护菜品、处理订单、看销售统计,管理员管餐厅和商家。技术栈是 Spring Boot + uni-app + MySQL,B/S 架构,前后端分离。如果你正在找一个结构完整、业务闭环清晰、适合二次开发的 Java 全栈练手项目,这份文档值得认真读一遍。
2. 技术选型与整体架构:为什么是 Spring Boot 加 uni-app 这套组合
2.1 后端为什么选 Spring Boot 而不是传统 SSM
文档里明确写了后台使用 Spring Boot 新型轻量开发框架,这在毕业设计里是个很务实的选择。传统 SSM 要配一堆 XML,光是把 MyBatis、Spring MVC、数据源、事务管理器串起来就得折腾半天,对刚上手的学生来说心智负担太重。Spring Boot 的自动装配把大部分约定俗成的配置都收走了,一个application.yml加几个 starter 依赖就能把 Web 层、持久层、事务跑起来。
从项目实际需要看,这个系统的后端职责很清晰:接收前端请求、校验参数、操作数据库、返回统一格式的 JSON。没有复杂的分布式事务,没有高并发削峰,Spring Boot 的内嵌 Tomcat 加默认连接池完全够用。文档里提到的公共模块类——连接数据库、分页显示、返回信息格式、表格工具类——本质上就是 Spring Boot 项目里最常见的几个基础设施封装,后面第 4 章会逐个拆。
2.2 前端为什么用 uni-app 而不是纯 Vue
这里有个容易被忽略的细节:文档摘要里写的是“前台技术使用可跨 Android、IOS、H5、小程序的 uni-app 进行开发”,但关键词和正文里又反复出现 vue.js。这两者不矛盾——uni-app 本身就是基于 Vue 语法的跨端框架,写一套代码可以编译到多个平台。对校园订餐场景来说,学生不一定都装 App,H5 和小程序才是触达成本最低的入口,uni-app 正好覆盖这个需求。
如果换成纯 Vue 做 Web 端,学生得打开浏览器输网址,体验上差一截;如果做原生 App,开发和维护成本又太高。uni-app 在这个项目里是个性价比很高的折中。实际开发时,页面用 Vue 的模板语法写,调用后端接口用uni.request,条件编译处理各端差异。
2.3 B/S 架构下的角色划分
系统采用基本 B/S 互联网架构,角色分三类:学生/教职工、商家、食堂管理员。学生端是移动端订餐入口,商家端和管理员端是后台管理界面。文档第 2 章的功能需求把权限边界划得很清楚:
| 角色 | 核心功能 | 对应模块 |
|---|---|---|
| 学生/教职工 | 浏览菜品、下单、支付、查订单、取消订单 | 用户订餐、我的订单 |
| 商家 | 菜品增删改查、发布菜单、订单处理、销售统计 | 菜品管理、订单管理、菜品统计 |
| 食堂管理员 | 新增/删除商家、查看商家详情 | 餐厅管理 |
这种三角色划分在数据库设计上会体现为用户表加角色字段,登录后根据角色跳不同首页。后面讲登录模块时会看到具体的拦截逻辑。
2.4 开发运行环境清单
文档第 2 章给了明确的软硬件环境,照着配基本不会翻车:
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10 |
| Web 服务 | Tomcat 7.0(Spring Boot 内嵌可替代) |
| 数据库 | MySQL 8.0 |
| 后端 IDE | IntelliJ IDEA 2019.3.1 x64 |
| 前端 IDE | WebStorm 2020.1 x64 |
| CPU | 1.6G 主频 4 核 |
| 内存 | 4G 及以上 |
| 磁盘 | 最低 1TB |
提示:文档写的是 Tomcat 7.0 外置部署,但 Spring Boot 项目通常用内嵌 Tomcat 直接
java -jar启动,实际搭建时不必单独装 Tomcat,把打好的 jar 跑起来即可。MySQL 8.0 要注意驱动版本和连接串里的时区参数,这是新手最容易卡住的地方。
3. 数据库与模块设计:从 E-R 图到五张核心表的落地
3.1 数据库设计原则与 E-R 图分析
文档第 3 章明确了几条数据库设计原则,归纳起来就是:表结构要贴合业务实体、字段类型要精确、主外键关系要清晰、避免冗余存储。系统涉及的实体主要有用户(学生/教职工)、商家、餐厅、菜品、订单、订单明细。E-R 图里能看出几个关键关系:一个餐厅下有多个商家,一个商家维护多个菜品,一个用户可以有多个订单,一个订单包含多条菜品明细。
这种一对多和多对多的关系,落到表设计上就是:菜品表带商家 ID 外键,订单表带用户 ID 外键,订单明细表同时带订单 ID 和菜品 ID。订单明细表是典型的中间表,用来解决订单和菜品之间的多对多关系,同时记录下单时的菜品快照(单价、数量),避免菜品后续改价影响历史订单。
3.2 核心数据表结构
根据文档第 3.3.3 节“数据表的创建”和功能模块描述,可以还原出几张核心表的结构。下面用建表语句的形式给出,字段名按常见命名习惯补全,实际以文档原表为准:
-- 用户表:学生、教职工、商家、管理员共用,用 role 区分 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT '加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', role TINYINT NOT NULL COMMENT '1学生 2教职工 3商家 4管理员', card_no VARCHAR(30) COMMENT '一卡通卡号', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 餐厅表:管理员维护 CREATE TABLE restaurant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '餐厅名称', location VARCHAR(200) COMMENT '位置描述', status TINYINT DEFAULT 1 COMMENT '1营业 0停业' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品表:商家维护,关联餐厅 CREATE TABLE dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT, restaurant_id BIGINT NOT NULL COMMENT '所属餐厅', name VARCHAR(100) NOT NULL COMMENT '菜品名称', price DECIMAL(10,2) NOT NULL COMMENT '单价', stock INT DEFAULT 0 COMMENT '供应量', material VARCHAR(200) COMMENT '原材料', image_url VARCHAR(255) COMMENT '图片地址', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_restaurant (restaurant_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表:记录订单主信息 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号', user_id BIGINT NOT NULL COMMENT '下单用户', restaurant_id BIGINT NOT NULL COMMENT '所属餐厅', total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总额', status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已完成 3已取消', pay_time DATETIME COMMENT '支付时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表:记录每笔订单的菜品快照 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '订单ID', dish_id BIGINT NOT NULL COMMENT '菜品ID', dish_name VARCHAR(100) COMMENT '下单时菜品名快照', price DECIMAL(10,2) COMMENT '下单时单价快照', quantity INT NOT NULL COMMENT '数量', KEY idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段建表语句有几个设计点值得说。第一,用户表用role字段区分四种身份,而不是拆成四张表,好处是登录逻辑统一,坏处是角色专属字段(比如一卡通卡号)会稀疏存储,对这个规模的项目来说可以接受。第二,订单明细表冗余了dish_name和price,这是有意为之——菜品改价或下架后,历史订单展示的仍是下单时的信息,这是订单类系统的通用做法。第三,订单表加了order_no唯一索引,对外暴露订单号而不是自增 ID,避免被遍历。
3.3 功能模块划分与数据流
文档第 3.4 节把系统功能模块拆成五块:食堂管理、菜品管理、食堂订餐管理、菜品统计管理、系统管理。对应到代码结构上,后端一般按 controller、service、mapper 三层组织,每个模块一组类。数据流图上能看到两条主链路:一条是学生端“浏览菜品 → 加购 → 提交订单 → 支付 → 查看订单”,另一条是商家端“维护菜品 → 接收订单 → 更新状态 → 统计销售”。
这两条链路在数据库层面是交汇的:学生下单写orders和order_item,商家处理订单读同一批表并更新status,统计模块则对order_item按dish_id聚合求和。理解了这个交汇点,后面看订单管理和统计模块的代码就不会迷路。
4. 核心模块代码拆解:公共类、登录、订单与统计的实现细节
4.1 公共模块类:统一返回格式与分页封装
文档第 4.1 节列了四个公共模块类:连接数据库、分页显示、返回信息格式、表格工具类。其中“返回信息格式”是整个前后端协作的契约,最值得先看。常见做法是定义一个泛型结果类,包含状态码、提示信息、数据体:
public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String message; // 提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("操作成功"); r.setData(data); return r; } public static <T> Result<T> fail(String message) { Result<T> r = new Result<>(); r.setCode(500); r.setMessage(message); return r; } // getter/setter 省略 }这个类的价值在于把“成功/失败”的判断标准化。前端拿到响应后先看code,200 才取data渲染,否则弹message。如果没有这层封装,每个接口返回结构都不一样,前端得写一堆兼容逻辑,维护起来很痛苦。
分页封装通常配合 MyBatis 的分页插件使用,Service 层接收pageNum和pageSize两个参数,返回一个包含total(总条数)和list(当前页数据)的对象。文档里“分页显示”被单独列为一个公共类,说明作者把它做成了可复用的工具,菜品列表、订单列表、统计列表都走同一套分页逻辑。
4.2 登录模块:角色区分与拦截器
登录是文档第 4.2 节的重点。这个系统的登录不只是校验账号密码,还要根据角色决定跳转和权限。核心逻辑分两步:先查用户,再比对密码,最后把用户信息写入 Session 或返回 Token。
@PostMapping("/login") public Result<UserVO> login(@RequestBody LoginDTO dto, HttpSession session) { // 1. 按用户名查用户 User user = userService.getByUsername(dto.getUsername()); if (user == null) { return Result.fail("账号不存在"); } // 2. 密码比对(实际项目用 BCrypt 等加密,这里示意) if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.fail("密码错误"); } // 3. 写入会话,供拦截器校验 session.setAttribute("loginUser", user); // 4. 返回脱敏后的用户信息,含角色 UserVO vo = new UserVO(); vo.setId(user.getId()); vo.setRealName(user.getRealName()); vo.setRole(user.getRole()); return Result.success(vo); }参数说明:LoginDTO承载前端传来的用户名和密码;HttpSession用来存登录态,后续请求靠拦截器从 Session 里取loginUser判断是否登录。UserVO是视图对象,故意不带密码字段,避免敏感信息泄露。
配套的拦截器大致长这样:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { Object user = req.getSession().getAttribute("loginUser"); if (user == null) { // 未登录,返回 401,前端跳登录页 resp.setStatus(401); return false; } return true; } }这里有个容易踩的坑:拦截器要排除登录接口和静态资源,否则登录请求本身会被拦下来,形成死循环。配置类里用excludePathPatterns("/login", "/static/**")处理。
4.3 订单管理:下单、改单与状态流转
订单模块是文档第 4.3.3 节的内容,也是整个系统业务最重的地方。学生下单时要做三件事:校验菜品库存、生成订单主记录、写入订单明细。这三步必须在一个事务里完成,否则可能出现订单生成了但明细没写进去的脏数据。
@Transactional(rollbackFor = Exception.class) public String createOrder(Long userId, List<OrderItemDTO> items) { // 1. 计算总价并校验库存 BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : items) { Dish dish = dishMapper.selectById(item.getDishId()); if (dish == null || dish.getStatus() == 0) { throw new BizException("菜品已下架:" + item.getDishId()); } if (dish.getStock() < item.getQuantity()) { throw new BizException("库存不足:" + dish.getName()); } total = total.add(dish.getPrice().multiply(new BigDecimal(item.getQuantity()))); } // 2. 生成订单主记录 Orders order = new Orders(); order.setOrderNo(generateOrderNo()); // 时间戳+随机数 order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); // 待支付 orderMapper.insert(order); // 3. 写入明细并扣减库存 for (OrderItemDTO item : items) { Dish dish = dishMapper.selectById(item.getDishId()); OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setDishId(dish.getId()); oi.setDishName(dish.getName()); // 快照 oi.setPrice(dish.getPrice()); // 快照 oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); dishMapper.decreaseStock(dish.getId(), item.getQuantity()); } return order.getOrderNo(); }逻辑说明:@Transactional保证三步要么全成功要么全回滚;库存校验放在循环里逐条做,虽然效率不是最优,但对校园食堂这种单笔订单菜品数量有限的场景完全够用;generateOrderNo()用时间戳加随机数生成唯一订单号,比直接用自增 ID 安全。改单和撤单的逻辑类似,核心是判断订单状态——只有“待支付”状态才允许修改或取消,已支付的订单要走退款流程,文档里提到“一定时间范围修改和撤销订单”,实际实现就是在 Service 层加一个时间窗口判断。
4.4 菜品统计:按菜品聚合销售数据
统计模块是商家最关心的功能,文档第 4.3.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 IN (1, 2) -- 只统计已支付和已完成 AND o.create_time BETWEEN #{start} AND #{end} GROUP BY oi.dish_id, oi.dish_name ORDER BY total_qty DESC;这条 SQL 的关键在WHERE条件:只统计已支付和已完成的订单,待支付和已取消的不算销售额,否则统计结果会虚高。按dish_id和dish_name一起分组,是因为明细表里存的是快照名称,同一菜品 ID 理论上名称一致,但保险起见一起带上。导出功能一般用 EasyExcel 或 POI 把查询结果写成 Excel,文档里“表格工具类”大概率就是干这个的。
5. 避坑与常见问题:搭建和调试时最容易翻车的几个点
5.1 启动报数据库连接失败
现象:Spring Boot 启动时抛Communications link failure或Access denied。原因通常是 MySQL 8.0 的驱动类名和连接串参数没配对——8.0 要用com.mysql.cj.jdbc.Driver,连接串要带serverTimezone=Asia/Shanghai和useSSL=false。解决:检查application.yml里的url、username、password,确认数据库已建库且账号有权限。
5.2 登录后刷新页面又回到登录页
现象:登录成功,但跳转后接口返回 401。原因多半是 Session 没保持住,前端请求没带 Cookie,或者跨域配置里allowCredentials没开。解决:前端uni.request确认withCredentials为 true;后端跨域配置用allowedOriginPatterns而不是allowedOrigins("*"),并开启allowCredentials(true)。
5.3 下单时库存扣成负数
现象:并发下单后菜品库存出现负值。原因:先查库存再扣减,两步之间没有原子性,两个请求同时通过校验。解决:扣减 SQL 加条件UPDATE dish SET stock = stock - #{qty} WHERE id = #{id} AND stock >= #{qty},根据影响行数判断是否扣减成功,失败则抛异常回滚。
5.4 统计金额和订单列表对不上
现象:菜品统计的总金额比订单列表里已支付订单的总额少。原因:统计 SQL 的WHERE条件漏了某种订单状态,或者时间范围用了create_time但业务上应该按pay_time统计。解决:先明确统计口径——按支付时间还是下单时间、包含哪些状态——再统一 SQL 条件,最好在文档里写死口径,避免后续改动时两边不一致。
5.5 uni-app 端请求后端跨域被拦
现象:H5 端调接口报 CORS 错误,但 Postman 正常。原因:浏览器同源策略拦截,后端没配跨域或配置不完整。解决:后端加全局跨域配置类,允许前端开发地址;如果前端用 HBuilderX 内置服务器,也可以在manifest.json里配代理。上线后前后端同域部署就不存在这个问题。
6. 二次开发改造思路:从毕业设计到能用的系统还差哪几步
拿到这份文档和配套代码后,如果只是跑通看效果,按第 2 章的环境清单配好、导入 SQL、改数据库连接、启动前后端就差不多了。但真要拿去二次开发,有几个地方我建议优先动。
第一是支付模块。文档里写的是“线上学生一卡通支付”,毕业设计阶段大概率是模拟支付——点一下按钮就把订单状态改成已支付。真实场景需要对接学校一卡通系统的接口,或者至少做一个支付回调的幂等处理。改造时把支付逻辑抽成独立的PaymentService接口,模拟实现和真实实现分开,后面换起来不动订单主流程。
第二是订单状态机。现在状态是 0 到 3 四个值,散落在各个 Service 里用 if 判断。建议抽一个状态流转的枚举或配置表,明确“待支付只能转已支付或已取消”“已支付只能转已完成”,非法流转直接抛异常。这样后面加退款、加配送状态时不会把判断逻辑写乱。
第三是统计口径的固化。第 5 章提到的统计对不上问题,根子在口径没写死。改造时把统计 SQL 抽到独立的 Mapper 方法里,方法名带上口径,比如sumByPayTimeRange,并在注释里写清楚包含哪些状态。商家端加一个时间筛选,默认查最近七天,避免全表扫描。
第四是权限控制。现在靠拦截器判断是否登录,但没判断“这个商家能不能改那个餐厅的菜”。建议在 Service 层加数据权限校验,商家操作菜品前先校验dish.restaurant_id是否属于当前登录商家,管理员才放开全量。这一步不做,多商家场景下会出现越权改别人菜品的问题。
验证改造是否成功,我一般用三个动作:一是用两个不同商家的账号交叉操作,确认越权被拦;二是并发压一下下单接口,看库存会不会扣负;三是把统计结果和手工从订单表算出来的数对一遍,差一分钱都要查清楚。
说个自己的教训。早些年我改这类订单系统,图省事直接在 Controller 里写业务逻辑,结果后面加一个“取消订单要退库存”的需求,发现扣库存的代码散在三个地方,改漏一处就出了脏数据。从那以后我每次动订单相关代码,都强制走一遍“下单、支付、取消、再查库存”的完整链路,确认每一步的库存和状态都对得上才提交。希望这份拆解能帮你少走点弯路,把这份毕业设计真正跑起来、改下去。
本文还有配套的精品资源,点击获取