校园食堂订餐系统实战:Spring Boot+Redis防超卖与取餐码生成
简介这份资源是面向高校计算机相关专业学生与Java Web开发学习者的毕业设计完整文档主题为基于Spring Boot的校园食堂订餐系统的设计与实现可帮助读者理解如何将所学框架知识落地为真实项目适合作为毕设选题参考或课程设计模板。压缩包内共1个doc文件大小约3.32MB内容涵盖系统总体设计、功能模块划分、数据库设计、测试调试及总结展望等完整章节。系统后台采用Spring Boot轻量框架与B/S架构前端使用可跨Android、iOS、H5及小程序的uni-app开发数据库选用MySQL并借助IntelliJ IDEA与WebStorm完成编码。核心功能包括手机端菜品浏览、订单提交与限时修改撤销、校园一卡通在线支付以及菜品销售统计同时针对疫情期间食堂拥挤、外卖不健康等问题给出了技术解决思路。目前已有9246人学习下载可为读者撰写论文、搭建项目框架与梳理技术选型提供直接参考。1. 校园食堂订餐系统从排队二十分钟到扫码三分钟中午十二点下课铃一响几千人同时涌向食堂窗口前的队伍从打饭台一直甩到门口——这个场景几乎每所高校都在上演。基于 Spring Boot 的校园食堂订餐系统要解决的就是把「人到窗口才能点单」变成「人在教室就能下单、到店即取」。它的核心链路并不复杂学生端选餐下单、后厨接单出餐、档口叫号取餐背后是一套订单状态机加库存扣减逻辑。适合谁做课程设计想拿高分的学生、想练手完整业务闭环的初级开发者、以及被食堂高峰逼得想自己动手的在校团队。我见过太多人一上来就堆功能结果订单和库存对不上这份笔记按能跑通的顺序讲。2. 需求拆解与技术选型为什么是 Spring Boot 而不是别的2.1 三类角色与最小可用功能集校园食堂订餐系统看着简单角色一拆就清楚了。学生端要能浏览当日菜品、加购物车、下单支付、查看取餐号档口端要能接单、出餐、叫号、临时下架售罄菜品管理端要能维护菜品和价格、设置每日供应量、看订单流水。最小可用版本MVP只做三件事下单、扣库存、出取餐码。别急着加评价、加积分、加推荐算法那些是第二阶段的事。我一般建议第一版把「支付」做成模拟支付因为真实支付涉及资质和回调课程设计阶段没必要卡在这。订单状态机定成五个状态待支付、已支付、制作中、待取餐、已完成外加一个已取消。状态流转必须单向不能从「已完成」倒回「制作中」这是后面所有对账逻辑的地基。2.2 技术栈选型Spring Boot MyBatis-Plus Redis 的组合理由选 Spring Boot 不是因为它时髦是因为自动装配能把数据库连接、Web 容器、JSON 序列化这些样板配置压到最低。一个校园项目通常两三个人做时间有限把精力花在业务上比花在 XML 配置上划算。持久层用 MyBatis-Plus 而不是 JPA原因是食堂菜品查询有大量条件组合按窗口、按餐段、按是否售罄MyBatis-Plus 的 LambdaQueryWrapper 写起来直观分页插件也现成。Redis 在这个系统里承担两个职责一是缓存当日菜单避免每次刷新都打数据库二是做库存扣减的原子操作。食堂高峰的并发量其实不高一个档口峰值也就每秒几十次下单但库存超卖是必须防的用 Redis 的 DECR 做预扣减再异步落库比直接数据库行锁体验好。组件版本区间选它的理由Spring Boot2.7.x / 3.x自动装配省配置生态成熟MyBatis-Plus3.5.x条件查询灵活分页插件开箱即用MySQL8.0事务可靠校园项目够用Redis6.x / 7.x缓存菜单 原子扣库存JWTjjwt 0.11.x无状态鉴权适合前后端分离2.3 数据库表设计五张核心表撑起整个系统表不用多五张就够跑通主流程用户表、菜品表、订单表、订单明细表、库存表。关键在字段设计。订单表里要有pickup_code取餐码和status状态取餐码建议用「档口编号 当日序号」生成比如 A0123学生一眼能看出去哪个窗口。订单明细表要冗余存菜品名称和单价因为菜品价格会变历史订单不能跟着变。库存表单独拆出来字段是dish_id、date、total_qty、sold_qty。为什么按日期存因为食堂菜品是每日供应的今天的红烧肉和明天的没关系。每天凌晨用定时任务初始化当天库存这个设计比在菜品表上直接挂一个库存字段要清晰得多。CREATE TABLE order_main ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 下单学生ID, pickup_code VARCHAR(16) NOT NULL COMMENT 取餐码如A0123, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3待取餐 4已完成 5已取消, total_amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_pickup_code (pickup_code) ) COMMENT订单主表;这段建表语句里pickup_code加了唯一索引防止并发下生成重复取餐码。status用 TINYINT 而不是字符串查询和索引效率都更好。total_amount用 DECIMAL 不用 FLOAT金额计算不能有精度丢失这是血泪经验。3. 核心链路实现下单、扣库存、取餐码生成3.1 下单接口从参数校验到订单落库下单是整个系统最容易出问题的地方。参数校验要做三层第一层校验用户是否登录、是否重复提交第二层校验菜品是否存在、是否在供应时段第三层校验库存是否充足。三层都过了才进扣减逻辑。重复提交用 Redis 存一个「用户ID 菜品ID」的短时令牌两秒内相同请求直接拒绝。PostMapping(/order/create) public Result createOrder(RequestBody Valid OrderCreateDTO dto, RequestHeader(Authorization) String token) { Long userId jwtUtil.getUserId(token); // 1. 防重复提交同一用户2秒内相同菜品组合只允许一次 String lockKey order:lock: userId : dto.getDishHash(); Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 2, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { return Result.fail(请勿重复提交); } // 2. 校验菜品供应状态与库存 for (OrderItemDTO item : dto.getItems()) { DishVO dish dishService.getById(item.getDishId()); if (dish null || dish.getStatus() 0) { return Result.fail(菜品已下架 item.getDishName()); } Integer stock stockService.getRemainStock(item.getDishId(), LocalDate.now()); if (stock item.getQuantity()) { return Result.fail(库存不足 item.getDishName()); } } // 3. 生成订单并扣减库存 OrderVO order orderService.createOrder(userId, dto); return Result.ok(order); }逻辑说明setIfAbsent是 Redis 的原子操作只有 key 不存在时才设置成功天然适合做防重锁。参数里的dishHash是把菜品ID和数量拼起来做哈希保证「同样的菜点两次」和「点不同的菜」能区分开。库存校验放在扣减之前但真正扣减在orderService.createOrder里用 Redis DECR 完成校验和扣减之间有时间窗口所以扣减后还要再校验一次返回值如果 DECR 后小于零就回滚。3.2 库存扣减Redis 预扣 数据库最终一致库存扣减是超卖问题的核心。直接UPDATE stock SET sold_qty sold_qty 1 WHERE sold_qty total_qty在低并发下没问题但食堂高峰期多个学生同时下单同一道菜数据库行锁会让响应变慢。我的做法是 Redis 预扣减数据库异步落库。public boolean deductStock(Long dishId, LocalDate date, int quantity) { String stockKey stock: date : dishId; // Lua脚本保证判断和扣减的原子性 String luaScript local stock redis.call(GET, KEYS[1]) if stock and tonumber(stock) tonumber(ARGV[1]) then redis.call(DECRBY, KEYS[1], ARGV[1]) return 1 else return 0 end; DefaultRedisScriptLong script new DefaultRedisScript(luaScript, Long.class); Long result redisTemplate.execute(script, Collections.singletonList(stockKey), String.valueOf(quantity)); if (result null || result 0) { return false; } // 异步落库最终一致 stockMqProducer.sendStockDeductMessage(dishId, date, quantity); return true; }Lua 脚本把「判断库存够不够」和「扣减」合成一个原子操作这是防超卖的关键。如果只用 GET 再 DECR两个请求可能同时 GET 到 1然后都 DECR库存变成 -1。异步落库用消息队列或者 Spring 的Async都行校园项目用Async加线程池就够。落库失败要有补偿我一般加一个定时任务扫 Redis 和数据库的差值差值超过阈值就告警。3.3 取餐码生成档口编号加当日序号取餐码要满足三个条件唯一、可读、能看出档口。格式定为「档口字母 四位数字」比如 A0123 表示 A 档口第 123 号。生成用 Redis 的 INCRkey 是pickup:seq:A:20240520每天重置。public String generatePickupCode(String stallCode) { String dateStr LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String seqKey pickup:seq: stallCode : dateStr; Long seq redisTemplate.opsForValue().increment(seqKey); // 设置过期时间为48小时避免key堆积 redisTemplate.expire(seqKey, 48, TimeUnit.HOURS); return stallCode String.format(%04d, seq); }increment是原子操作并发下不会生成重复序号。%04d保证数字部分固定四位取餐码长度一致叫号屏显示整齐。过期时间设 48 小时是因为当天订单可能跨夜处理但不需要永久保留。如果 Redis 重启导致序号丢失用数据库当天最大序号加一兜底这个兜底逻辑要写在生成方法里。4. 避坑与排查订单系统上线后最容易翻车的五个点4.1 库存扣了但订单没生成现象学生反馈下单失败但菜品显示已售罄。原因Redis 预扣减成功但后续订单落库抛异常没有回滚 Redis 库存。解决在订单创建的 catch 块里显式回滚 Redis 库存用 INCRBY 把扣掉的加回去。更稳妥的做法是把「扣库存」和「建订单」放在同一个本地事务里Redis 操作放在事务提交后执行但这样又失去了预扣减的意义。我的折中是扣减后立即建订单建订单失败则同步回滚同时记录一条补偿日志定时任务扫描补偿。4.2 取餐码重复导致取错餐现象两个学生拿到同一个取餐码档口叫号时两人都来取。原因Redis 的 INCR key 过期时间设置不当或者 Redis 主从切换时序号回退。解决取餐码字段加数据库唯一索引插入冲突时重新生成。另外叫号时档口端要核对订单号后四位不能只看取餐码。这个坑我在模拟项目里踩过当时没加唯一索引测试环境并发一上来就重复了。4.3 菜品缓存与库存不一致现象菜单页显示某菜品有库存点进去下单却提示售罄。原因菜单缓存了菜品信息包括库存但库存扣减后没有刷新缓存。解决菜单缓存只缓存菜品基本信息名称、价格、图片库存单独查 Redis不放进同一个缓存对象。如果非要缓存库存过期时间设短一点比如 10 秒并且扣减后主动删除缓存。4.4 订单状态流转卡在「制作中」现象学生已支付档口端也接单了但状态一直停在「制作中」无法变成「待取餐」。原因档口端出餐操作没有触发状态更新或者更新时乐观锁版本号冲突。解决状态更新用UPDATE order_main SET status ? WHERE id ? AND status ?把当前状态作为条件更新影响行数为 0 就说明状态已被其他操作改变需要重新查询再判断。档口端出餐按钮要做防抖避免重复点击。4.5 高峰时段数据库连接池被打满现象中午十二点系统响应变慢日志里大量「获取数据库连接超时」。原因下单接口里做了太多同步数据库操作连接被长时间占用。解决把非核心操作异步化比如订单日志记录、库存落库、通知推送都走异步线程池。连接池参数也要调HikariCP 的maximumPoolSize默认 10校园项目调到 20 到 30 比较合适但不要超过数据库最大连接数。监控上要看连接等待时间超过 100ms 就要警惕。5. 压测验证与一个让取餐码更抗并发的技巧系统能不能扛住中午高峰不能靠感觉要压测。我用 JMeter 模拟 200 个用户同时下单持续 60 秒观察三个指标下单成功率、平均响应时间、库存是否超卖。第一次压测大概率会翻车响应时间飙到两三秒这时候看日志定位瓶颈通常是数据库连接或者 Redis 热点 key。压测脚本里要注意每个线程用不同的用户 token否则防重锁会把请求全挡掉。库存校验用「下单成功数 剩余库存 初始库存」来验证不等就说明有超卖或漏扣。我一般跑三轮第一轮找瓶颈第二轮调参后验证第三轮加异常场景比如 Redis 超时看降级逻辑。# JMeter命令行压测示例200并发持续60秒 jmeter -n -t order_stress.jmx -l result.jtl -Jthreads200 -Jduration60 # 压测后统计成功率 awk -F, NR1 {total; if($8true) success} END {print 成功率:, success/total*100%} result.jtl这个命令里-n表示非 GUI 模式-J传参覆盖脚本里的变量。压测结果文件用 awk 统计成功率比在 JMeter 界面里看更灵活。注意压测环境要和生产环境配置接近否则数据没参考价值。最后一个技巧取餐码的序号生成如果 Redis 挂了怎么办我的做法是降级到数据库号段模式。在数据库建一张pickup_seq表每次取一批号段比如 100 个缓存在本地内存用完再取下一段。这样即使 Redis 不可用取餐码生成也不受影响只是序号可能不连续但取餐码本来就不要求连续。这个降级逻辑平时不触发但关键时刻能救命。做这类校园系统我的习惯是先把状态机和库存扣减写对再堆功能。功能少但数据对得上比功能多但订单乱套要强得多。希望帮到你。本文还有配套的精品资源点击获取