基于Spring Boot和MyBatis的酒店预订管理系统核心实现与避坑指南 简介这是一份基于Java的酒店预订管理系统毕业设计论文面向计算机相关专业学生、需要完成课程设计或毕业设计的开发者以及希望快速了解酒店类管理系统设计思路的产品与研发人员。文档从开发背景与需求分析入手梳理了传统预订方式在数据处理时长、信息准确性等方面的痛点并完整介绍了个人中心、用户管理、房间类型管理、房间信息管理、房间预订管理等核心功能模块对管理员操作与用户预订流程均有说明。技术层面围绕B/S架构、Java语言、MySQL数据库及SSM框架展开覆盖业务逻辑层、Web层与数据访问层的实现思路同时提及系统上线后可使信息处理时间缩短至十几秒内体现其效率价值。资源以1个docx文档打包约1016KB正文包含摘要、目录及章节内容预览适合作为毕业设计选题参考、论文框架模板也可为同类管理系统的初期设计提供蓝图。该文档已有46人学习下载内容结构清晰便于按章节快速查阅。1. 酒店预订管理系统为什么这个 Java 项目值得认真做一遍不少人在第一次接触“基于java的酒店预订管理系统”这个题目时会以为它只是一套简单的增删改查建一个房间表建一个订单表再做两个页面就能交差。实际上这个小系统把库存、价格、状态机和并发锁全压在一张订单上比绝大多数培训班的 CRUD 项目更接近真实业务。常见的落地路径是 Spring Boot MyBatis MySQL 搭一个单体应用管理端用 Thymeleaf 或 Vue客户端走 REST API它解决的是酒店前台查房、预订、入住、退房、房态维护和简单报表的完整闭环。适合拿来做毕业设计或课程设计的 Java 开发者也适合业务量不大的小酒店把手工登记表替换成信息化系统。2. 代酒店预订系统的模块划分与选型先把业务流程画清楚再写第一行代码写代码之前我建议先把酒店的业务流程画出来。这不是走流程而是因为后面所有表结构、接口方法、状态枚举都是从这张图里“长”出来的。2.1 一条订单要经过哪些状态转变一次完整的酒店预订流程是客人查房 → 选房下单 → 支付确认 → 到店入住 → 退房离店 → 客房清扫 → 房间恢复可售。前台的每一步操作背后都对应一个状态变化如果一开始没想清楚后面一定会出现“房间已经退房但没打扫下一单又把它卖出去了”这种尴尬问题。这个项目里最关键的认知是订单状态和房间状态必须是两套状态不能混在一起。订单状态从“待支付”变成“已确认”再从“已确认”变成“已入住”最后变成“已退房”中间还有“已取消”和“超时未到”。房间状态则是“可售”“锁定”“已入住”“清扫中”“维修中”。很多新人会把订单的“已退房”直接当成房间的“可售”结果把清扫环节漏掉了。我一般会把这两套状态各做成一个枚举在代码里强制互相转换而不是随手写数字。2.2 技术选型为什么我选 Spring Boot MyBatis 而不是 JPA面对这种业务系统常见的可靠方案是 Spring Boot MyBatis MySQL前端管理后台可以用 Thymeleaf 模板也可以前后端分离用 Vue。为什么不用 Spring Data JPA不是因为 JPA 不能做而是酒店系统里报表和查房的 SQL 比较复杂需要手写 SQL 控制查询条件比如“某个日期区间内哪些房间没被占用”这种日期交叉判断在 JPA 里要么写 JPQL要么写原生 SQL反而绕了一圈。MyBatis 的 XML 文件能直接把 SQL 写清楚维护成本低非常适合这种中小型管理系统。另外这个项目一般不需要上微服务也不用引入消息队列。一个小型单体应用就够了否则光部署和监控就把业务逻辑的时间挤掉了。权限方面我也不建议一开始就上 Spring Security 全家桶用拦截器加 Session或者前后端分离时用 JWT都能满足前台、经理、住客三种角色的区分。2.3 工程结构与关键配置一个能跑通的最小骨架我常用的包结构是这样的com.hotel ├── HotelApplication.java ├── controller │ ├── RoomController.java │ └── BookingController.java ├── service │ ├── RoomService.java │ └── BookingService.java ├── mapper │ ├── RoomMapper.java │ └── BookingMapper.java ├── entity │ ├── Room.java │ ├── RoomType.java │ └── Booking.java ├── common │ ├── Result.java │ └── BusinessException.java └── resources ├── mapper │ ├── RoomMapper.xml │ └── BookingMapper.xml └── application.yml对应的 Maven 核心依赖常见做法如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies这套依赖的逻辑很直白web 负责接口mybatis 负责数据库访问mysql-connector-j 是 8.x 版本常用的连接驱动validation 用来校验前端参数比如入住日期不能为空、房型 ID 必须是正整数。application.yml 里最需要注意的是 JDBC 连接串中的时区参数spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hotel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hotel.entity参数说明driver-class-name 必须用 com.mysql.cj.jdbc.Driver这是 MySQL 8.x 的驱动类老项目里的 com.mysql.jdbc.Driver 在 8.x 下会直接启动失败serverTimezoneAsia/Shanghai 是解决数据库日期时区混乱的关键不加的话查询出来的 Date 类型可能比实际时间少 8 小时mapper-locations 告诉 MyBatis 到哪里找 XML 文件type-aliases-package 让 XML 里可以直接写 Room、Booking 这样的短类名。2.4 三种角色与权限边界这个系统里我一般会划分三种角色系统管理员、前台接待、普通住客。管理员能维护房型、房价、房间信息和查看全部报表前台能查房、创建订单、办理入住退房住客只能在自己的订单范围内操作。权限实现不用很复杂管理端用拦截器校验 Session 里的用户角色即可住客端接口可以通过 JWT 传递用户身份。接口层面做一件事就够了凡是修改订单状态的操作必须在方法内部校验当前用户是否为该订单的前台操作员或者该住客本人。很多项目在这里偷懒结果就是普通用户能通过猜 URL 取消别人的订单。2.5 用一个完整场景把模块串起来假如客人要预订 5 月 10 日到 5 月 12 日的大床房流程是这样的客户端先调“查房接口”传入日期区间和房型 ID后台执行日期交叉 SQL返回可售房间列表客人选定某一间提交订单后台在事务里锁住该房间校验档期按每晚价格汇总生成总金额写入订单表和订单明细表前台在管理端确认订单后房间状态变为“已锁定”入住当天前台点“入住”订单状态变为“已入住”房间状态变为“占用”退房时再走一次状态流转并把房间置为“清扫中”。整个过程所有状态变更都通过带条件的 UPDATE 完成而不是先查出来再改。3. 先定数据库8 张核心表的建表 SQL 与状态设计酒店预订系统的核心数据量不大但表之间关系比较密。我建议把房型、房间、住客、订单、订单明细、房价日历、房态日志、用户表这 8 张表一次性建好后面写代码时就不会反复改表。3.1 8 张核心表的建表 SQL下面是我在这个项目里最常用的一套建表语句字段命名统一用大写加下划线状态字段用有意义的字符串而不是 0 和 1 的魔法数字CREATE TABLE room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL COMMENT 房型名大床房/标准间/套房, base_price DECIMAL(10,2) NOT NULL COMMENT 基准价格, bed_count TINYINT NOT NULL DEFAULT 1 COMMENT 床位数, max_guest TINYINT NOT NULL DEFAULT 2 COMMENT 最多入住人数, area INT COMMENT 面积单位平方米, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT房型表; CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL COMMENT 房间号如 1208, room_type_id INT NOT NULL, floor INT COMMENT 所在楼层, status VARCHAR(20) NOT NULL DEFAULT AVAILABLE COMMENT AVAILABLE/CLEANING/MAINTENANCE, remark VARCHAR(255), KEY idx_type (room_type_id), CONSTRAINT fk_room_type FOREIGN KEY (room_type_id) REFERENCES room_type(id) ) COMMENT房间表; CREATE TABLE guest ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, id_card_no VARCHAR(30), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone) ) COMMENT住客表; CREATE TABLE booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_no VARCHAR(32) NOT NULL COMMENT 订单号, guest_id INT NOT NULL, room_id INT NOT NULL, room_type_id INT NOT NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, status VARCHAR(20) NOT NULL DEFAULT PENDING COMMENT PENDING/CONFIRMED/CHECKED_IN/CHECKED_OUT/CANCELLED/NO_SHOW, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, deposit_amount DECIMAL(10,2) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, cancel_time DATETIME, actual_check_in DATETIME, actual_check_out DATETIME, UNIQUE KEY uk_booking_no (booking_no), KEY idx_room_status_date (room_id, status, check_in, check_out) ) COMMENT预订单表; CREATE TABLE booking_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_id BIGINT NOT NULL, night_date DATE NOT NULL, price DECIMAL(10,2) NOT NULL, KEY idx_booking (booking_id), CONSTRAINT fk_detail_booking FOREIGN KEY (booking_id) REFERENCES booking(id) ) COMMENT订单每晚价格明细表; CREATE TABLE room_price ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_type_id INT NOT NULL, price_date DATE NOT NULL, price DECIMAL(10,2) NOT NULL, is_manual TINYINT DEFAULT 0 COMMENT 是否人工改过价格, KEY idx_type_date (room_type_id, price_date) ) COMMENT房价日历表; CREATE TABLE room_status_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, action VARCHAR(30) NOT NULL COMMENT CLEANED/MAINTENANCE/ENABLE, operator_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255) ) COMMENT房态操作日志表; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL COMMENT ADMIN/STAFF/USER, real_name VARCHAR(50), status TINYINT NOT NULL DEFAULT 1 ) COMMENT登录用户表;参数说明booking 表里我特意建了联合索引 idx_room_status_date顺序是 room_id、status、check_in、check_out这是查房 SQL 里最常用的过滤条件订单明细表 booking_detail 按每晚拆分价格这样后面出“某一天哪几种房型卖了多少间”的报表时直接按 night_date 聚合即可不必去订单主表里做日期区间计算。3.2 房间状态与订单状态分开建模房间状态我建议只保留 AVAILABLE、CLEANING、MAINTENANCE 以及 OCCUPIED 这几种而“已预订”不应该出现在房间状态里它应该由订单状态推出来。查询可售房间时把预订状态为 CONFIRMED 且日期区间的订单对应的房间排除掉就能得到真实的可售房间。订单状态则单独建枚举PENDING 表示客人刚提交但还没支付或前台还没确认CONFIRMED 表示已确认并占用了房间档期CHECKED_IN 表示已入住CHECKED_OUT 表示已退房CANCELLED 表示取消NO_SHOW 表示超时未到。这里有个实际业务细节PENDING 状态不应该锁房只保留一个短时间的锁定期比如 15 分钟超时后由定时任务把 PENDING 改成 CANCELLED释放房间。如果每个待支付订单都锁房酒店会大量出现“系统有房、实际卖不出去”的情况。3.3 房价日历为什么不能只存一个基准价格酒店房价不是恒定不变的周末、节假日、淡旺季都要调整价。如果只在 room_type 表里放一个 base_price后面所有改价都会变成在代码里写 if-else。常见做法是用 room_price 表按“房型 日期”存储价格查房时取的是 room_price 里的价格而不是房型表的基准价。计算订单总价时从 check_in 到 check_out 的每一个自然日都从 room_price 表查一次价格汇总到 booking_detail 表。这样订单的每一晚价格都有据可查后续客人和前台对账时也不会出现“为什么昨天 300今天 500”的问题。3.4 实体类自动生成建表 SQL 的坑现在有不少生成工具声称可以根据 Java 实体类自动生成建表 SQL比如 MyBatis-Plus 的相关工具能少写很多 DDL。我在实际项目里用过总结下来就是可以用但一定要人工检查字段类型。自动生成的 BigDecimal 有时会映射成 DOUBLE金额精度就丢了LocalDate 有时会被生成成 datetime日期时间混用查房时日期比较会出现偏差。所以我的建议是建表语句手工写实体类手工写两者保持一致如果只是为了节省时间生成后必须逐字段核对一遍再执行。4. 核心订单流程的 Java 实现查房、下单、入住与退房这一章是真正能“抄作业”的部分。先说明一个总体原则所有改变状态的数据库操作都要用带条件的 UPDATE 或者事务锁不能先 SELECT 再 UPDATE那是经典的并发翻车点。4.1 查房用日期交叉判断找出可售房间查房是整个系统最核心的 SQL。判断一个房间在某个日期区间是否可用不能只看某一天而要看订单日期是否与查询日期重叠。重叠条件用“左闭右开”表示订单的 check_in 必须小于查询的 check_out并且订单的 check_out 必须大于查询的 check_in这样才能正确覆盖入住当晚和退房当天的边界。对应的 MyBatis 查询 XML 如下select idlistAvailableRooms resultTypecom.hotel.entity.Room SELECT r.* FROM room r WHERE r.status AVAILABLE AND r.id NOT IN ( SELECT b.room_id FROM booking b WHERE b.status IN (CONFIRMED, CHECKED_IN) AND b.check_in lt; #{checkOut} AND b.check_out gt; #{checkIn} ) /select逻辑说明外层先过滤房间本身状态为 AVAILABLE内层找出所有在查询日期区间内仍然有效的订单再把对应房间排除。status 过滤只保留 CONFIRMED 和 CHECKED_IN因为订单处于 PENDING 时不应该锁房处于 CANCELLED 时自然也不应该锁房。参数说明#{checkIn}和#{checkOut}在 Java 层用 LocalDate 类型传入对应 SQL 里的 DATE 类型。这里要特别强调XML 里的小于号和大于号必须写成lt;和gt;这是 XML 转义不转义的话 MyBatis 解析 XML 文件会直接报错。这套查房逻辑同样适用于换房场景只要把房间 ID 排除条件加上其它部分完全一样。4.2 下单事务与行锁缺一不可酒店订单并发问题是这个项目最容易被面试官追问的点也是实际系统最容易出错的地方。下单前两个人同时看到同一间房同时提交订单如果不加控制就会超卖。常见做法是在同一个事务里先把目标房间的行锁住再校验档期最后插入订单。Service public class BookingService { private final RoomMapper roomMapper; private final BookingMapper bookingMapper; private final RoomPriceMapper roomPriceMapper; Transactional public Booking createBooking(BookingRequest req) { LocalDate checkIn req.getCheckIn(); LocalDate checkOut req.getCheckOut(); // 1. 锁行防止同一间房被两个并发请求同时预订 Room room roomMapper.selectRoomForUpdate(req.getRoomId()); if (room null) { throw new BusinessException(房间不存在); } // 2. 在锁内做档期校验 int overlap bookingMapper.countOverlap( room.getId(), checkIn, checkOut, List.of(CONFIRMED, CHECKED_IN)); if (overlap 0 || !AVAILABLE.equals(room.getStatus())) { throw new BusinessException(该房间在所选日期内已被预订); } // 3. 按每晚价格汇总总金额 BigDecimal total BigDecimal.ZERO; ListBookingDetail details new ArrayList(); for (LocalDate d checkIn; d.isBefore(checkOut); d d.plusDays(1)) { BigDecimal price roomPriceMapper.getPrice(room.getRoomTypeId(), d); total total.add(price); details.add(new BookingDetail(room.getRoomTypeId(), d, price)); } // 4. 写入订单主表 Booking booking Booking.builder() .bookingNo(generateBookingNo()) .guestId(req.getGuestId()) .roomId(room.getId()) .roomTypeId(room.getRoomTypeId()) .checkIn(checkIn) .checkOut(checkOut) .status(CONFIRMED) .totalAmount(total) .build(); bookingMapper.insert(booking); // 5. 写入每晚明细便于后续报表与对账 bookingMapper.insertDetails(booking.getId(), details); return booking; } }逻辑说明步骤 1 的 selectRoomForUpdate 对应 SQL 是SELECT * FROM room WHERE id #{id} FOR UPDATE。这一步会把 room 表的这一行锁住第二个请求会阻塞到第一个事务提交或回滚然后再读到最新状态。步骤 2 在锁内重新做重叠校验这时候的查询结果就是准确的了。步骤 3 用 LocalDate 循环从入住日到退房日的前一天每天取一次价格这样跨月、跨年的计算都不会出错。参数说明方法上必须加Transactional它保证锁、校验、插入、明细写入作为一个原子操作。注意 Spring AOP 事务默认只在外部调用时生效同一个类内部调用这个方法是不会开新事务的这是一个非常常见的“八股文式”坑但实战里也真的会出现。4.3 入住与退房用带条件的 UPDATE 推进状态入住本身不需要查一堆数据只需要把订单从 CONFIRMED 变成 CHECKED_IN把房间从 AVAILABLE 变成 OCCUPIED。关键是不能无条件 UPDATE否则一个已经取消的订单也能被误办入住。Transactional public void checkIn(Long bookingId, Long operatorId) { // 1. 先锁定订单行 Booking booking bookingMapper.selectById(bookingId); if (booking null) { throw new BusinessException(订单不存在); } // 2. 只有 CONFIRMED 状态才能办理入住 int rows bookingMapper.updateStatusOnly( bookingId, CONFIRMED, CHECKED_IN, LocalDateTime.now()); if (rows ! 1) { throw new BusinessException(订单状态不是已确认不能办理入住); } // 3. 房间状态改为占用 roomMapper.updateStatus(booking.getRoomId(), OCCUPIED); }逻辑说明updateStatusOnly 的 SQL 是UPDATE booking SET status #{newStatus}, actual_check_in #{time} WHERE id #{id} AND status #{oldStatus}这个 SQL 的精髓在于 WHERE 条件里带了旧状态。如果当前订单已经被取消或者已经被别人抢先办了入住更新影响行数就会是 0方法里通过rows ! 1直接抛出异常避免状态被二次覆盖。退房逻辑完全对称先将订单从 CHECKED_IN 变成 CHECKED_OUT将离店时间写入 actual_check_out再把房间状态改成 CLEANING而不是 AVAILABLE。房间必须经过清扫才能回到可售状态这是酒店业务里最容易漏的一环。4.4 今日入住率与预计离店列表管理端报表是这个项目里比较加分的内容。今日入住率可以用两个 count 查询解决房间里状态为 OCCUPIED 的数量除以可售房间总数预计离店列表则是查今天退房的订单。select idlistExpectedCheckOut resultTypecom.hotel.dto.CheckOutDto SELECT b.booking_no, b.check_out, b.total_amount, r.room_no, g.name AS guest_name FROM booking b JOIN room r ON b.room_id r.id JOIN guest g ON b.guest_id g.id WHERE b.status CHECKED_IN AND b.check_out CURDATE() ORDER BY r.room_no /select这段 SQL 的意义在于它把订单表、房间表、住客表的三表查询放在一个语句里完成而不是在 Java 内存里循环拼接。这里参数只有check_out CURDATE()没有外部传入所以适合直接放在凌晨的定时任务或者前台早上刷新列表时调用。5. 最容易翻车的 5 个坑重复预订、时区错乱和状态魔法值这个系统看起来简单实际跑起来后真正折磨人的往往不是业务逻辑而是一些底层细节。下面这些是我在类似项目里反复踩过的坑。5.1 房间重复预订导致超卖现象两个前台几乎同时给同一间房下单两个订单都显示预订成功但房间只有一间。原因代码里“先查是否可订再插入订单”的间隙里两个并发请求都读到了同样的可售状态然后各自插入成功。问题本质是这一步没有加锁也不是原子操作。解决把校验和插入放在同一个事务里并先对房间行执行 SELECT ... FOR UPDATE让第二个请求必须等待第一个事务结束。另外可以在 booking 表加一个唯一索引例如 room_id check_in check_out status让数据库层面再兜一道底。两个手段一起上才是完整的防线。提示FOR UPDATE 一定要放在事务内才有效而且锁的是 room 表的行不是业务语义上的“锁房间”。如果查询条件没有命中索引锁的粒度可能变成表锁反而拖慢其它操作。5.2 入住日期总是变成前一天现象前端选的入住日期是 2026-05-10数据库里存进去后显示成了 2026-05-09。原因这是典型的时区错乱。MySQL 连接串没有指定 serverTimezone 时驱动会按 JVM 默认时区处理而 JVM 和数据库所在系统时区不一致或者前端把 Date 类型用时间戳传递时发生了 UTC 转换。解决第一Java 层处理日期一律用 LocalDate不要用 java.util.Date第二数据库字段用 DATE 类型而不是 DATETIME第三JDBC 连接串加上 serverTimezoneAsia/Shanghai第四前端传参直接传字符串字符串“yyyy-MM-dd”由后端按 LocalDate 解析。这四件事都做到了日期基本不会再漂移。5.3 状态字段写成魔法数字现象SQL 里到处是WHERE status 2、UPDATE room SET status 3过两个月再看完全不知道 2 和 3 分别代表什么更不敢随便改。原因为了图省事把状态字段设计成了 TINYINT但又没有建立一致的映射文档也没有在代码里定义常量或枚举。解决状态字段存有意义的字符串比如 CONFIRMED、CANCELLED、OCCUPIED然后在 Java 层定义枚举 OrderStatus 和 RoomStatus。虽然字符串比数字占空间但可读性高得多排错时能直接看出业务含义。如果一定要用数字那就在实体类里建立枚举映射并且永远不要直接在 SQL 里手写数字。5.4 项目启动失败最常见的原因不在业务代码里现象启动 Spring Boot 时控制台报Field bookingMapper in com.hotel.service.BookingService required a single bean或者更早的时候直接报java.sql.SQLException: Unsupported character encoding utf8。原因前者通常是主启动类上漏了MapperScan(com.hotel.mapper)或者 MyBatis XML 里的 namespace 写错导致 mapper 接口没有注册成 Spring Bean后者是 JDBC 连接串里的 characterEncoding 参数写成了utf8而不是UTF-8。解决启动失败时先看错误栈最下面的 Caused by不要被中间长长的异常链带跑。常见顺序是先确认 JDK 版本与 java 环境变量是否配置正确再确认 MySQL 驱动版本最后检查 MapperScan 和 XML 文件里每一条 namespace。很多所谓“玄学启动失败”到最后都只是这三个环节之一。5.5 联合索引顺序写反报表越查越慢现象房间列表页和离店列表页数据量只有几千条但接口响应经常超过一两秒。原因booking 表上虽然建了索引但索引顺序是 status、room_id、check_in而查询条件通常是 room_id、status、日期区间。MySQL 的联合索引遵循“最左前缀”原则room_id 没放在最左边时这个索引没法被充分利用。解决把联合索引改为 room_id、status、check_in、check_out与查房 SQL 的 WHERE 顺序保持一致。另外避免在日期字段上使用函数例如写DATE(check_in) CURDATE()会导致索引失效应该直接写成check_in CURDATE() AND check_in DATE_ADD(CURDATE(), INTERVAL 1 DAY)。6. 上线前这样验证三步跑通一个真实入住流程并同步你的惰性项目写完不代表结束我习惯在交付前把一条完整业务流程从头到尾跑一遍用最原始的手段验证状态是否正确。先启动项目mvn spring-boot:run确认控制台出现Started HotelApplication后先查一次可用房间curl -X GET \ http://localhost:8080/api/room/available?checkIn2026-05-10checkOut2026-05-12roomTypeId1拿到一个房间 ID 后再发创建订单请求curl -X POST \ http://localhost:8080/api/booking \ -H Content-Type: application/json \ -d {guestId:1,roomId:101,checkIn:2026-05-10,checkOut:2026-05-12}然后模拟入住再看数据库SELECT id, booking_no, status, check_in, check_out, total_amount FROM booking WHERE booking_no xxxxx;重点看三件事第一订单状态是否从 CONFIRMED 变成 CHECKED_IN第二房间状态是否从 AVAILABLE 变成 OCCUPIED第三total_amount 是否等于每晚价格之和。这三项都对核心链路就算稳了。我最后想分享一个自己的教训有一回我把退房后的房间直接改成了 AVAILABLE结果客房阿姨还没打扫下一个客人已经通过线上渠道把房间订走了前台只能再打电话解释并换房。从那以后我在系统里把所有退房操作都强制导向 CLEANING 状态只有保洁完成才能恢复 AVAILABLE。状态机不是纸上谈兵它是一套防止人为失误的后悔药。希望这个细节也能帮到你。本文还有配套的精品资源点击获取