简介:这是一份基于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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false 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 下会直接启动失败;serverTimezone=Asia/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 id="listAvailableRooms" resultType="com.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 < #{checkOut} AND b.check_out > #{checkIn} ) </select>逻辑说明:外层先过滤房间本身状态为 AVAILABLE,内层找出所有在查询日期区间内仍然有效的订单,再把对应房间排除。status 过滤只保留 CONFIRMED 和 CHECKED_IN,因为订单处于 PENDING 时不应该锁房,处于 CANCELLED 时自然也不应该锁房。
参数说明:#{checkIn}和#{checkOut}在 Java 层用 LocalDate 类型传入,对应 SQL 里的 DATE 类型。这里要特别强调,XML 里的小于号和大于号必须写成<和>,这是 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; List<BookingDetail> 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 id="listExpectedCheckOut" resultType="com.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 连接串加上 serverTimezone=Asia/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?checkIn=2026-05-10&checkOut=2026-05-12&roomTypeId=1'拿到一个房间 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。状态机不是纸上谈兵,它是一套防止人为失误的后悔药。希望这个细节也能帮到你。
本文还有配套的精品资源,点击获取