ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

校园线上订餐系统Java后端实战:Spring Boot+MyBatis+Redis扛住午高峰

校园线上订餐系统Java后端实战:Spring Boot+MyBatis+Redis扛住午高峰 简介本资源为基于Java的校园线上订餐系统毕业设计论文文档面向软件工程、计算机相关专业的本科毕业生及需要课程设计参考的学生帮助解决订餐类系统选题、功能规划与论文撰写等实际问题。压缩包内共1个docx文件约788KB内容为完整的毕业设计论文涵盖摘要、目录及各章节正文围绕收货地址管理、菜品管理、菜品收藏与评价、订单管理、购物车管理、字典管理、用户与管理员管理等模块展开并采用MySQL作为数据库平台。目前已有72人学习下载。读者可从中获取完整的选题背景、需求分析、功能设计思路与数据库选型依据了解Java后端与前端交互的整体实现逻辑同时参考论文的章节结构与写作规范为自身毕业设计或课程项目提供可借鉴的框架与素材。1. 校园线上订餐系统从课程设计到能扛住午高峰的 Java 后端每年毕业季基于 Java 的校园线上订餐系统设计与实现都会被大量同学选中理由很实在业务闭环清晰、技术栈主流、答辩时演示效果直观。但真正动手就会发现能跑通和能用是两回事。中午 12 点几千人同时点单库存超卖、订单重复、支付回调丢失这些问题会集中爆发。这个标题背后其实是一套完整的 Java 后端工程训练Spring Boot 做 Web 层MyBatis 管数据访问MySQL 存业务数据Redis 扛热点消息队列削峰。它适合正在做课程设计的学生也适合想补一个完整项目经验的 Java 入门者。接下来我会按真实落地顺序把选型、建表、核心接口、并发处理和踩坑记录拆开讲代码可以直接抄改。2. 技术选型与工程骨架为什么是 Spring Boot MyBatis 而不是别的2.1 校园订餐场景对技术栈的真实约束校园订餐和外卖平台有本质区别。用户群体固定、菜品数量有限、营业时间集中、支付方式单一多数走校园卡或模拟支付。这意味着不需要微服务拆分单体应用足够不需要分库分表单表百万级数据 MySQL 完全扛得住但必须处理午晚高峰的瞬时并发这是核心矛盾。选 Spring Boot 的理由很直接自动配置省去大量 XML内嵌 Tomcat 让部署变成一条命令生态里 Redis、RabbitMQ、MyBatis 的 starter 都成熟。选 MyBatis 而不是 JPA是因为订餐系统里多表关联查询多订单、订单明细、菜品、窗口复杂 SQL 用 XML 写更可控也方便做行级权限过滤——比如只让商家看到自己窗口的订单。前端可以前后端分离Vue 或 React 都行本文聚焦后端。一个常见的误区是上来就堆技术。我见过有同学在课程设计里塞进 Nacos、Gateway、Sentinel结果答辩时连服务注册都没跑通。校园订餐系统的技术深度应该体现在并发控制和数据一致性上而不是组件数量。2.2 用 Spring Initializr 搭出可运行骨架第一步不是写业务是把骨架跑起来。用 Spring Initializr 或 IDE 新建项目依赖勾选 Spring Web、MyBatis Framework、MySQL Driver、Spring Data Redis、Lombok。生成后先确认能启动。!-- pom.xml 关键依赖版本按 Spring Boot 父工程管理 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依赖说明mybatis-spring-boot-starter 的版本要和 Spring Boot 版本匹配3.x 对应 Spring Boot 3.x2.x 对应 2.x版本错配会报 NoSuchMethodError。MySQL 驱动从 8.x 开始包名是 com.mysql.cj.jdbc.Driver老教程里的 com.mysql.jdbc.Driver 已经废弃。配置文件里把数据源和 Redis 写清楚# application.yml spring: datasource: url: jdbc:mysql://localhost:3306/campus_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver data: redis: host: localhost port: 6379 database: 0 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case 打开后数据库的 create_time 会自动映射到 Java 的 createTime省掉大量 resultMap。serverTimezone 必须设否则 MySQL 8 会报时区错误这是新手最常见的启动失败原因之一。2.3 分层结构与包命名规范包结构建议按功能分而不是按层分。按层分controller、service、dao 各一个包在业务变多后会失控。按功能分com.campus.order ├── config // Redis、MyBatis、拦截器配置 ├── common // 统一返回体、异常、常量 ├── user // 用户模块 ├── merchant // 商家/窗口模块 ├── dish // 菜品模块 ├── order // 订单模块 └── pay // 支付模块每个模块内部再分 controller、service、mapper。这样改订单逻辑时只动 order 包符合面向对象编程的高内聚原则。统一返回体用泛型封装 code、msg、data前端处理起来一致。3. 数据库设计与核心表订单表怎么建才不埋雷3.1 六张核心表与字段取舍校园订餐系统最少需要六张表用户表、商家窗口表、菜品表、订单主表、订单明细表、支付流水表。字段设计直接决定后面并发处理难不难。表名关键字段说明userid, student_no, nickname, phone, rolerole 区分学生/商家/管理员merchantid, name, location, statusstatus 控制窗口是否营业dishid, merchant_id, name, price, stock, versionversion 做乐观锁ordersid, order_no, user_id, merchant_id, total_amount, status, create_timeorder_no 唯一索引order_itemid, order_id, dish_id, dish_name, price, quantity冗余菜品名和价格pay_recordid, order_no, pay_no, amount, status, pay_timepay_no 唯一订单明细表里冗余 dish_name 和 price 是故意的。菜品可能改价或下架但历史订单必须显示下单时的信息这是血泪经验。很多同学只存 dish_id结果菜品一改价历史订单金额全乱了。3.2 建表 SQL 与索引设计CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status), KEY idx_merchant_create (merchant_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;索引说明uk_order_no 保证订单号唯一防止重复提交产生两笔订单。idx_user_status 支撑「我的订单」按状态查询。idx_merchant_create 支撑商家按时间拉取订单列表。金额用 DECIMAL 不用 FLOAT浮点数算钱会出现 0.10.20.30000000000000004 这种问题对账时是灾难。菜品表的库存扣减字段CREATE TABLE dish ( id BIGINT NOT NULL AUTO_INCREMENT, merchant_id BIGINT NOT NULL, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_merchant (merchant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;version 字段是乐观锁用的后面并发扣库存会讲。3.3 订单号生成策略订单号不能用自增 id会暴露业务量也不方便分库。常见做法是「时间戳 用户 id 后四位 随机数」或者用雪花算法。校园项目用前者足够public static String generateOrderNo(Long userId) { String time LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)); String suffix String.format(%04d, userId % 10000); String random String.format(%04d, ThreadLocalRandom.current().nextInt(10000)); return time suffix random; }逻辑说明时间戳精确到秒同一用户同一秒内下单概率极低再加四位随机数基本不会碰撞。数据库还有唯一索引兜底插入冲突时捕获异常重试即可。参数上如果并发量再大把随机数位数加到 6 位或者引入 Redis 的 INCR 生成序列。4. 下单与支付核心链路并发扣库存和防重复提交4.1 下单接口的完整流程下单不是简单 insert。完整流程是校验用户和菜品 → 校验库存 → 扣库存 → 创建订单主表 → 创建订单明细 → 返回订单号。任何一步失败都要回滚。Service public class OrderService { Autowired private DishMapper dishMapper; Autowired private OrderMapper orderMapper; Autowired private OrderItemMapper orderItemMapper; Transactional(rollbackFor Exception.class) public String createOrder(Long userId, Long merchantId, ListOrderItemDTO items) { BigDecimal total BigDecimal.ZERO; // 1. 逐个扣库存乐观锁 for (OrderItemDTO item : items) { int affected dishMapper.deductStock(item.getDishId(), item.getQuantity()); if (affected 0) { throw new BizException(菜品[ item.getDishId() ]库存不足); } Dish dish dishMapper.selectById(item.getDishId()); total total.add(dish.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 2. 创建订单 String orderNo OrderNoUtil.generate(userId); Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setMerchantId(merchantId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 3. 创建明细 for (OrderItemDTO item : items) { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setDishId(item.getDishId()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); } return orderNo; } }Transactional 的 rollbackFor 必须写 Exception.class默认只回滚 RuntimeException受检异常不会回滚这是经典翻车点。4.2 乐观锁扣库存的 SQL 写法扣库存的 mapper 方法update iddeductStock UPDATE dish SET stock stock - #{quantity}, version version 1 WHERE id #{dishId} AND stock #{quantity} AND version #{version} /update逻辑说明WHERE 里 stock quantity 保证不会扣成负数version 条件保证并发时只有一个线程能更新成功。返回的 affected rows 为 0 就说明库存不足或有并发冲突。参数说明dishId 是菜品主键quantity 是购买数量。如果业务允许超卖一点点比如食堂备餐有余量可以去掉 version 条件只靠 stock quantity这样并发性能更好但可能少量超卖。4.3 防重复提交的三种手段用户手抖连点两次提交会产生两笔订单。三种防护前端按钮置灰只能防君子。后端用 Redis 做幂等下单前用 userId 菜品组合生成 keysetnx 设置 5 秒过期设置失败直接返回「请勿重复提交」。更可靠的是数据库唯一索引订单号唯一重复插入会抛异常。最彻底的是支付回调时用 pay_no 唯一索引同一笔支付只处理一次。public boolean tryLock(Long userId, String bizKey) { String key order:lock: userId : bizKey; Boolean ok redisTemplate.opsForValue() .setIfAbsent(key, 1, 5, TimeUnit.SECONDS); return Boolean.TRUE.equals(ok); }参数说明5 秒是经验值够用户完成一次提交又不会锁太久影响正常重试。bizKey 可以用菜品 id 排序后拼接保证同一批菜品生成同一个 key。4.4 支付回调与数据一致性校园项目通常模拟支付但回调逻辑要按真实写。支付成功后更新订单状态为已支付 → 写支付流水 → 发消息通知商家。这三步要保证最终一致。Transactional(rollbackFor Exception.class) public void handlePayCallback(String orderNo, String payNo, BigDecimal amount) { // 幂等pay_no 唯一索引重复回调会抛异常 PayRecord record new PayRecord(); record.setOrderNo(orderNo); record.setPayNo(payNo); record.setAmount(amount); record.setStatus(1); payRecordMapper.insert(record); int updated orderMapper.updateStatus(orderNo, 0, 1); if (updated 0) { throw new BizException(订单状态异常可能已处理); } // 发消息失败不影响主流程 try { rabbitTemplate.convertAndSend(order.paid, orderNo); } catch (Exception e) { log.error(通知商家失败orderNo{}, orderNo, e); } }逻辑说明先插支付流水靠唯一索引挡住重复回调。再更新订单状态用「原状态0」作为条件防止已取消的订单被支付。消息发送失败只记日志因为订单状态已经正确商家可以主动刷新这就是最终一致性。参数上updateStatus 的旧状态条件很关键少了它会出现「取消后又被支付」的脏数据。5. 避坑与排查那些让答辩翻车的细节5.1 库存扣了但订单没创建现象菜品库存少了但订单列表里没有对应订单。原因扣库存和创建订单在同一个事务里但扣库存用的是 MyBatis 的 update如果后面抛异常事务应该回滚。翻车通常是 Transactional 没生效——比如方法被同类内部调用或者异常被 catch 了没重新抛出。解决确认注解在 public 方法上异常要 throw 出去或者用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记回滚。5.2 订单号重复导致插入失败现象高并发下偶尔报 Duplicate entry for key uk_order_no。原因时间戳精确到秒同一用户同一秒内多次下单随机数碰撞。解决把随机数位数从 4 位加到 6 位或者用 Redis INCR 生成全局序列。更简单的办法是捕获 DuplicateKeyException 后重新生成订单号重试一次。5.3 Redis 连接超时导致下单失败现象午高峰下单接口大量超时日志显示 Redis command timed out。原因防重复提交的 Redis 操作没有设超时或者 Redis 和 MySQL 在同一个事务里事务持有连接时间过长。解决Redis 操作不要放进数据库事务先做幂等检查再开事务。给 Redis 客户端设置合理的 timeout比如 200ms超时就走降级逻辑直接放行靠数据库唯一索引兜底。5.4 商家看到的订单列表越来越慢现象商家后台加载订单列表从 200ms 涨到 3 秒。原因orders 表数据量到几十万后idx_merchant_create 索引没覆盖查询字段回表次数多。解决查询只取必要字段或者建覆盖索引 (merchant_id, create_time, status, total_amount)。另外分页要用游标分页where create_time 上一页最后时间而不是 limit offsetoffset 大了会全表扫描。5.5 时区问题导致订单时间差 8 小时现象数据库里 create_time 是对的但接口返回给前端少了 8 小时。原因MySQL 连接串没设 serverTimezone或者 Jackson 序列化时用了 UTC。解决连接串加 serverTimezoneAsia/Shanghai实体类时间字段加 JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。这个坑在答辩演示时特别致命老师一看时间不对就会追问。6. 压测验证与进阶技巧让系统在午高峰不崩写完代码不算完得验证。用 JMeter 或 wrk 对下单接口压测模拟 500 并发、持续 60 秒。重点看三个指标TPS、错误率、库存是否扣成负数。我一般会先跑一轮不加锁的版本看超卖多少再加乐观锁对比这样能直观感受到并发控制的价值。压测时把日志级别调到 WARN否则大量 INFO 日志会拖慢系统。数据库连接池 HikariCP 的 maximum-pool-size 设成 CPU 核数乘 2 再加磁盘数校园项目 20 就够。Redis 连接池 max-active 设 50。一个进阶技巧是用 Redis 预扣库存。把菜品库存加载到 Redis下单时用 Lua 脚本原子扣减扣成功再异步落库。这样数据库压力骤降但要注意 Redis 和 MySQL 的最终一致——定时任务对账发现 Redis 库存和数据库不一致就以数据库为准修正。Lua 脚本-- KEYS[1] dish:stock:{dishId} -- ARGV[1] quantity local stock tonumber(redis.call(get, KEYS[1]) or -1) if stock 0 then return -1 end if stock tonumber(ARGV[1]) then return 0 end redis.call(decrby, KEYS[1], ARGV[1]) return 1返回 -1 表示库存未预热0 表示不足1 表示扣减成功。这个脚本在 Redis 单线程里执行天然原子。验证数据一致性还有个笨办法但很有效写个对账 job每天凌晨跑一次比对订单明细的总销量和菜品表的扣减量不一致就告警。我吃过亏有次 Redis 重启丢了库存数据靠这个对账才发现。最后说个习惯所有涉及钱的接口入参和出参都打日志但金额字段要脱敏。订单状态变更必须记录操作人和时间。这些在课程设计里可能不加分但工作后是基本功。希望帮到你。本文还有配套的精品资源点击获取
返回列表