Spring Boot民宿预定系统设计与实现:订单并发控制与状态机实战
发布时间:2026/9/11 21:37:01来源:尧图网络
简介一套面向毕业设计与课程作业的民宿在线预定平台完整项目基于Spring Boot构建采用前后端分离架构适合Java学习者进行项目实战与论文支撑。资源共780个文件、约17.62MB主要包含122个Java后端源码、46个Vue前端页面、153个JavaScript脚本、44个CSS样式、16个XML配置以及MySQL数据库脚本、Maven配置和一键启动/运行脚本目录结构清晰便于导入IDE运行和二次开发。已有85人学习下载。功能覆盖游客注册登录、按目的地与日期搜索筛选、民宿列表与地图展示、房型选择、在线支付、订单管理、收藏评价同时提供民宿业主端房源管理与订单处理、管理员后台权限控制。通过该项目可熟悉Spring Boot整合MyBatis/JPA、RESTful接口开发、RBAC权限设计、第三方支付对接及Vue前端交互。压缩包内还附带毕业论文相关文档、数据库初始化SQL和批量构建脚本便于快速完成演示答辩或作为课题支撑材料。1. 民宿在线预定系统难的不是 CRUD 而是订单与库存的博弈任何一个做过酒店或民宿后台的工程师都会有同感这类系统的 Controller 和 Service 写起来并不难无非是房源、房型、订单、用户几个模块的增删改查。真正让人头疼的是两个藏在业务深处的问题——同一间房在同一日期被两个人同时下单时该让谁成功以及订单从“已支付”到“已入住”再到“已退房”的流转如何在前端按钮、后台接口和数据库状态三者之间保持一致。Spring Boot 之所以成为这类毕业设计和中小型项目的主流选择不只是因为起步快更重要的是它自带依赖管理、内嵌 Tomcat、统一的配置体系和成熟的 Starter 生态让开发者能把精力集中在业务模型上而不是一遍遍搭框架骨架。这篇内容会从数据模型说起一路走到接口设计、并发控制和部署验证。适合正在做 Spring Boot 方向毕业设计的学生也适合刚接手类似业务系统、想看看别人怎么设计订单状态机的在职开发。你不需要事先掌握微服务或分布式中间件MySQL 加 Spring Boot 加 Maven 就足以跑通整条链路。2. 民宿平台的领域模型与数据库表设计2.1 一张 ER 图如何决定后期开发效率民宿在线预定平台的业务域可以拆成五个核心实体用户、房源、房型、订单、评价。很多初学者一上来就想做“用户-订单-房源”三张表等做到价格日历和房态管理时会发现表结构根本撑不住需求只能返工。建议在建表之前先把 ER 图按如下方向理清。用户表与订单表是一对多房源表与房型表是一对多房型表与订单表是一对多评价表与订单表是一对一。价格和库存不要直接挂在房型表上因为民宿的价格往往随日期浮动同一间房在不同日期、不同节假日价格不一致。正确做法是单独建一张库存价格表以“房型 ID 日期”作为唯一键同时记录总房间数、已售数量和当日价格。-- 房型库存价格表核心表 CREATE TABLE room_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, house_id BIGINT NOT NULL COMMENT 房源ID, room_type_id BIGINT NOT NULL COMMENT 房型ID, stock_date DATE NOT NULL COMMENT 日期, total_count INT NOT NULL DEFAULT 1 COMMENT 房间总数, sold_count INT NOT NULL DEFAULT 0 COMMENT 已售数量, price DECIMAL(10,2) NOT NULL COMMENT 当日单价, UNIQUE KEY uk_room_date (room_type_id, stock_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 DDL 的核心在于UNIQUE KEY uk_room_date (room_type_id, stock_date)它保证了同一房型在同一天只有一条库存记录。sold_count字段是后续处理并发下单的突破口。订单表在创建时不要直接写“剩余房间数”而是通过 update 语句去扣减库存只有受影响行数为 1 时才允许创建订单。这个思路后面会详细展开。2.2 Spring Boot 项目中的表结构落地与 JPA 映射实际创建 Spring Boot 项目时建议使用spring-boot-starter-data-jpa加 MySQL 驱动因为 JPA 的实体映射能直接对应 ER 图中的关系减少手写 SQL 的重复劳动。对于毕业设计这种量级JPA 的懒加载和级联操作足够用不必引入 MyBatis-Plus。Entity Table(name room_stock) public class RoomStock { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name house_id, nullable false) private Long houseId; Column(name room_type_id, nullable false) private Long roomTypeId; Column(name stock_date, nullable false) private LocalDate stockDate; Column(name total_count, nullable false) private Integer totalCount; Column(name sold_count, nullable false) private Integer soldCount; Column(name price, nullable false) private BigDecimal price; }创建实体时要注意 JPA 的字段命名策略。Spring Boot 2.x 默认使用SpringPhysicalNamingStrategy会把stockDate自动转换为stock_date与数据库列名匹配。如果发现映射不上检查application.yml中是否配置了spring.jpa.hibernate.ddl-autoupdate它可以让 Hibernate 自动建表但生产环境建议改为validate避免实体字段误删导致数据丢失。3. 基于 Spring Boot 的民宿预定核心接口实现3.1 从 Controller 到 Service 的分层与职责边界接口开发遵循常见的三层结构Controller 负责接收参数和返回结果Service 负责业务规则Repository 负责数据访问。民宿预定系统的核心接口主要有三个查询可订房源、提交订单、支付回调更新订单状态。RestController RequestMapping(/api/booking) public class BookingController { Resource private BookingService bookingService; PostMapping(/create) public ResultLong createOrder(RequestBody Validated OrderCreateRequest request) { Long orderId bookingService.createOrder(request); return Result.success(orderId); } }OrderCreateRequest中至少要包含房型 ID、入住日期、离店日期、入住人姓名和手机号。Validated注解用于触发参数校验比如入住日期不能晚于离店日期可以用自定义注解或手动在 Service 中判断。不要把业务校验写进 Controller因为一个订单创建接口以后可能还要被管理后台调用校验逻辑必须沉淀在 Service 层。3.2 下单并发控制用数据库锁替代 synchronized民宿预定最大的技术挑战是防止超卖。假设某房型在国庆期间只剩一间房两个用户同时点击预定如果先查询库存再下单两次查询看到的都是“剩余 1 间”最终就会产生两笔订单卖出一间房。最直接的解决方案是使用数据库的乐观锁或悲观锁。悲观锁的写法是在事务内使用SELECT ... FOR UPDATE锁定库存行等订单创建完成后再释放锁。对于单机部署的毕业设计而言悲观锁简单、可靠、易于理解面试时也能把原理讲清楚。Transactional public Long createOrder(OrderCreateRequest request) { // 锁定库存行防止并发扣减 RoomStock stock roomStockRepository.findByRoomTypeIdAndStockDateForUpdate( request.getRoomTypeId(), request.getCheckInDate()); if (stock.getSoldCount() stock.getTotalCount()) { throw new BusinessException(该日期房源已售罄); } // 扣减库存 int updated roomStockRepository.decreaseStock( request.getRoomTypeId(), request.getCheckInDate()); if (updated 0) { throw new BusinessException(库存扣减失败请重试); } // 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setRoomTypeId(request.getRoomTypeId()); order.setStatus(OrderStatus.PENDING_PAYMENT); order.setTotalAmount(stock.getPrice().multiply( BigDecimal.valueOf(request.getNights()))); return orderRepository.save(order).getId(); }decreaseStock对应的 JPQL 或原生 SQL 是UPDATE room_stock SET sold_count sold_count 1 WHERE room_type_id ? AND stock_date ? AND sold_count total_count这样即使两个请求同时到达MySQL 的行锁也会让第二个请求的 update 等待第一个事务提交然后发现sold_count已经等于total_count影响行数为 0直接抛出异常。简单总结查询用FOR UPDATE加锁扣减用条件更新兜底两道防线都做才踏实。4. 订单状态机与 Spring Boot 中的状态流转控制4.1 民宿订单的五种状态与流转条件民宿订单不同于电商标准订单它有明确的时间轴语义。整个生命周期可以划分为五种状态待支付、已确认、已入住、已退房、已取消。状态之间不是任意跳转的比如“已退房”不能回到“已确认”“已取消”不能再次变成“待支付”。待支付 - 已确认支付成功 已确认 - 已入住到达民宿办理入住 已入住 - 已退房离店结算 待支付 - 已取消超时未支付或用户主动取消 已确认 - 已取消入住前免费取消将这些状态定义成枚举是常见做法。Java 枚举不仅可以保存状态码还能把可流转到的状态集合也写在里面让非法流转在编译期就能被发现。public enum OrderStatus { PENDING_PAYMENT(0, 待支付), CONFIRMED(1, 已确认), CHECKED_IN(2, 已入住), CHECKED_OUT(3, 已退房), CANCELLED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public boolean canTransitTo(OrderStatus target) { switch (this) { case PENDING_PAYMENT: return target CONFIRMED || target CANCELLED; case CONFIRMED: return target CHECKED_IN || target CANCELLED; case CHECKED_IN: return target CHECKED_OUT; default: return false; } } }4.2 状态变更接口与并发重复提交的防护状态流转变更时前端按钮要跟着状态展示后端接口必须做幂等处理。比如用户重复点击“确认入住”按钮系统不能重复把订单从“已确认”改成“已入住”更不能因此触发两次房间库存的释放逻辑。Transactional public void changeOrderStatus(Long orderId, OrderStatus targetStatus) { Order order orderRepository.findById(orderId) .orElseThrow(() - new BusinessException(订单不存在)); // 乐观锁版本号控制防止重复提交 int updated orderRepository.compareAndSetStatus( orderId, order.getStatus(), targetStatus, order.getVersion()); if (updated 0) { throw new BusinessException(订单状态已变更请刷新后重试); } }compareAndSetStatus使用UPDATE orders SET status ?, version version 1 WHERE id ? AND status ? AND version ?由数据库保证同一时刻只有一个请求能完成状态切换。这个方案比在 Service 层加synchronized更可靠因为synchronized只对单个 JVM 实例有效一旦将应用部署到多节点锁就失效了。数据库版本号方案天然支持多实例部署代码不复杂推荐优先采用。5. JWT 认证与 Spring Boot 拦截器保护预定接口5.1 为什么选择 JWT 而不是 Session民宿预定平台的用户端和管理端都需要登录认证。采用 JWT 的原因是它天然适合前后端分离架构后端不存会话状态前端在请求头中携带Authorization: Bearer token即可。对于毕业论文项目写完 JWT 登录认证也能体现对 Spring Boot 拦截器和现代 Web 安全方案的理解。实际搭建时登录接口用BCryptPasswordEncoder做密码校验登录成功后使用jjwt库生成 token并将用户 ID 写入 token 的 claims 中过期时间设为 24 小时。String token Jwts.builder() .setSubject(user.getId().toString()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 86400000L)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact();5.2 拦截器中解析 token 与白名单配置JWT 生成后需要写一个拦截器统一校验请求头。在 Spring Boot 中实现HandlerInterceptor并在配置类中注册同时放行登录注册接口和房源查询接口锁住下单、支付、评价和管理后台接口。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String header request.getHeader(Authorization); if (header null || !header.startsWith(Bearer )) { throw new BusinessException(未登录或登录已过期); } try { Claims claims Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(header.substring(7)) .getBody(); request.setAttribute(userId, Long.valueOf(claims.getSubject())); return true; } catch (Exception e) { throw new BusinessException(token 无效请重新登录); } } }配置注册时推荐用/api/booking/**、/api/user/**、/api/admin/**这类路径前缀做拦截粒度控制。放行的路径不要用/**一刀切因为这样任何请求都能穿透认证接口形同虚设。还要注意密码加密数据库不能存明文密码BCryptPasswordEncoder的加密结果每次随机盐值不同比对用matches方法不能直接equals。6. 本地部署验证与并发下单压测技巧6.1 打包运行与接口自测Spring Boot 项目完成核心功能后先不要急着写前端页面而是用 Maven 打包然后做接口级验证。打包命令是mvn clean package -DskipTests生成可执行 jar 后通过nohup java -jar target/bnb-platform-0.0.1-SNAPSHOT.jar --server.port8080 启动。启动日志中出现Started Application in xx seconds后用 curl 检查接口是否正常。curl -X POST http://localhost:8080/api/booking/create \ -H Content-Type: application/json \ -H Authorization: Bearer token \ -d {roomTypeId:1,checkInDate:2025-10-01,checkOutDate:2025-10-03,guestName:张三,guestPhone:13800000000}若返回的 JSON 中包含订单 ID说明整个链路已经跑通。容易出现的问题集中在room_stock表没有初始化数据导致查询不到库存记录而报空指针。建议写一个CommandLineRunner在项目启动时自动为未来 30 天的日期生成库存记录房型价格按工作日和周末分别设置不同值方便前端联调。6.2 用并发循环模拟超卖场景验证库存扣减是否正确可以借助 bash 的并发循环配合 curl 发请求。将同一个可售房型的下单接口同时发起 10 个请求观察最终成功订单数量。for i in $(seq 1 10); do curl -s -X POST http://localhost:8080/api/booking/create \ -H Content-Type: application/json \ -H Authorization: Bearer token \ -d {roomTypeId:1,checkInDate:2025-10-01,checkOutDate:2025-10-02,guestName:并发用户$i,guestPhone:1380000000$i} done wait执行后查询订单表中 2025-10-01 当天的订单数量应该等于该房型的房间总数超出部分必须有“已售罄”的异常返回。如果发现超卖检查 Service 层是否开启了Transactional如果没有事务包裹FOR UPDATE的锁会在第一条语句执行后释放后续请求就会越过锁检查直接读到旧库存值。另外也要确认数据库隔离级别不是READ UNCOMMITTED否则事务未提交的数据被其他请求读到同样会导致库存判断失真。6.3 actuator 与敏感配置的自我保护项目验收前建议留意两个容易忽略的配置。spring-boot-starter-actuator是调试时的好帮手但发布前要关闭敏感端点在application.yml中设置management.endpoints.web.exposure.includehealth,info避免/actuator/heapdump被访问后下载堆内存文件造成数据库密码和 JWT 密钥泄露。此外数据库密码不要明文写在 yml 里可以使用jasypt-spring-boot-starter加密配置项写成ENC(xxx)启动时通过环境变量传入解密密钥。项目演示或部署时这两个细节做好系统的完成度和安全观感会明显不一样。本文还有配套的精品资源点击获取
网站建设高端定制企业官网