从静态页面原型一路走向可答辩、可演示的完整项目这个过程比想象中要琐碎得多。这篇博文我会把自己做这套早餐预订平台的设计思路、技术选型、数据库建模、核心订单流程的实现细节以及调试阶段踩过的坑按一线开发的逻辑完整梳理一遍。如果你也正在做Spring Boot相关的Web或App毕业设计这篇文章应该能帮你省下不少瞎折腾的时间。1. 这个选题背后的考察逻辑为什么是“预订”而不是“外卖”很多同学拿到“早餐预订平台”这个题目第一反应是把它做成一个简化版外卖App这其实是最大的误区。早餐预订和外卖虽然表面相似但核心业务模型有本质区别搞清楚这个区别系统设计的格局就完全不一样了。1.1 早餐预订的核心业务特征早餐预订最大的特点是强时间约束和计划性消费。用户不是下单后等30分钟配送而是在前一天晚上或者当天早上提前选好第二天的早餐约定一个取餐时间商家按预估数量备餐。这意味着系统里天然存在“预订日期”和“备餐时间窗”这两个外卖平台没有的维度。另一个特征是履约密度高但单品逻辑简单。早餐品类相对固定——豆浆、包子、煎饼、粥SKU不会像正餐外卖那么复杂但往往有“售完即止”的时段库存概念。比如某个店每天7:30到8:00只能出40份煎饼订完就没了。这跟酒店订房、电影选座的库存模型非常相似。1.2 从毕业设计评审角度看这个题目站在毕业设计评审老师的角度来看这个题目有几个很加分的点。首先是业务闭环完整从用户注册、浏览、下单、支付、商户接单到订单状态流转整条链路覆盖了Web开发的大多数基本功。其次是技术点有深度时段时间库存、防止超卖、订单状态机这些都是能在答辩时讲清楚的高质量素材。第三是数据模型有设计感涉及用户、商家、商品、时段库存、订单、明细、评价多张核心表的关联足以体现数据库设计能力。同样的思路如果放在“某商品管理系统”或者“个人博客系统”上最多就是CRUD的水平很难拉开差距。所以这个选题本身含金量不低就看怎么把业务特征转化为系统设计。1.3 和同类型题目的差异化对比我做之前也对比过几个经典题目差别很值得说说。题目类型核心模型复杂度焦点适合展示的技术点网上商城即时购买购物车商品SPU/SKU、促销Redis缓存、秒杀校园外卖即时配送骑手订单分配、定位地图服务、消息推送早餐预订计划消费时段履约时段库存、预订日期状态机、防超卖、定时任务早餐预订恰好落在中间既有商城类系统的商品管理能力又有外卖类系统的订单履约逻辑还多了“预订日期”这个时间维度带来的设计空间。答辩时如果老师问“你怎么理解这个系统的核心难点”直接回答“在有限备餐能力下实现按时段的安全售卖”就够了。2. 技术选型与项目架构这套组合是怎么定下来的2.1 后端框架为什么落定Spring Boot选Spring Boot不是因为它热门而是它在做这类单体业务系统时效率确实最高。内嵌的Tomcat让部署少了配置Web容器这一步Starter机制让集成Redis、MyBatis、JWT这些组件变成加依赖和写配置的事关键是可以把主要精力放在业务代码上而不是环境搭建上。我用的是Spring Boot 2.7.x版本搭配Java 8理由很简单网上资料最丰富遇到问题基本都能搜到答案。Java 17虽然新但有些老版本的MyBatis生成器或者插件兼容性会出问题花时间折腾不值得。2.2 App端的选择Android原生还是跨平台框架标题说的是App开发但“App”这个词可以有很多实现方式。我最终选择了Android原生Kotlin搭配MVVM架构。理由很现实毕业设计答辩现场通常需要手机演示原生App在流畅度、网络请求调试、日志查看上最直观遇到问题自己能完全掌控。如果你时间紧张也可以考虑uni-app或者Flutter一套代码可以同时生成Android和iOS界面开发效率更高。但要注意的是用跨平台框架就要额外处理原生插件的兼容问题比如推送服务、文件选择器调试链路比原生长。我更推荐时间充裕的选原生时间紧的用uni-app。2.3 整体架构与依赖组件系统整体是标准的前后端分离结构App端只做界面展示和交互所有业务逻辑都在后端接口中完成。这种方式的优点是接口可以复用于后续的Web管理端也方便答辩时直接在浏览器中演示商家端功能。我的依赖选型和理由如下Spring Boot Starter Web提供RESTful接口能力MyBatis-Plus比起纯MyBatisCRUD操作方便很多分页查询直接用内置插件MySQL 8.0存储业务数据Redis承接时段库存预扣减、短信验证码、Token缓存JWT无状态登录认证Hutool工具类库生成订单号、Bean复制都很方便Swagger/Knife4j自动生成接口文档答辩演示神器另外文件存储我用的是本地Nginx代理静态资源路径避免引入OSS依赖配置的复杂度在本地环境表现已经很稳定。3. 需求分析与功能模块拆解先画清业务边界再写代码3.1 三类用户角色的权限边界早餐预订平台表面上是一个App但完整使用时至少要覆盖三类角色C端用户、商家、平台管理员。我一开始犯过的错误是想把商家功能也塞进同一个App后来发现重得没法维护果断改成商家用独立的管理端独立前端项目App只服务消费者后台管理员用浏览器访问。用户端的功能边界注册登录手机号验证码浏览附近早餐店铺和按时间段展示的可预订餐品创建预订订单、在线支付真实支付或模拟支付查看历史订单、取消预订对已完成订单进行评价商家端的功能边界店铺基础信息维护营业时间、公告餐品上下架、库存量设置按日期和时间窗查看预订订单点击完成备餐查看每日营收统计平台端就比较轻量主要是用户封禁、商家入驻审核和基础数据统计。3.2 时间窗模型把备餐能力切成可卖的时段这个设计是整个系统最核心的点。早餐备餐是分批次的7:00-7:30一批7:30-8:00一批每批次有最大产出量。所以餐品数据不能只是一个库存数字而是和时间关联的一组库存数。我设计了一个独立的“库存时段表”一个餐品对应多条时段记录商品名称鲜肉包子 日期2025-05-20 时段07:00-07:30 余量25 总量30 时段07:30-08:00 余量30 总量30 日期2025-05-21 时段07:30-08:00 余量12 总量20用户端的点餐列表就是按这个模型查询的先选日期和取餐时段然后看该时段下可预订的餐品。这个方式把一个模糊的“早餐店”问题变成了清晰的“库存时间”问题。3.3 用户端核心操作流梳理我把用户最常用的流程画成了一条线代码实现就围绕这条线展开浏览店铺列表 → 进入店铺选择日期/时段 → 选择餐品加入预订单 → 提交订单 → 支付 → 收到取餐凭证 → 线下到店取餐 → 订单完成 → 评价这里有个细节是订单和支付的关系。我做了“先创建待支付订单、再支付、支付回调后把订单改成已支付”的三段式流程。这样做的好处是下单后有一个支付缓冲时间支付超时后系统自动取消订单释放库存避免用户下了单又流失导致的库存浪费。4. 数据库设计与核心表关系一张订单明细表撑起所有关键逻辑4.1 核心数据表清单与字段设计数据库我设计了10张核心表真正长期使用且业务依赖强的是下面这些-- 店铺表 CREATE TABLE shop ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_name VARCHAR(50) NOT NULL, address VARCHAR(200), phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 0关闭 1营业, open_time VARCHAR(10), close_time VARCHAR(10) ); -- 餐品表 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_id BIGINT NOT NULL, product_name VARCHAR(50) NOT NULL, description VARCHAR(255), price DECIMAL(10,2) NOT NULL, image_url VARCHAR(255), status TINYINT DEFAULT 1 COMMENT 0下架 1上架 ); -- 时段库存表关键表 CREATE TABLE stock_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, stock_date DATE NOT NULL, start_time VARCHAR(5) NOT NULL, end_time VARCHAR(5) NOT NULL, total_stock INT NOT NULL, remain_stock INT NOT NULL, UNIQUE KEY uk_product_date_slot (product_id, stock_date, start_time, end_time) );时段库存表用了联合唯一索引防止同一个日期的同一时段被重复录入两次。订单主表和明细表是最重要的-- 订单主表 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, shop_id BIGINT NOT NULL, order_date DATE NOT NULL, pickup_slot VARCHAR(20) NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL COMMENT 0待支付 1已支付 2备餐中 3待取餐 4已完成 5已取消, pay_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单明细表 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name VARCHAR(50), price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, slot_id BIGINT NOT NULL COMMENT 关联时段库存表 );明细表中冗余了product_name和price虽然稍微违背设计范式但这样做的收益是订单历史不受商品改价、改名影响每次查询都不用再去关联商品表。4.2 库存扣减的事务设计这是整个系统最容易出bug的地方。用户提交订单时后端要做的事情是查询该日期时段库存是否充足扣减对应时段库余量创建订单主表和明细表数据这三个操作必须放在一个数据库事务里同时还要避免并发问题。我的方案是先用乐观锁更新库存Update(UPDATE stock_slot SET remain_stock remain_stock - #{quantity} WHERE id #{slotId} AND remain_stock #{quantity}) int deductStock(Param(slotId) Long slotId, Param(quantity) Integer quantity);如果这个update返回的行数为0说明库存不足或者已被并发扣完直接抛出业务异常阻止下单。这种“CAS式”扣减是防止超卖的最简单有效手段不需要引入Redis分布式锁也能在单机部署下保证安全。4.3 定时任务清理过期订单用户创建了待支付订单后如果一直不支付库存一直占着会影响其他用户购买。我增加了一个Spring定时任务每两分钟扫描一次订单表把创建超过15分钟且仍处于待支付状态的订单自动取消同时回补库存。Scheduled(fixedDelay 120000) public void autoCancelExpiredOrders() { // 查出待支付且创建时间早于当前时间15分钟的订单 // 逐条执行更新订单状态为已取消 释放库存 }这个逻辑虽然简单但在答辩时属于非常高频的加分点因为很多同学的演示项目里都没有处理“支付超时”这个现实问题。5. 核心接口设计与后端实现从用户登录到订单状态机5.1 统一返回结构提前省掉无休止的联调纠纷App端和后端对接时最烦的就是各接口返回格式不统一前端得为每一种格式写特判。我一开始就定下一个统一的返回包装类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }配合全局异常处理器所有业务异常和系统异常都能被转换成统一结构返回App端只需解析code判断结果逻辑清爽很多。5.2 下单接口的完整链路这是用户端的核心接口我拆成几个清晰的Service方法Transactional(rollbackFor Exception.class) public OrderVO createOrder(CreateOrderDTO dto) { // 1. 校验店铺营业状态 Shop shop shopMapper.selectById(dto.getShopId()); // 2. 遍历DTO中的餐品列表查询库存并累加总价 // 3. 循环调用库存扣减乐观锁 // 4. 生成订单号 插入订单主表 // 5. 插入订单明细表 // 6. 返回订单号和应付金额 }一个容易被忽略的细节是订单号的生成。我用了时间戳 用户ID后四位 随机数字的方式保证唯一性的同时方便从订单号反查用户信息不依赖数据库自增ID。5.3 支付处理模拟支付和真实支付的两条路径毕业设计的场景下接入真实支付渠道往往需要企业资质不值得花费精力。我提供了模拟支付通道用户点击“支付”后后端直接把订单状态更新为已支付同时扣减库存状态从“预占”转为“实际扣减”。这里有一个我实际踩过的坑。最初的实现里下单时就扣减库存支付失败订单取消时再回补逻辑绕且容易出现库存扣了但订单没生成。改成“下单时扣减库存取消或超时时回补”后代码清晰很多异常链路也好处理。5.4 订单状态机的流转边界订单状态我用一张离职面图式的状态枚举来约束防止出现用户能直接取消“已完成”订单这种逻辑漏洞待支付(0) → 已支付(1) → 备餐中(2) → 待取餐(3) → 已完成(4) 待支付(0) → 已取消(5) 已支付(1) → 已取消(5)仅限商家取消状态机的核心意义在于把每个状态下允许的操作固定下来。比如用户端取消订单只能发生在待支付阶段商家确认备餐只能从“已支付”转移到“备餐中”。用一张状态转换表做校验就能杜绝非法状态跃迁。6. App端实现细节与前后端联调界面之外更重要的几件事6.1 Android端网络层设计Android端我用了Retrofit2 OkHttp Kotlin协程的组合。Retrofit负责接口定义和JSON转换OkHttp拦截器负责携带Token和统一打印日志协程负责异步线程切换。Token的存取是重点。用户在登录成功后后端返回一个JWTApp把它存到SharedPreferences每次请求通过OkHttp的Interceptor自动添加到请求头Authorization字段。对于401响应全局拦截后跳转回登录界面并清空本地Token。6.2 订单状态轮询与取餐通知App端“我的订单”页面需要实时反映订单状态变化。我最初尝试过WebSocket但在毕业设计这种体量下引入WebSocket会让后端代码复杂度明显上升。最终采用了一个很务实的方案订单详情页在回显数据后启动一个定时器每10秒钟调用一次订单详情接口刷新状态页面销毁时取消定时器。既不阻塞用户操作又能把最新的备餐进度同步给用户。这里也顺便处理了用户最关心的“取餐凭证”用户到店后在订单详情页点击“出示取餐码”App从后端接口获取一个当天有效的取餐码随机数字串商家输入后完成核销。这个闭环在人手量不大的毕业设计项目里完全可以承担。6.3 前后端联调的常见障碍联调阶段遇到最多的问题是时间格式。LocalDateTime在Jackson序列化后默认是一串很长的ISO字符串Android端Gson解析经常报错。我的解决办法是在后端application.yml里统一配置全局时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8另一个高频问题是大文件图片上传。Android端上传图片时需要把位图压缩到合理大小再以multipart格式提交否则后端默认的1MB限流就报错了。我配置了Spring的文件上传大小上限并且在实际使用时建议前端先压缩图片再上传spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB7. 系统优化与压力测试怎么让项目从“能跑”变成“能打”7.1 接口响应慢的排查与优化系统开发完成后我用JMeter做了一轮简单的压力测试发现接口平均响应时间在500ms左右主要是SQL执行和序列化两个环节拖慢了速度。针对性能瓶颈做了三项优化第一列表查询接口中的关联查询改为一次性查询后内存组装。例如店铺列表需要带出当日可预订的餐品数量和起订价原本是循环查询每间店铺耗时随店铺数线性增长。改为一次性查出该区域内所有店铺的餐品统计后接口耗时从800ms降到120ms。第二高频只读数据加Redis缓存。时段库存数据查询非常频繁但变更是低频操作按“日期店铺”维度缓存对应的可售餐品列表缓存过期时间设为60秒基本能减少80%的数据库压力。第三热点接口SQL加索引。订单详情查询最常用到order_no字段时段库存查询最常用到product_id stock_date的联合条件全部建了索引后所有查询都走了索引扫描。7.2 防超卖场景的压力验证我用JMeter的并发线程组模拟了20个用户同时抢购同一时段同一餐品的场景每个用户下1单。在乐观锁方案下最终成功下单数严格等于库存数比如库存10成功的就是10个剩余11个用户全部提示库存不足没有出现超卖。这个测试结论对答辩特别有价值因为它用数据证明了自己设计的可靠性。7.3 项目部署与演示环境准备毕设答辩一般要求在指定环境下演示最稳妥的方式是打包后在本地起一个Spring Boot Jar包加上Android模拟器。我把前端资源打包进了Spring Boot的静态资源目录这样商家管理端和用户App都只需要访问一个服务即可端口统一演示环境搭建只需要安装JDK和MySQL。需要特别提醒的是有些答辩场地没有外网所以所有依赖Maven仓库缓存、Android依赖、甚至数据库初始化脚本都要提前在本地准备好一份离线副本否则演示时遇到环境缺失会非常尴尬。8. 后期扩展方向与个人经验总结8.1 可以继续深入的方向做完这套系统之后我整理了几个有潜力的扩展点后续如果想把它做成更完整的项目可以按这个路径走。推荐系统的引入就是其中一个方向目前的餐品列表只是按店铺和时段展示没有考虑用户偏好可以考虑接入简单的协同过滤算法分析用户历史订单数据做“猜你喜欢”推荐这在答辩时会是很好的研究亮点。消息推送方面目前订单状态变化依赖轮询拉取体验一般。可以考虑接入第三方推送通道把备餐完成、订单取消等关键事件主动推送到用户手机这也是真实商业平台的标准能力。再往下做可以将数据库升级为分布式架构或引入分库分表但毕设阶段建议点到即止把原理讲清楚比强行上生产级方案更有价值。8.2 踩过坑之后的几条实在建议最后根据自己的实际开发体验给正在做类似项目的同学几条提醒。项目从第一天开始就要用Git管理代码不要到最后答辩前才建立仓库一来是防止误删除和错误改动无法回退二来是简历上展示Git提交记录时连续的提交时间线比一次性上传几万字代码更真实可信。编写代码时一定要统一命名规范包名、类名、字段名不要中英混合也不要随意缩写答辩演示和后续评审对代码质量的关注度不低。关键模块记得要写注释解释清楚“为什么这么设计”比解释“这段代码做了什么”更能体现工程素养。核心问题最好整理进README文件包括系统架构图、数据库设计说明、接口清单和部署步骤答辩前自己多看几遍老师提问时就能从容很多。这个项目从无到有做完我最大的感受是毕设选题的价值不在于堆砌热门技术而在于能不能把一个真实业务场景用合理的工程手段落地出来。把“早餐预订平台”这件事想明白、做完整、讲清楚就是一份合格的答卷了。