简介:本资源为基于SpringBoot的酒店客房管理系统完整源码包,面向Java全栈学习者、课程设计或毕业设计开发者,帮助快速搭建一套前后端分离的酒店业务管理平台。后端采用SpringBoot、MybatisPlus、MySQL、Redis与Shiro权限中间件,提供RESTful风格接口;前端基于Vue、Vuex、Vue-Router、Axios,配合Apex图表与Antd UI组件,覆盖管理员公告、客房收藏与房间管理、酒店商品与采购记录、部门与职位信息、附近美食推荐、客户留言与订单评价、客房预约下单、员工及用户管理等十余个业务模块。压缩包共625个文件,约4.28MB,以195个Java后端源码、169个XML映射配置、147个Vue页面组件为主,另含少量JS、图片、FTL模板与配置文件,结构清晰便于二次开发。目前已有147人学习下载,适合需要完整项目实战、接口设计与权限控制参考的开发者。
1. 从一张 Excel 排房表说起:SpringBoot 酒店客房管理系统到底在解决什么
很多中小酒店和民宿老板的日常是这样的:前台电脑上挂着一个 Excel,房态靠颜色标记,客人退房后手动改单元格,携程、美团、飞猪的订单再各自抄一遍。旺季一忙,超售、漏单、押金对不上是家常便饭。基于 SpringBoot 酒店客房管理系统要解决的,就是把「房态、订单、入住、退房、账务」这条链路从手工表格搬进一个能并发、能追溯、能对接前台的 Web 系统里。它适合谁?适合想做一个真实业务闭环练手的 Java 后端,也适合中小型酒店做轻量自研或二次开发。核心难点不在增删改查,而在房态并发、订单状态机和日期区间冲突检测——这三块做不对,系统上线就是灾难。下面按「选型 → 建模 → 核心逻辑 → 避坑 → 进阶」的顺序,把我实际落地时踩过的路讲清楚。
2. 技术选型与工程骨架:为什么是 SpringBoot 而不是别的
2.1 选型理由:单体 SpringBoot 在这个场景里够用且更省心
酒店客房管理系统的业务体量,一家 100 间房的酒店,日订单峰值也就几百单,QPS 撑死几十。这种量级上微服务是自找麻烦:分布式事务、链路追踪、服务注册,全是成本,收益几乎为零。所以我的建议是老老实实做一个单体 SpringBoot 应用,前后端分离,后端一个 jar 包,前端 Vue 打包成静态资源,Nginx 一挂就完事。
SpringBoot 在这里的价值主要是三点:一是自动配置把数据源、事务、Web 层这些样板代码全包了,spring-boot-starter-web+spring-boot-starter-data-jpa(或 MyBatis)两个依赖就能跑起来;二是内嵌 Tomcat,java -jar直接启动,部署到宝塔面板或者用 Docker 都很顺;三是生态成熟,定时任务、缓存、消息队列这些后续要加的能力,starter 一引就有。
关于「springboot 版本太高」这个热搜词,确实是个真实的坑。SpringBoot 3.x 要求 JDK 17 起步,而且把javax.*全换成了jakarta.*,很多老教程里的代码直接编译不过。如果你团队还在 JDK 8,就老老实实用 2.7.x 这条线,别硬上 3.x。选版本的原则是:跟着你团队最熟的 JDK 走,而不是跟着最新版走。
2.2 工程目录结构:一个能长期维护的分层
「springboot 项目结构」和「springboot web 项目结构目录」是高频搜索,说明很多人卡在不知道怎么分层。酒店系统业务不复杂,但实体关系多(房间、房型、订单、客人、账单),分层乱了后期改起来很痛苦。我一般用这样的结构:
hotel-management/ ├── src/main/java/com/example/hotel/ │ ├── HotelApplication.java // 启动类 │ ├── config/ // 配置类:拦截器、跨域、线程池 │ ├── controller/ // 接口层,只做参数校验和转发 │ ├── service/ // 业务逻辑,事务边界在这一层 │ │ └── impl/ │ ├── repository/ // 数据访问层(JPA 或 MyBatis Mapper) │ ├── entity/ // 数据库实体 │ ├── dto/ // 请求/响应对象,和 entity 分开 │ ├── enums/ // 订单状态、房态等枚举 │ └── common/ // 统一返回、异常、工具类 └── src/main/resources/ ├── application.yml └── mapper/ // MyBatis XML(如果用 MyBatis)关键原则:entity不要直接暴露给前端,用dto转换。订单状态、房态这种有限集合,一定用枚举而不是魔法数字,否则半年后你自己都看不懂status=3是什么意思。
2.3 依赖与配置:最小可运行集合
pom.xml里核心依赖就这些,别一上来堆一堆:
<dependencies> <!-- Web 层 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据访问,二选一:JPA 或 MyBatis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 参数校验 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>application.yml里几个必须调对的参数:
spring: datasource: url: jdbc:mysql://localhost:3306/hotel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password hikari: maximum-pool-size: 10 # 小系统 10 足够,别开太大 connection-timeout: 30000 jpa: hibernate: ddl-auto: validate # 生产环境绝不用 update/create show-sql: false # 生产关掉,开发可开 server: port: 8080ddl-auto这个参数是血泪教训:开发期用update方便,但上线前一定要改成validate或none,用 Flyway/Liquibase 管表结构。我见过有人生产环境留着update,一次实体改动把线上表字段删了。
3. 数据建模与房态设计:房间、房型、订单怎么拆
3.1 实体关系:房型与房间必须分开
新手最容易犯的错是把「房型」和「房间」合成一张表。正确做法是两张表:room_type(房型:大床房、标间,含价格、床型、可住人数)和room(具体房间:301、302,关联房型 ID)。为什么?因为价格、描述挂在房型上,改一次全房型生效;而房态、清洁状态挂在具体房间上。合成一张表,改价格要更新几十行,迟早不一致。
订单表order是核心,关键字段:room_type_id、check_in_date、check_out_date、status、guest_id、total_amount。注意这里订单关联的是房型而不是具体房间——客人订的是「一间大床房」,具体分到 301 还是 302 是入住时前台操作的。这个设计决定了后面库存扣减的逻辑。
3.2 订单状态机:别用一堆 if-else 硬编码
订单状态流转是酒店系统的命脉,常见状态:PENDING(待支付)、PAID(已支付待入住)、CHECKED_IN(已入住)、CHECKED_OUT(已退房)、CANCELLED(已取消)、NO_SHOW(未到店)。用枚举定义合法流转:
public enum OrderStatus { PENDING, PAID, CHECKED_IN, CHECKED_OUT, CANCELLED, NO_SHOW; // 定义每个状态能流转到哪些状态 public boolean canTransferTo(OrderStatus target) { switch (this) { case PENDING: return target == PAID || target == CANCELLED; case PAID: return target == CHECKED_IN || target == CANCELLED || target == NO_SHOW; case CHECKED_IN: return target == CHECKED_OUT; default: return false; // 终态不可再流转 } } }在 service 层做状态变更时,先校验canTransferTo,不合法直接抛业务异常。这样任何非法流转(比如已退房又改成已入住)都会被拦住,而不是靠人肉 review 代码。
3.3 房态与库存:可用房数量怎么算
「某房型在某天还剩几间」这个问题,不要用一张inventory表每天一行去维护,容易和订单不同步。更稳的做法是实时计算:可用数 = 房型总房数 − 该日期区间内已占用数。查询时用一条 SQL 聚合:
-- 查询某房型在 [checkIn, checkOut) 区间内已占用的房间数 SELECT COUNT(*) FROM `order` WHERE room_type_id = #{roomTypeId} AND status IN ('PAID', 'CHECKED_IN') -- 只有这两种状态占库存 AND check_in_date < #{checkOut} -- 区间重叠判断 AND check_out_date > #{checkIn};区间重叠的判断是重点:两个日期区间[a1, a2)和[b1, b2)重叠的条件是a1 < b2 AND a2 > b1。注意用左闭右开,因为客人 5 号退房、5 号新客人入住是不冲突的。这个边界搞错,就会出现「明明有空房却订不了」或者「超售」。
提示:
status IN ('PAID','CHECKED_IN')这个条件决定了哪些状态占库存。待支付订单要不要占?我的做法是下单时短暂占用(配合超时释放),支付失败或超时自动取消。简单点也可以只让 PAID 占库存,代价是并发下单时可能超卖,需要靠下面的锁兜底。
4. 核心业务落地:下单、并发与定时任务
4.1 下单接口:先校验再落库,顺序不能乱
下单是并发最集中的地方,逻辑顺序错了就是超售。标准流程:校验日期合法性 → 校验库存 → 创建订单 → 扣减(或标记占用)。库存校验和创建之间必须加锁,否则两个请求同时查到「还剩 1 间」,都创建成功,就超售了。
@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public Order createOrder(CreateOrderRequest req) { // 1. 基础校验:入住日期不能早于今天,退房必须晚于入住 if (!req.getCheckOut().isAfter(req.getCheckIn())) { throw new BizException("退房日期必须晚于入住日期"); } // 2. 加锁后校验库存,锁的粒度是房型 synchronized (("roomType:" + req.getRoomTypeId()).intern()) { int occupied = orderRepository.countOccupied( req.getRoomTypeId(), req.getCheckIn(), req.getCheckOut()); int total = roomRepository.countByRoomTypeId(req.getRoomTypeId()); if (occupied >= total) { throw new BizException("该房型在所选日期已订满"); } // 3. 创建订单 Order order = buildOrder(req); return orderRepository.save(order); } } }逻辑说明:synchronized锁的是房型维度的字符串常量(intern()保证相同字符串是同一对象),这样不同房型的下单互不阻塞,同一房型串行。参数上,countOccupied就是 3.3 那条 SQL。注意这是单机方案,多实例部署时synchronized失效,要换成 Redis 分布式锁或数据库悲观锁(SELECT ... FOR UPDATE)。小系统单机跑,这个方案足够。
4.2 超时未支付自动取消:用 SpringBoot 定时任务
「springboot 定时任务」是热搜词,这里正好用得上。待支付订单超过 15 分钟自动取消并释放库存:
@Component public class OrderTimeoutTask { @Autowired private OrderService orderService; // 每 5 分钟扫一次,cron 表达式按需调整 @Scheduled(cron = "0 */5 * * * ?") public void cancelTimeoutOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(15); List<Order> timeoutOrders = orderService.findPendingBefore(deadline); for (Order order : timeoutOrders) { try { orderService.cancel(order.getId(), "超时未支付自动取消"); } catch (Exception e) { // 单条失败不影响其他订单,记录日志人工排查 log.error("取消超时订单失败, orderId={}", order.getId(), e); } } } }逻辑说明:@Scheduled的 cron 是秒 分 时 日 月 周,0 */5 * * * ?表示每 5 分钟的第 0 秒执行。参数上,扫描间隔要小于超时时间(15 分钟),否则订单会晚取消。注意两点:一是启动类要加@EnableScheduling;二是多实例部署时这个任务会重复执行,需要加分布式锁或者用数据库行锁保证幂等——cancel方法内部要先判断状态是否还是PENDING,是才取消,这样重复执行也安全。
4.3 退房与账务:金额计算别用 double
退房时要结算房费、押金、可能的额外消费。金额一律用BigDecimal,绝不用double——0.1 + 0.2 != 0.3这个玄学问题在账务上是致命的。房费 = 房型单价 × 入住天数,天数用ChronoUnit.DAYS.between(checkIn, checkOut)算,注意这个 API 算的是完整天数差,正好符合「住几晚」的语义。
public BigDecimal calcRoomFee(BigDecimal pricePerNight, LocalDate checkIn, LocalDate checkOut) { long nights = ChronoUnit.DAYS.between(checkIn, checkOut); if (nights <= 0) { throw new BizException("入住天数不合法"); } return pricePerNight.multiply(BigDecimal.valueOf(nights)) .setScale(2, RoundingMode.HALF_UP); // 保留两位,四舍五入 }参数说明:setScale(2, RoundingMode.HALF_UP)是金额标准处理,两位小数、四舍五入。别用HALF_EVEN(银行家舍入),除非财务明确要求,否则对不上账。
5. 避坑与排查:上线前必须过的几道坎
5.1 坑一:日期区间边界搞错导致超售或订不了
现象:明明显示有房,下单却提示已满;或者反过来,同一间房被两个订单占用。原因:区间重叠判断写成了check_in_date <= #{checkOut} AND check_out_date >= #{checkIn},用了闭区间,导致 5 号退房和 5 号入住被判为冲突。解决:统一用左闭右开check_in_date < #{checkOut} AND check_out_date > #{checkIn},并在单元测试里专门覆盖「退房日=入住日」这个边界。
5.2 坑二:@Transactional 自调用失效
现象:下单方法里调了本类的另一个@Transactional方法,事务没生效,库存扣了订单没落库。原因:Spring 的事务基于 AOP 代理,类内部this.method()调用不走代理。解决:要么把方法拆到另一个 service,要么注入自身代理,要么用TransactionTemplate手动控制。这个坑几乎每个新手都会踩一次。
5.3 坑三:ddl-auto 在生产环境乱改表
现象:某次发版后线上表字段莫名消失或类型变了。原因:spring.jpa.hibernate.ddl-auto=update在生产环境被保留,Hibernate 按实体自动改表。解决:生产环境一律validate或none,表结构变更走 Flyway 脚本,每次变更可追溯、可回滚。
5.4 坑四:定时任务在多实例下重复执行
现象:订单被取消两次,或者库存被释放两次导致数据错乱。原因:两个实例的@Scheduled同时触发。解决:cancel方法内先做状态判断(乐观锁),只有PENDING才取消;或者引入 Redis 分布式锁,同一时刻只有一个实例执行。前者更简单,推荐。
5.5 坑五:时间字段用错类型导致时区错乱
现象:订单创建时间比实际早 8 小时,或者跨时区客人看到的时间不对。原因:数据库用datetime但没设时区,JDBC URL 里serverTimezone没配或配错。解决:JDBC URL 明确写serverTimezone=Asia/Shanghai,实体时间字段统一用LocalDateTime,别混用java.util.Date。
6. 进阶技巧:用状态机 + 乐观锁把并发订单做扎实
前面第 4 章的synchronized只适合单机。真要多实例部署,或者你想让这套系统经得起面试追问,就得把并发控制做扎实。我的做法是「乐观锁 + 状态机」双保险。
先给订单表加一个version字段,用 JPA 的@Version注解:
@Entity public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Version private Integer version; // 乐观锁版本号,每次更新自动 +1 // 其他字段省略 }然后在状态变更时,用「带条件的更新」代替「先查后改」:
@Modifying @Query("UPDATE Order o SET o.status = :target, o.version = o.version + 1 " + "WHERE o.id = :id AND o.status = :expected AND o.version = :version") int updateStatusWithVersion(@Param("id") Long id, @Param("expected") OrderStatus expected, @Param("target") OrderStatus target, @Param("version") Integer version);逻辑说明:这条 UPDATE 把「状态必须是 expected」和「版本号必须匹配」两个条件写进 WHERE,返回受影响行数。如果返回 0,说明订单已被别人改过(状态变了或版本变了),当前操作失败,抛异常让前端重试。这样即使两个请求同时想取消同一订单,也只有一个能成功,另一个拿到 0 行,天然幂等。
参数上,expected是操作前查到的状态,version是查到的版本号。这套组合的好处是:不用数据库悲观锁(FOR UPDATE)那么重,性能好;又比纯synchronized可靠,多实例也安全。代价是失败要重试,所以前端接口要能处理「操作冲突,请刷新重试」这种返回。
再进一步,可以把状态流转规则抽成一张配置表或者用 Spring StateMachine,让「哪些状态能转到哪些状态」可配置化。但我的经验是,酒店系统状态就那几个,用第 3.2 节的枚举canTransferTo已经够清晰,上状态机框架反而增加理解成本。技术选型要克制,能用一个枚举解决的问题,别引一个框架。
最后说个验证方法:并发这块光靠看代码不放心,写个压测脚本,用 JMeter 或者简单的多线程程序,对同一个房型同时发 50 个下单请求,看最终成功的是不是正好等于房间数。我当初就是靠这个测出区间判断的边界 bug 的。这个习惯我一直保留着——凡是涉及库存、金额、状态的逻辑,写完必压一遍,比 code review 管用。希望帮到你。
本文还有配套的精品资源,点击获取