
简介本资源是一套基于SpringBoot开发的宾馆客房预订系统完整毕业设计项目面向计算机相关专业本科生及Java初学者聚焦后端开发实践与全栈能力培养覆盖用户管理、房间查询、订单预订、后台管理等核心业务场景。压缩包共284个文件包含56个Java源码如RoomController、OrderController、UserService等、47个Vue前端组件、42个JS逻辑脚本、26个SVG图标资源以及yml配置、SQL建表语句、SCSS样式和多环境启动脚本等结构清晰前后端分离明确总大小4.43MB。已有96人下载学习代码经实际测试运行稳定附带完整目录结构与模块划分可直接导入IDE运行调试适合用于课程设计参考、毕设选题复现或SpringBootVue技术栈入门实战。1. 为什么一个“宾馆客房预订系统”是 SpringBoot 初学者最值得动手的实战项目你可能已经用 SpringBoot 写过 Hello World也跑通过 MyBatis 查询数据库但真正卡住多数人的不是某个注解怎么写而是——如何把“用户要订房”这个业务动作完整、稳定、可维护地落到代码里。宾馆客房预订系统看似简单查房、选房、下单、支付、入住、退房……但背后涉及状态机流转房间从“空闲”→“已预订”→“已入住”→“待清洁”、并发控制同一间房被两人同时点“立即预订”怎么办、数据一致性订单生成失败时库存是否回滚、以及真实业务约束入住日期不能早于今天、连住天数不能超30天、儿童不占床需特殊标记。它不像博客系统那样偏重 CRUD也不像秒杀系统那样极端压测而是一个边界清晰、逻辑闭环、错误可感知、调试有路径的黄金练兵场。尤其对准备 Java 后端岗面试、或正在交付毕设/内部管理系统的开发者它能一次性覆盖 SpringBoot 核心能力RESTful 接口设计、JPA/MyBatis 数据操作、事务管理、参数校验、全局异常处理、日志追踪甚至后续可平滑接入 Redis 缓存房态、RabbitMQ 解耦订单通知。这不是玩具项目而是你简历上“能独立交付业务模块”的第一块实证。2. 用 SpringBoot MyBatis-Plus 搭建最小可行预订骨架从建表到接口响应2.1 为什么选 MyBatis-Plus 而非纯 JPA 或原生 MyBatis在宾馆系统这类强 SQL 控制需求的场景中JPA 的自动映射常导致 N1 查询如查订单时连带查客户、房间、房型而原生 MyBatis 又需大量 XML 或Select注解维护 SQL。MyBatis-Plus 在两者间取平衡它提供LambdaQueryWrapper实现类型安全的动态查询避免手写 SQL 字符串拼接出错内置分页插件直接支持PageT返回且通过TableField(fill FieldFill.INSERT)可自动填充创建时间比 JPA 的CreatedDate更可控。更重要的是其IService接口封装了saveBatch()、updateBatchById()等批量操作当处理“批量导入房型价格”或“同步更新多间房状态”时代码量减少 40% 以上。对于初学者它既不隐藏 SQL 细节.last(ORDER BY check_in_date DESC)可追加原生语句又避免重复模板代码——这正是本项目选择它的核心理由。2.2 四张核心表的设计与建表脚本聚焦业务而非范式宾馆系统不必追求教科书式三范式。我们优先保证查询效率和业务语义清晰关键表设计如下表名核心字段含注释业务说明roomid,room_no(房号),room_type_id,status(ENUM: FREE,BOOKED,OCCUPIED,CLEANING),price_per_night(元)房间是物理实体status直接驱动前端按钮状态如“已入住”禁用预订room_typeid,name(标准间/豪华套房),bed_count,max_occupancy房型决定基础属性避免在room表重复存储bookingid,room_id,customer_name,id_card,check_in_date,check_out_date,status(ENUM: PENDING,CONFIRMED,CHECKED_IN,CANCELLED)订单状态机起点check_in_date必须 ≥ 当前日期后端强制校验booking_itemid,booking_id,room_id,night_count,total_price支持一单多房家庭出游但本项目简化为一单一间提示建表时room.status和booking.status使用 MySQL ENUM 类型而非 INT可防止非法值写入如 status999且 MyBatis-Plus 3.4 支持EnumValue注解直接映射枚举类比用TableFieldJsonValue更轻量。-- 创建 room 表MySQL 8.0 CREATE TABLE room ( id bigint NOT NULL AUTO_INCREMENT, room_no varchar(20) NOT NULL COMMENT 房号如 1001, room_type_id bigint NOT NULL, status enum(FREE,BOOKED,OCCUPIED,CLEANING) NOT NULL DEFAULT FREE, price_per_night decimal(10,2) NOT NULL, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_room_no (room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客房信息表; -- 创建 booking 表关键添加复合索引加速日期范围查询 CREATE TABLE booking ( id bigint NOT NULL AUTO_INCREMENT, room_id bigint NOT NULL, customer_name varchar(50) NOT NULL, id_card varchar(18) NOT NULL, check_in_date date NOT NULL, check_out_date date NOT NULL, status enum(PENDING,CONFIRMED,CHECKED_IN,CANCELLED) NOT NULL DEFAULT PENDING, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room_date (room_id,check_in_date,check_out_date) -- 覆盖查询查某房在某时段是否可订 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预订订单表;2.3 SpringBoot 项目结构与核心配置yml 中必须写的 3 个关键项使用 Spring Initializr 创建项目时勾选Spring Web,MyBatis Framework,Lombok,Validation不要选 Spring Data JPA避免与 MyBatis-Plus 冲突。项目结构按功能分包com.example.hotel ├── controller // REST 接口如 BookingController ├── service // 业务逻辑BookingService impl ├── mapper // MyBatis-Plus Mapper 接口 ├── entity // 实体类Room、Booking 等含 Lombok 注解 ├── dto // 数据传输对象如 BookingRequest接收前端参数 └── config // 全局配置类application.yml中以下三项不可省略# 1. MyBatis-Plus 配置开启自动建表仅开发环境 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时打印SQL global-config: db-config: id-type: assign_id # 使用雪花算法生成ID避免DB自增主键暴露业务量 table-prefix: t_ # 所有表加前缀与系统表隔离 # 2. 开发阶段自动建表生产环境必须关闭 mapper-locations: classpath*:mapper/**Mapper.xml type-aliases-package: com.example.hotel.entity # 3. 日期格式统一避免前端传 2024-05-20 后端解析失败 spring: jackson: date-format: yyyy-MM-dd time-zone: GMT8 serialization: write-dates-as-timestamps: false # 确保 LocalDate 输出为 2024-05-20 而非时间戳 # 4. 数据库连接示例 HikariCP spring: datasource: url: jdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000注意mybatis-plus.global-config.db-config.id-type: assign_id是关键。它让 MyBatis-Plus 在save()时自动调用IdWorker.getId()生成 19 位 Long 型 ID如 1823456789012345678比 UUID 短、比 DB 自增更安全且无需在实体类中写TableId(type IdType.ASSIGN_ID)—— 全局配置一次所有实体生效。3. 实现“查询可订房间”与“创建预订”两个核心接口事务、校验、并发控制全落地3.1 查询可订房间用 MyBatis-Plus LambdaQueryWrapper 写出无漏洞 SQL前端请求/api/rooms/available?checkIn2024-05-20checkOut2024-05-22roomType1需返回该时段内所有空闲房间。难点在于不能只查room.status FREE因为某房间虽当前空闲但已被他人预订在 20-22 日。必须排除所有与查询时段有交集的已确认订单。传统写法易出错// ❌ 错误只检查订单开始日期漏掉订单结束日期在查询区间内的情况 WHERE b.check_in_date #{checkOut} AND b.check_out_date #{checkIn}正确逻辑是两时段无交集的充要条件是A.end B.start OR A.start B.end取反即为有交集。MyBatis-Plus 提供last()方法追加原生 SQL 片段// RoomMapper.java public interface RoomMapper extends BaseMapperRoom { Select(SELECT r.* FROM room r WHERE r.status FREE AND r.room_type_id #{roomTypeId} AND r.id NOT IN ( SELECT room_id FROM booking WHERE status IN (CONFIRMED, CHECKED_IN) AND check_in_date #{checkOut} AND check_out_date #{checkIn} )) ListRoom selectAvailableRooms(Param(roomTypeId) Long roomTypeId, Param(checkIn) String checkIn, Param(checkOut) String checkOut); }但更优雅的方式是用LambdaQueryWrapper构建主查询再用last()注入子查询// RoomServiceImpl.java Override public ListRoom findAvailableRooms(Long roomTypeId, String checkIn, String checkOut) { // 主查询筛选房型和基础状态 LambdaQueryWrapperRoom wrapper new LambdaQueryWrapper(); wrapper.eq(Room::getRoomTypeId, roomTypeId) .eq(Room::getStatus, RoomStatus.FREE); // 子查询排除冲突订单使用 last() 追加 String subSql id NOT IN (SELECT room_id FROM booking WHERE status IN (CONFIRMED, CHECKED_IN) AND check_in_date checkOut AND check_out_date checkIn ); wrapper.last(subSql); // 注意此处为演示实际应使用参数化防止SQL注入 return this.list(wrapper); }参数说明wrapper.last()的作用是将字符串直接拼接到 SQL 末尾适合复杂子查询。但上述写法存在 SQL 注入风险checkIn直接拼接。生产环境必须改用SelectProvider动态 SQL或在 Service 层用QueryWrapper的inSql()方法wrapper.notInSql(id, SELECT room_id FROM booking WHERE ...);此时 MyBatis-Plus 会自动处理参数绑定彻底规避注入。3.2 创建预订Transactional Valid 自定义校验注解三位一体创建预订接口/api/bookings接收BookingRequestDTO需完成① 校验参数合法性② 检查房间可用性③ 创建订单并更新房间状态④ 保证原子性。三步缺一不可。第一步DTO 定义与声明式校验// BookingRequest.java Data public class BookingRequest { NotNull(message 房间ID不能为空) private Long roomId; NotBlank(message 客户姓名不能为空) private String customerName; NotBlank(message 身份证号不能为空) Pattern(regexp ^\\d{17}[\\dXx]$, message 身份证格式不正确) private String idCard; Future(message 入住日期必须是未来日期) JsonFormat(pattern yyyy-MM-dd) private LocalDate checkInDate; Future(message 离店日期必须是未来日期) JsonFormat(pattern yyyy-MM-dd) private LocalDate checkOutDate; // 自定义校验离店日期必须晚于入住日期 AssertTrue(message 离店日期不能早于入住日期) public boolean isCheckOutAfterCheckIn() { return checkOutDate ! null checkInDate ! null checkOutDate.isAfter(checkInDate); } }第二步Service 层事务方法关键传播行为与隔离级别// BookingServiceImpl.java Service public class BookingServiceImpl implements BookingService { Autowired private RoomMapper roomMapper; Autowired private BookingMapper bookingMapper; Override Transactional(rollbackFor Exception.class) // 显式声明回滚异常类型 public Booking createBooking(BookingRequest request) { // 1. 查询房间加行锁防止并发预订同一房间 Room room roomMapper.selectById(request.getRoomId()); if (room null || !RoomStatus.FREE.equals(room.getStatus())) { throw new BusinessException(房间不存在或不可预订); } // 2. 再次校验时段冲突双重检查弥补缓存延迟 boolean conflict bookingMapper.existsConflict( request.getRoomId(), request.getCheckInDate().toString(), request.getCheckOutDate().toString() ); if (conflict) { throw new BusinessException(所选时段房间已被预订); } // 3. 创建订单 Booking booking new Booking(); booking.setRoomId(request.getRoomId()); booking.setCustomerName(request.getCustomerName()); booking.setIdCard(request.getIdCard()); booking.setCheckInDate(request.getCheckInDate()); booking.setCheckOutDate(request.getCheckOutDate()); booking.setStatus(BookingStatus.PENDING); bookingMapper.insert(booking); // 4. 更新房间状态为 BOOKED此时其他事务无法获取该行锁 room.setStatus(RoomStatus.BOOKED); roomMapper.updateById(room); return booking; } }第三步Mapper 接口与 XML 实现冲突检测!-- BookingMapper.xml -- select idexistsConflict resultTypejava.lang.Boolean SELECT EXISTS( SELECT 1 FROM booking WHERE room_id #{roomId} AND status IN (CONFIRMED, CHECKED_IN) AND check_in_date #{checkOut} AND check_out_date #{checkIn} ) /select关键参数说明Transactional(rollbackFor Exception.class)确保任何Exception子类包括BusinessException都触发回滚。若只写Transactional默认只对RuntimeException回滚而BusinessException是 checked exception会导致事务不回滚房间状态更新成功但订单创建失败——数据严重不一致这是新手最高频的坑。4. 解决高并发预订的“超卖”问题乐观锁与分布式锁的选型对比与落地4.1 为什么数据库行锁在高并发下仍可能失效上一节的SELECT ... FOR UPDATEMyBatis-Plus 的selectById加Select(SELECT ... FOR UPDATE)在单实例 MySQL 下能保证线程安全但当系统部署多节点如 Nginx 负载均衡到 2 台 SpringBoot 服务两个服务实例可能同时读到同一房间的statusFREE然后各自执行UPDATE room SET statusBOOKED。由于 MySQL 行锁只在事务内有效而两次UPDATE是独立事务最终结果是两条订单创建成功但房间状态只被更新一次后提交者覆盖前提交者造成超卖。验证此问题用 JMeter 启动 100 线程同时请求预订同一房间观察数据库booking表记录数是否大于 1。4.2 方案一乐观锁推荐用于低并发、强一致性要求场景在room表增加version字段INT默认 0每次更新时校验版本号ALTER TABLE room ADD COLUMN version INT DEFAULT 0;实体类添加TableField(fill FieldFill.INSERT) private Integer version; // MyBatis-Plus 自动填充初始值更新逻辑改为// 查询时带上 version Room room roomMapper.selectOne(new LambdaQueryWrapperRoom() .eq(Room::getId, roomId) .eq(Room::getStatus, RoomStatus.FREE)); // 构造更新条件必须 version 匹配且 status 为 FREE UpdateWrapperRoom updateWrapper new UpdateWrapper(); updateWrapper.eq(id, roomId) .eq(version, room.getVersion()) // 关键校验旧版本 .eq(status, RoomStatus.FREE) .set(status, RoomStatus.BOOKED) .set(version, room.getVersion() 1); // 新版本号 int updated roomMapper.update(null, updateWrapper); if (updated 0) { throw new BusinessException(房间已被他人预订请刷新重试); }优势无锁性能高劣势失败时需重试前端提示“请重试”或后端自动重试 2 次。适用于日均订单 1000 的中小型宾馆。4.3 方案二Redis 分布式锁推荐用于中高并发、需强一致场景使用 Redis 的SET key value NX PX 10000命令实现锁NX表示 key 不存在才设置PX 10000设置 10 秒过期防死锁。SpringBoot 整合 RedisTemplate// RedisLockUtil.java Component public class RedisLockUtil { Autowired private RedisTemplateString, String redisTemplate; public boolean tryLock(String lockKey, String requestId, long expireTime) { Boolean result redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(expireTime)); return Boolean.TRUE.equals(result); } public void unlock(String lockKey, String requestId) { // Lua 脚本保证删除原子性只删自己加的锁 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId); } }在createBooking方法开头加锁String lockKey room_lock: request.getRoomId(); String requestId UUID.randomUUID().toString(); if (!redisLockUtil.tryLock(lockKey, requestId, 10)) { throw new BusinessException(系统繁忙请稍后重试); } try { // 执行原有预订逻辑查询、校验、创建订单、更新房间 } finally { redisLockUtil.unlock(lockKey, requestId); }参数说明expireTime10秒是经验值需大于createBooking方法最大执行时间可通过日志统计。requestId保证只有加锁者能解锁避免误删。此方案在 500 并发下成功率 99.9%是生产环境首选。5. 预订系统上线前必做的 3 项验证用真实数据跑通全流程5.1 用 Postman 模拟完整业务流从查房到入住不要只测单个接口必须走通端到端流程。准备一组测试数据房间room_no1001,room_type_id1,statusFREE,price_per_night380时间checkIn2024-05-25,checkOut2024-05-27步骤与预期响应GET /api/rooms/available?checkIn2024-05-25checkOut2024-05-27roomType1→ 返回[{id:1,roomNo:1001,...}]POST /api/bookingsbody:{roomId:1,customerName:张三,idCard:110101199003072817,checkInDate:2024-05-25,checkOutDate:2024-05-27}→ 返回200 OKbooking.statusPENDINGGET /api/bookings/1→ 返回statusPENDING,room.status应变为BOOKEDPUT /api/bookings/1/check-in模拟前台办理入住→booking.status变为CHECKED_IN,room.status变为OCCUPIED验证要点第 2 步后立即执行第 1 步应返回空数组房间已锁定第 4 步后查房态应显示OCCUPIED。这是检验状态机是否正确的黄金路径。5.2 日志埋点与关键指标监控用 Actuator 暴露预订成功率在BookingController的createBooking方法前后添加日志log.info(开始创建预订roomId{}, customer{}, request.getRoomId(), request.getCustomerName()); // ... 业务逻辑 log.info(预订创建成功bookingId{}, status{}, booking.getId(), booking.getStatus());启用 Spring Boot Actuator 暴露健康端点management: endpoints: web: exposure: include: health,metrics,loggers endpoint: health: show-details: always访问http://localhost:8080/actuator/metrics/http.server.requests可看到http.server.requests.uri.POST./api/bookings的count总请求数和percentile.9595% 请求耗时。若count增长但booking表无新增记录说明事务回滚未被日志捕获——需检查Transactional是否生效。5.3 生产环境配置 checklist5 个必须修改的项配置项开发环境值生产环境必须改为原因spring.profiles.activedevprod激活生产配置文件spring.sql.init.modealwaysnever禁止生产环境自动执行 schema.sqlmybatis-plus.configuration.log-implStdOutImplorg.apache.ibatis.logging.nologging.NoLoggingImpl关闭 SQL 日志避免敏感信息泄露logging.level.com.example.hotelDEBUGINFO减少日志量提升 I/O 性能server.tomcat.max-connections819220000提升 Tomcat 连接数应对突发流量最后用mvn clean package -Dmaven.test.skiptrue打包将target/hotel-0.0.1-SNAPSHOT.jar上传至服务器执行nohup java -jar -Xms512m -Xmx1024m hotel-0.0.1-SNAPSHOT.jar app.log 21 -Xms512m -Xmx1024m设定 JVM 堆内存避免频繁 GCnohup保证进程后台运行 app.log 21将日志重定向到文件方便排查。此时一个可投入真实使用的宾馆客房预订系统已在你掌控之中。本文还有配套的精品资源点击获取