十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot酒店客房管理平台:状态流转设计与实战踩坑

Spring Boot酒店客房管理平台:状态流转设计与实战踩坑 简介基于 Spring Boot 的酒店客房管理平台设计与实现文档主要面向酒店管理系统开发学习者、计算机专业毕业设计学生以及需要快速完成同类工程项目的开发者。全文按设计论文结构组织包含需求分析、系统总体架构、微服务模块划分、数据库实体与表设计、经济与技术可行性等内容并围绕客房信息、客房预订、客房入住、客房结算四个核心功能模块展开详细说明系统采用 RESTful API 完成模块间交互设计思路清晰完整。资源包共 1 个 DOCX 文件大小 3.42MB为可直接编辑的完整设计文档读者可将其作为设计说明书、课程报告或论文初稿的参考底稿节省从头搭建文档结构的时间。目前已有 77 人学习适合想了解 Spring Boot 在酒店管理业务中落地思路并梳理毕业设计框架的读者。 如果把“基于Spring Boot的酒店客房管理平台”这个题目扔给刚学完Java Web的同学大部分人的第一反应是打开IDEA新建一个Spring Initializr项目赶紧把CRUD写出来。我当初也这么干过结果写了一半发现房间状态、订单状态到处乱飞连自己都理不清哪张表该在什么时候改哪个字段。后来把流程彻底想透了推倒重写一遍整个项目才真正立住。这篇东西不打算讲太多虚的就围绕这个题目的“设计”和“实现”两件事把需求边界、技术选型、数据库设计、核心链路、定时任务以及我在实测中踩过的几个坑一次讲完。适合正在做这个题目的同学参考也适合想拿Spring Boot练手、但不想写那种“假项目”的人看一看。1. 先拆需求酒店客房管理平台到底要管多少东西1.1 别把毕业设计做成酒店ERP做这个题目最容易犯的错是觉得功能越多越好。什么会员等级、积分商城、酒水消费、对接门锁全塞进来最后代码写了一万多行答辩的时候连自己都讲不清模块关系。实际上酒店客房管理平台的核心流程非常集中客人订房 → 前台登记入住 → 退房结账。围绕这一条主线后台需要维护房型、房间、订单和登录账号。你把这四类数据之间的关系理清了项目就完成了八成。我最终把系统边界定成一句话给中小型酒店前台用的客房管理工具管的是“房间状态”和“订单状态”不管财务不管会员营销。这句话在后面的表设计里直接决定了字段怎么安排。1.2 角色清单一列前端页面数量就出来了分清角色很重要。这个平台里只有两类人管理员/前台登录后台维护房型和房间、处理预订、办理入住退房、查看统计报表客人可选如果做前台代客下单就不需要客人端如果想做成在线预订需要给客人一个查询房间和提交订单的入口我当时选了“前台代客下单客人可查询”的混合模式。客人端只做两件事查房和提交预订其余全部由后台完成。这样做的好处是少写一套用户中心页面数量和接口数量都控制在合理范围。1.3 功能清单应该这么列把需求落到表格里比任何口头描述都清晰模块功能点优先级登录认证账号密码登录、JWT鉴权、验证码必做房型管理房型增删改查、价格、床型、面积必做房间管理房间编号、楼层、状态维护必做预订管理创建订单、取消订单、订单列表查询必做入住管理根据预订办理入住、直接开房必做退房管理退房结账、房间状态更新必做定时任务超时未支付订单自动取消加分数据统计今日入住率、营收统计加分这里我特意把“必做”和“加分”分开。做毕设最重要的不是功能多而是功能闭环。五个必做功能全部跑通体验比十个半成品好太多。2. 技术选型与项目初始化版本这里就埋了最大的雷2.1 Spring Boot到底用2.7还是3.x说实话如果现在开始做Spring Boot 3.x已经很成熟了。但我依然建议这个题目用Spring Boot 2.7.x JDK 8的组合。为什么两个原因。一是包名兼容性。Spring Boot 3.x把javax.*迁到了jakarta.*网上大量教程、博客还是基于2.x写的你抄一段代码发现import javax.servlet报红查半天才明白是版本导致的。二是生态兼容。很多老牌的第三方依赖对3.x的支持并不无缝MyBatis Plus老版本、某些工具类在3.x下会有兼容问题。这不是说3.x不能用而是做毕业设计讲究“稳”。技术评审老师问起来你说“用2.7是为了兼容大量成熟生态”这个理由完全站得住。2.2 组件的选择逻辑我的技术栈是这样的Spring Boot 2.7.18核心框架MyBatis Plus 3.5.x持久层单表CRUD和分页非常省事Redis存验证码、存登录Token、缓存房态数据JWTio.jsonwebtoken登录令牌MySQL 8.x数据库Hutool工具库生成订单号、加密用起来很舒服Lombok省略getter/setter很多人纠结要不要用Spring Security。我的结论是别用至少别在核心登录逻辑里用。这个题目对权限的要求只有“是否登录”和“是不是管理员”两层用拦截器注解就能解决。Spring Security的过滤器链和配置规则对新手非常不友好光一个“为什么我放行的接口还被拦截”就能卡你两天。2.3 application.yml的关键配置这里直接给出一份我当时调通的配置。注意JWT的密钥一定不能写进代码里放在配置文件中并设置过期时间server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hotel_room?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true jwt: secret: your-256-bit-secret-key-here-please-change expire: 86400000map-underscore-to-camel-case这个配置必须打开否则数据库字段room_no映射不到实体的roomNo上。Jackson的日期格式化一定要配否则后面LocalDateTime返回给前端会是一串数字数组这是很常见的问题。3. 数据库设计一张房态表撑起整个业务3.1 核心表数量别贪多我最终的数据库只有四张核心业务表外加一张操作日志表表名用途关键字段sys_user后台账号username, password, role, statusroom_type房型定义name, price, bed_num, area, remarkroom具体房间room_no, room_type_id, floor, statusreservation_order订单主表order_no, room_id, customer_name, phone, check_in_date, check_out_date, total_price, statusoperation_log操作日志user_id, action, detail, create_time订单表是绝对的核心。很多同学纠结要不要把入住记录和订单分开我的做法是订单表贯穿全过程。订单状态从“待支付”一路走到“已退房”中间所有状态变化都体现在这条订单记录上。房间表只负责维护当前物理状态。这样设计写代码的时候心智负担小很多。3.2 状态字段是整个系统的灵魂这个项目的复杂度和状态机直接相关。我用了两个状态字段一个管房间一个管订单room.status: 0 空闲 1 已预订 2 入住中 3 清洁中 reservation_order.status: 0 待支付 1 已确认已预订 2 已入住 3 已退房完成 4 已取消 5 已过期超时未支付系统取消状态流转一定要提前画清楚不然写接口的时候就是一场灾难。我的流转过程可以这样描述创建订单room 0 → 1order 0支付/确认order 0 → 1办理入住room 1 → 2order 1 → 2办理退房room 2 → 3order 2 → 3清洁完成room 3 → 0取消含超时room 1 → 0order 回到4或5注意每次状态变更都是一条SQL同时更新两张表必须放在同一个事务里。网上很多教程只更新订单状态、忘记回滚房间状态这是最常见的逻辑漏洞。3.3 并发场景下怎么防止“一间房被订两次”这是一道很容易被问到的高频面试题。学生价位的方案是把“抢占房间”做成一个条件更新UPDATE room SET status 1 WHERE id #{roomId} AND status 0通过status 0这个条件保证只有当前空闲的房间才能被订走返回受影响行数。如果影响行数为0说明房间已经被人抢了直接抛业务异常。这个写法本质上就是乐观锁的一种变形面试的时候完全可以这样讲。我在订单表上还加了一个唯一索引uk_order_no保证同一时刻不会生成重复订单号。另外在代码里用synchronized锁房间ID也是一种做法但集群环境下不可靠条件更新更通用。4. 核心链路实现登录、订房、入住、退房4.1 JWT登录与权限拦截登录逻辑不复杂校验用户名密码 → 生成JWT → 把Token写入Redis。Redis的过期时间必须和JWT过期时间保持一致这样退出登录时只需要删Redis里的Token就能实现真正的“注销”。JWT工具类我直接用的io.jsonwebtoken核心方法就两个public String createToken(Integer userId, String role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }拦截器的逻辑更简单每次请求从Header里拿Token解析成功再去Redis查一下这个Token还在不在两步都通过才放行String token request.getHeader(token); if (token null || !redisTemplate.hasKey(login:token: token)) { throw new BusinessException(401, 未登录或登录已过期); }用Redis存Token的好处很明显后台可以随时踢人下线这在答辩演示的时候是个亮点。还有一点密码存储要加密不要明文存库。用Hutool的BCrypt.hashpw()就行几行代码的事。4.2 创建订单别让事务只停留在注解上订房接口是整个平台最核心的方法。我给出一个核心逻辑的骨架Transactional(rollbackFor Exception.class) public Long createOrder(CreateOrderDTO dto) { // 1. 幂等校验手机号入住日期范围内已经存在未取消订单 int exists orderMapper.checkExists(dto.getRoomId(), dto.getCheckInDate(), dto.getCheckOutDate()); if (exists 0) { throw new BusinessException(该房间在所选日期内已被预订); } // 2. 乐观锁抢占房间 int rows roomMapper.casLockRoom(dto.getRoomId()); if (rows 0) { throw new BusinessException(房间已被预订请重新选择); } // 3. 计算金额生成订单号 RoomType type roomTypeMapper.selectById(dto.getRoomTypeId()); BigDecimal total type.getPrice() .multiply(BigDecimal.valueOf( ChronoUnit.DAYS.between(dto.getCheckInDate(), dto.getCheckOutDate()))); ReservationOrder order new ReservationOrder(); order.setOrderNo(HutoolUtil.generateOrderNo()); order.setStatus(0); order.setTotalPrice(total); orderMapper.insert(order); return order.getId(); }注意三个细节Transactional必须指定rollbackFor Exception.class因为Spring默认只回滚RuntimeException如果你抛的是自定义检查异常不加这个事务不会回滚。幂等校验一定要做。前台手一抖点两次提交就会生成两笔订单。我的方案是查一下这个手机号在这个日期段内有没有未取消的订单有就直接拒绝。金额计算用BigDecimal绝不用double。这是最基本的财务安全常识。4.3 入住退房的代码套路完全一样入住和退房本质上是两个状态的协同更新套路一模一样Transactional(rollbackFor Exception.class) public void checkIn(Long orderId) { ReservationOrder order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 1) { throw new BusinessException(订单不存在或状态不允许办理入住); } // 更新订单状态 orderMapper.updateStatus(orderId, 2); // 更新房间状态 roomMapper.updateStatus(order.getRoomId(), 2); } Transactional(rollbackFor Exception.class) public void checkOut(Long orderId) { ReservationOrder order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 2) { throw new BusinessException(订单不存在或状态不允许退房); } orderMapper.updateStatus(orderId, 3); // 退房后房间进入清洁中 roomMapper.updateStatus(order.getRoomId(), 3); }这个代码看起来简单但我当时踩过一个非常隐蔽的坑先查订单、再更新状态中间存在时间窗口。如果两个前台同时操作同一笔订单就可能出现重复办理入住。解决办法是在更新SQL里也带上状态条件比如UPDATE reservation_order SET status 2 WHERE id ? AND status 1再加一次行数判断。这种“以状态条件做乐观锁”的思路在这个项目里到处都用得上。5. 定时任务与数据统计让平台看上去完整5.1 超时未支付订单自动取消一个只有增删改查的平台答辩时很难出彩。加一个定时任务立刻显得系统“活”了。我加的是创建订单后30分钟内未支付自动取消并释放房间。实现方式非常简单Spring Boot自带的Scheduled就够了Component public class OrderTimeoutTask { Scheduled(cron 0 0/1 * * * ?) public void cancelTimeoutOrders() { ListReservationOrder orders orderMapper.selectTimeoutOrders(30); for (ReservationOrder order : orders) { orderCancelService.cancelBySystem(order.getId()); } } }对应的SQL逻辑是查出所有处于“待支付”状态、创建时间距今超过30分钟的订单。然后逐个调用取消服务取消服务内部做两件事订单状态改成5已过期房间状态从1改回0这两个操作同样在同一个事务里。这里有一个容易被忽略的细节定时任务里的业务逻辑一定要抽到Service里不要在Task类里直接写SQL。原因很现实——定时任务跑的时候很难排查问题把业务抽出来我可以在Controller里临时加一个手动触发接口测试的时候就方便多了不然每次想测都要等下一分钟。5.2 经营日报给答辩评委一个“你考虑过”的加分项统计模块我做了两个数据今日入住率和近七日营收。统计逻辑不复杂一条SQL就能搞定SELECT COUNT(DISTINCT room_id) AS occupied_rooms, COUNT(*) AS total_rooms FROM room;营收统计按订单表聚合SELECT DATE(create_time) AS day, SUM(total_price) AS revenue FROM reservation_order WHERE status 3 AND create_time DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time);做统计模块最关键的不是SQL本身而是控制好查询粒度。如果每次打开首页都实时统计数据量大了会卡。我的做法是每天凌晨用一个Scheduled任务把前一天的经营数据汇总到一张daily_summary表首页只查这张表。这个设计体现了基本的“空间换时间”思想答辩时主动讲出来评委是认可的。6. 实测踩坑完成期比开发期更值得记录6.1 Spring Boot 3.x 全家桶迁移的痛一开始我图新选的是Spring Boot 3.2.0结果第一周就给我上了一课。首先javax.servlet全部变成jakarta.servlet网上下载的许多工具类直接不能用了。然后连接Redis的jedis客户端和Spring Boot 3.2的自动配置版本冲突报了一堆看不懂的Bean创建异常。最后实在受不了退回2.7.18整个世界清静了。所以我的建议是如果你对生态不熟老老实实选2.7.x。技术选型从来不是越新越好而是在限定时间里风险最低的方案最合适。6.2 自动建表配置引发的连环问题我最初为了偷懒在MyBatis Plus里配置了表不存在自动建表的功能结果字段类型、长度和手动设计的SQL完全不匹配。比如字段在数据库里是datetime自动建表建出来可能是timestamp导致时间精度和默认值行为全部不一样查出来的数据和自己预想的对不上。排查了很久才发现是这个隐形配置在捣鬼。最终的解决办法是手工维护一份schema.sql每次环境重建都手动执行关闭所有自动建表功能。我见过很多同学在这个问题上耗掉大量时间其实一开始就用SQL文件控制数据库结构是最稳妥、也最方便答辩展示的做法。6.3 LocalDateTime 在前端炸成数字数组前后端联调的时候发现接口返回的日期字段是一串数组比如[2025, 6, 1, 12, 0, 0]。原因很简单Jackson默认序列化LocalDateTime的方式不是字符串。我一开始是在每个字段上加JsonFormat加了几十个之后实在受不了直接在配置文件里全局指定spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8加上之后返回给前端的日期就正常了。这个坑非常典型凡是用了LocalDate/LocalDateTime的项目都会遇到提前配好能省一天时间。6.4 事务失效订单状态和房间状态不一致这是我整个项目里排得最久的一个bug。某天测试发现订单取消了房间状态没有恢复又或者订单办入住了房间还是“已预订”。检查Log发现是事务没有回滚。原因非常经典——同一个类内部方法自调用导致Transactional被绕过。也就是说我在一个Service方法里直接调了同类里的另一个带事务的方法Spring的代理机制根本拦不到这次调用事务自然就失效了。解决办法是拆开把事务方法放到不同的Service里通过注入的方式调用。这个知识点太典型了稍微有经验的面试官一定会问你我后来在答辩PPT里也专门写了一页讲这个问题给自己加了不少印象分。6.5 跨域配置前端联调第一道坎如果你用Vue写前端那跨域问题基本避免不了。我在WebMvcConfigurer里加了全局CORS配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true); } }注意这里用allowedOriginPatterns而不是allowedOrigins因为allowCredentials(true)时allowedOrigins(*)在某些浏览器和框架组合下会被拒绝。这个细节网上很多教程都没提但实际联调时几乎都会遇到。整个项目做完再回头看真正让我成长的不是会用几个注解而是想清楚了“房间状态”和“订单状态”这两条线的流转关系。数据库设计的核心其实不是字段够不够全而是状态是否可控、并发是否安全、事务是否闭环。如果再来一次我会在写第一行代码之前先把状态流转图画在纸上然后用一个下午把表结构定死后面所有的开发都会顺畅很多。本文还有配套的精品资源点击获取
返回列表