ARTICLE DETAIL

资讯详情

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

Spring Boot酒店管理系统毕业设计:状态机建模与并发避坑实战

Spring Boot酒店管理系统毕业设计:状态机建模与并发避坑实战 简介面向高校毕业设计学生和初学 Spring Boot 的开发者该酒店管理系统项目包内含可直接运行的完整源代码与配套毕业论文聚焦客户信息管理、房间管理、订单管理等典型业务模块覆盖房间预订、客户信息录入与查询等常用操作。代码结构清晰、注释较丰富模块边界明确既便于阅读、维护和二次开发也能支撑后续功能扩展与业务调整。压缩包为 RAR 格式整体约 22MB除源码外还包括论文文档论文部分系统阐述项目背景、设计思路、技术选型、实现过程与测试结果并结合系统各模块交代了核心功能的实现方式对理解完整 Web 项目开发流程有直接帮助。资源已有 417 人浏览学习适合需要快速掌握 Spring Boot 实战技能或希望借鉴完整毕设项目与论文结构的同学。1. 为什么说基于 Spring Boot 的酒店管理系统是最适合毕业设计的闭环业务拿到“酒店管理系统”这个 Java 毕业设计题目的第一反应很多同学都会想这不过又是一套增删改查。真正动手做的人才会发现这个题目最值钱的地方根本不在接口数量而在房态与订单状态之间那套状态机。房间什么时候可订、什么时候不能释放订单从创建到结算经历了哪些状态这些规则一旦理清楚论文的核心章节自然就立住了。基于 Spring Boot 开发是因为它能让一个学生在一个学期内同时交付可用系统、源码和论文生态成熟、资料多、面试时还能拿出来讲。这篇笔记写给正在做或正准备做这个题目的人按“建模→开发→避坑→交付→答辩”的顺序走一遍落地过程。2. 先建模再写代码酒店管理系统的领域模型与数据库设计2.1 房间与订单的状态机业务规则先于代码我接触过不少酒店管理类的毕设源码最典型的问题不是代码写得烂而是业务上根本没有“规则”。房间状态是一个字符串字段订单状态是另一个字符串字段两个字段互相之间没有任何约束。评审老师问一句“已退房的房间为什么不能马上被预订”答案就卡壳了。正确做法是在写接口之前先把状态机画出来。房间里至少有五种状态空闲、已预订、入住中、打扫中、维修停用。订单至少有五种状态待支付、已支付、已入住、已退房、已取消。状态和状态之间的转换路径必须事先定死比如“已预订”只能由“空闲”过来不能从“入住中”跳过去。把这些状态定义成 Java 枚举比到处写字符串常量省心得多public enum RoomStatus { AVAILABLE(空闲), BOOKED(已预订), OCCUPIED(入住中), CLEANING(打扫中), MAINTENANCE(维修停用); private final String desc; RoomStatus(String desc) { this.desc desc; } public String getDesc() { return desc; } }同理订单状态对应一个 OrderStatus 枚举包含 CREATED、PAID、CHECKED_IN、CHECKED_OUT、CANCELLED 五类。为什么建议用枚举而不是直接在数据库里写字符串因为 Java 里可以借助valueOf()做合法值约束写接口时能提前感知状态是否写错毕业论文里画状态图也是把枚举的流转关系原样搬上去图和代码对得上。状态机还决定了 Service 层的方法划分。每个修改状态的动作都应该是一个独立 service 方法比如checkIn()、checkOut()、cleanRoom()不要在 Controller 里随手updateById。这样做的直接收益是事务边界清晰评审问“入住时到底改了几张表”时你能一条一条列出来。2.2 三张核心表怎么建房、单、住客记录分离数据库设计是酒店管理系统毕业论文的重头戏。我建议只保留三张核心业务表其余统计表、权限表按需再补。三张表分别是房间表 tb_room、订单表 tb_order、入住记录表 tb_checkin。把入住记录单独拆出来而不是在订单表上加两个时间字段是因为续住、换房、提前退房这些场景都需要保留历史痕迹订单状态变了入住记录不能跟着丢。初始化 SQL 可以这样写CREATE TABLE tb_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_number VARCHAR(10) NOT NULL, room_type VARCHAR(20) NOT NULL, floor INT NOT NULL DEFAULT 1, status VARCHAR(20) NOT NULL DEFAULT AVAILABLE, daily_price DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_room_number (room_number) ); CREATE TABLE tb_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, customer_name VARCHAR(50) NOT NULL, customer_phone VARCHAR(20), room_id BIGINT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT CREATED, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_order_room_date (room_id, check_in_date, check_out_date) ); CREATE TABLE tb_checkin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, room_id BIGINT NOT NULL, customer_name VARCHAR(50) NOT NULL, check_in_time DATETIME NOT NULL, check_out_time DATETIME NULL, actual_amount DECIMAL(10,2) NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_checkin_room (room_id) );这里有几个参数值得说道价格字段一律用DECIMAL(10,2)不用 double日期只精确到天所以 check_in_date、check_out_date 用 DATE 类型DATETIME 留给真正的“时间点”订单号加唯一索引但不用自增主键当订单号因为订单号要展示给客户自增 id 会暴露酒店一天的订单量这在论文的“安全性设计”里也能补一笔。Java 侧对应的实体类不需要写复杂注解普通 POJO 配合 MyBatis 的驼峰映射就够了。注意 status 字段在实体里建议用 String 存储查出来之后在 Service 层用RoomStatus.valueOf(room.getStatus())转换这样既不用写 TypeHandler又能享受枚举带来的类型约束。2.3 代码结构选型按业务模块分包比按技术层分包好写论文Spring Boot 项目的代码组织方式直接影响论文架构图。常见做法是按 controller、service、mapper 三层分包但对毕设来说这种结构有个缺点零散的类分布在三个包里论文写“模块设计”时东拉西扯评审不好核对。我更推荐按业务模块分包目录结构长这样src/main/java/com/example/hotel/ ├── HotelApplication.java ├── common/ │ ├── Result.java │ ├── BizException.java │ └── GlobalExceptionHandler.java ├── config/ │ ├── CorsConfig.java │ └── MyBatisConfig.java ├── module/ │ ├── room/ │ │ ├── Room.java │ │ ├── RoomMapper.java │ │ ├── RoomService.java │ │ └── RoomController.java │ ├── order/ │ │ ├── Order.java │ │ ├── OrderMapper.java │ │ ├── OrderService.java │ │ └── OrderController.java │ ├── checkin/ │ └── report/ └── resources/ ├── application.yml ├── mapper/ │ ├── RoomMapper.xml │ └── OrderMapper.xml └── db/ └── init.sqlmodule 下面按业务再分 room、order、checkin、report每个包内部仍然保持 controller-service-mapper 三层。这样论文里的“系统实现”章节可以直接照着 module 目录逐个模块写每个小节对应一个包做不到前后不一。另外 MyBatis 的 Mapper 接口与 XML 文件路径要一致MapperScan扫描的包和mapper-locations配置别写重了这也是新人最容易踩的项目启动失败点。3. 用 Spring Boot 把核心接口跑通工程配置、事务边界与分页插件3.1 最小可用工程pom.xml 与 application.yml 的关键配置酒店管理系统作为毕设依赖不需要多够用就行。我常用的组合是 Spring Boot 2.7 系 MyBatis PageHelper Lombok这套组合在 JDK 8 和 JDK 11 下都很稳。如果你的本机是 JDK 17建议直接切到 Spring Boot 3.xMyBatis 用 mybatis-spring-boot-starter 3.0 以上的版本否则启动会报模块访问错误。pom.xml 核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies数据库连接配置放在 application.yml 中参数要区分开“本地连接串”和“部署连接串”不要每次换机器改代码。以下是本地开发最常用的一套配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true几个关键参数说明一下。serverTimezoneAsia/Shanghai不加的话MySQL 8 的 JDBC 驱动在多数机器上会报时区错误map-underscore-to-camel-case让数据库的 room_number 自动映射成实体里的 roomNumber这个是必开的不然每张表都要手写 resultMapjackson 的 date-format 决定接口返回的 LocalDateTime 是“2025-01-01 12:30:00”而不是“2025-01-01T12:30:00”前端拿到后能直接展示。3.2 预订房间的接口并发才是酒店系统的真正难点预订接口是整张嘴的核心也是论文里最能体现设计能力的地方。很多毕设代码是先select查一下房间状态发现是 AVAILABLE再执行update改成 BOOKED。这段代码单独看没错但在并发场景下会翻车两个用户同时预订同一间房都先查到了“空闲”然后先后更新结果两个订单同时挂在了一间房上。解决这个问题不一定要用 Redis数据库层面加一把悲观锁就够了。Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateRequest req) { Room room roomMapper.selectByIdForUpdate(req.getRoomId()); if (room null) { throw new BizException(房间不存在); } if (!RoomStatus.AVAILABLE.name().equals(room.getStatus())) { throw new BizException(房间状态不可预订 room.getStatus()); } int conflict orderMapper.countDateConflict( req.getRoomId(), req.getCheckInDate(), req.getCheckOutDate()); if (conflict 0) { throw new BizException(该时间段已有订单请换房或改期); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setCustomerName(req.getCustomerName()); order.setCustomerPhone(req.getCustomerPhone()); order.setRoomId(req.getRoomId()); order.setCheckInDate(req.getCheckInDate()); order.setCheckOutDate(req.getCheckOutDate()); order.setStatus(OrderStatus.CREATED.name()); long nights req.getCheckOutDate().toEpochDay() - req.getCheckInDate().toEpochDay(); order.setTotalAmount(room.getDailyPrice().multiply(BigDecimal.valueOf(nights))); orderMapper.insert(order); roomMapper.updateStatus(req.getRoomId(), RoomStatus.BOOKED.name()); return order; }对应的 SQL 必须用FOR UPDATE加行锁而不是普通 selectselect idselectByIdForUpdate resultTypecom.example.hotel.module.room.Room SELECT id, room_number, room_type, floor, status, daily_price, create_time FROM tb_room WHERE id #{id} FOR UPDATE /select这里有两个要点。一是Transactional必须加在 createOrder 方法上锁是在方法事务里持有的如果事务没提交就释放锁那FOR UPDATE形同虚设。二是订单金额的计算用toEpochDay()差值得出住宿晚数DECIMAL金额参与乘法时用 BigDecimal避免 fload 的二进制精度问题。3.3 订单列表分页MyBatis 分页插件的标准用法酒店管理系统后台要展示订单列表几乎都是分页查询MyBatis 分页插件 PageHelper 是这里最常用的方案它的核心用法非常固定。先封一个查询参数对象再在调用 Mapper 方法之前启动分页public PageResultOrderVO pageOrders(OrderQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListOrderVO list orderMapper.selectOrderPage(query); PageInfoOrderVO pageInfo new PageInfo(list); return PageResult.of(pageInfo); }Mapper 里的动态查询 SQL 长这样select idselectOrderPage resultTypecom.example.hotel.module.order.OrderVO SELECT o.id, o.order_no, o.customer_name, o.customer_phone, o.check_in_date, o.check_out_date, o.total_amount, o.status, r.room_number, r.room_type FROM tb_order o LEFT JOIN tb_room r ON o.room_id r.id where if testcustomerName ! null and customerName ! AND o.customer_name LIKE CONCAT(%, #{customerName}, %) /if if teststatus ! null and status ! AND o.status #{status} /if if teststartDate ! null AND o.check_in_date gt; #{startDate} /if /where ORDER BY o.create_time DESC /selectPageHelper 的工作原理是拦截紧接着的这条 select自动生成 count 查询和 limit 拼接。三个使用习惯很重要startPage之后必须紧跟一条查询中间不能穿插其他数据操作不要在循环体内反复调用startPage第二次会把前一次的分页效果覆盖掉排序字段要写在查询语句里不要依赖分页插件去处理。XML 中gt;是的转义写法这个很容易被忽略写错会出现 MyBatis 解析异常。4. 酒店管理系统避坑指南5 个最容易翻车的问题4.1 重复预订并发下两个线程同时读到了可用房间现象同一个房间在很短时间被创建了两笔订单状态都是待支付一张房态表被更新了两次。原因代码写成“先查状态再更新状态”两次查询之间没有锁保护。Java 层面这是典型的 check-then-act 竞态问题。解决我在 3.2 里给出的 select for update 就是标准解法。把“查房间”和“改房间状态”放在同一个事务里数据库行锁保证第二个事务拿到锁后看到的是 BOOKED 而不是 AVAILABLE。如果不想用行锁也可以改成UPDATE tb_room SET status BOOKED WHERE id ? AND status AVAILABLE后判断影响行数影响行数为 0 就说明房间已经被占用但这种做法在判断日期冲突时不够直观。4.2 入住天数永远差一天日期边界没定清楚现象客户 12 月 1 日入住、12 月 3 日退房订单金额按每晚 300 元算是 900 元实际算出来只有 600 元。原因按照日历天计算12 月 1 日到 12 月 3 日确实是 2 天但酒店行业按床位晚计费这叫“三晚”。代码里直接做日期差没有加一。解决所有涉及按天计费的地方统一口径为“含首不含尾”或“含头不含尾”先定规则再写代码。比如 order 创建时用checkOutDate.toEpochDay() - checkInDate.toEpochDay()得到的差值就是晚数不需要加一但退房时间在中午 12 点前、下午 6 点前、晚上 6 点后要按不同的计费规则另算这种边界只写在文档里不够要写成一个独立的calculateNights()工具方法配套单元测试。4.3 金额用 double 计算结算单出现 0.30000000000000004现象订单总金额有时会出现一长串小数比如 0.30000000000000004前端展示十分尴尬。原因Java 的 double 和 float 采用二进制浮点表示0.1、0.2 这类十进制小数无法精确表达。数据库里虽然存了 DECIMAL但代码里如果先转成 double 再乘法精度就会丢。解决金额参与运算时统一用 BigDecimal构造 BigDecimal 时不要直接传 double应该用new BigDecimal(268.00)或BigDecimal.valueOf(268.00)。数据库层的 daily_price、total_amount、actual_amount 全部用 DECIMAL(10,2)避免 JDBC 驱动做隐式转换。最后入库时再setScale(2, RoundingMode.HALF_UP)统一四舍五入。4.4 退房后房间秒变“空闲”保洁流程被跳过现象客人退房后房态立刻变成 AVAILABLE下一单可以直接预订这一间但房间根本没打扫。原因退房接口只改了 room 表的 status没有引入“打扫中”这个中间状态。这在真实酒店流程里不可能发生——客房没有经过保洁确认前台是不允许把它标记为可售房的。解决在退房事件中把房态改成 CLEANING 而不是 AVAILABLE。新增一个保洁完成的接口只有当保洁员确认后CLEANING 才流转为 AVAILABLE。顺带在订单表上可以加一个cleaned_time字段记录时间论文里作为“业务流程完整性”的论据。4.5 前端联调跨域报错日期格式又是一坨现象Vue 或微信小程序调接口时浏览器报 CORS 错误后端返回的时间是 2025-01-01T12:00:00前端解析后显示格式不符合中文习惯。原因前后端分离开发时后端没有处理跨域Spring Boot 默认不允许跨域请求LocalDateTime 被 Jackson 默认序列化成 ISO 格式。解决写一个配置类实现 WebMvcConfigurer注册跨域规则同时在 application.yml 里配置 jackson 的 date-format 和 time-zone。跨域配置里要允许 GET、POST、PUT、DELETE 四个方法并支持 OPTIONS 预检请求。如果后续部署到同一域下就不需要跨域配置所以这个类可以加上Profile(dev)只在本地方生效。5. 论文、源码和答辩怎么把 Spring Boot 毕设交付到位5.1 论文章节与代码模块的对应关系毕设论文最怕写成一本文档说明书章章节节都在描述界面怎么点评审看十页也看不到与代码的对应关系。我建议按下面这种对应关系组织整本论文论文章节对应代码位置评审重点需求分析module/order 的用例场景有没有画出房态状态图、订单状态图系统设计resources/db/init.sqlER 图里三张表关联关系是否一致系统实现-预订模块OrderService.createOrder事务注解、并发锁、金额计算系统实现-房态管理RoomService 状态流转方法打扫中状态是否被体现系统测试src/test 下的核心用例是否覆盖重复预订、日期边界、退房流程我见过很多同学把论文里的功能列表写得很丰满比如“支持会员积分兑换”“支持多酒店连锁”代码里却找不到对应实现。评审老师随机点一个功能让现场演示翻车的是最常见的情况。所以论文里出现的每一个功能代码必须能跑通代码里已经写好的功能论文必须有所交代。宁可砍掉没做完的功能也不要扩写没做的功能。5.2 源码交付让拿项目的人能一次跑通交付毕设源码不是把一个 zip 压缩包丢过去就完事收源码的人多半是下一届学弟或者评审老师他们本地环境和你不一样。一个完整的源码交付目录至少要有工程源码、SQL 初始化脚本、README 和使用说明。README 要写清楚 JDK 版本、Maven 版本、MySQL 版本以及数据库初始化步骤。我习惯把配置文件按环境拆开application.yml 放公共配置application-local.yml 放本地数据库账号application-prod.yml 放服务器部署配置。系统里涉及文件上传之类的路径也不要写死用配置项引用。启动命令固定为mvn clean package -DskipTests java -jar target/hotel-0.0.1-SNAPSHOT.jar一个常见的交付教训是只给一个编译好的 jar 包不附带工程源码。有人想从 jar 反编译出项目来学习反编译工具确实能把 class 还原成 Java 源码但注解、注释、resources 目录里的配置会被剥离或错位反编译结果很难编译回去更不能当作毕设源码提交。正确做法是交付完整源码工程jar 只是让你验证运行结果用的。反编译这条路只适合应急不适合作为交付手段。5.3 答辩被追问最多的 3 个技术问题毕业答辩的问题本质上是面向 Java 基础知识的面试。很多学校答辩老师会直接拿代码里的某个点变成 java 面试题来问提前准备几个高频问题能省下很多临场压力。第一个问题Transactional 加在 private 方法上为什么不生效答案Spring 通过代理实现事务private 方法不走代理自然拿不到事务管理器。第二个问题数据库的 DECIMAL 和 Java 的 BigDecimal 什么关系答案DECIMAL 是数据库精确小数类型BigDecimal 是 Java 侧的对等类型double 会产生二进制浮点误差所以金额数据必须用这对组合。第三个问题如何防止房间被并发重复预订答案在事务内用 select for update 对房间行加锁或者用 update where status 的乐观锁写法核心是把 check 和 act 合成一个原子操作。6. 收尾技巧把演示数据固化成可重复回归的环境快到答辩那几天我遇到最多的情况是系统明明昨天跑得好好的今天现场演示时数据库被测试数据搞乱了某个房间状态停在“已入住”重新演示要手动改好几张表。解决这个问题有个笨但有效的方法写一个可重复执行的演示数据初始化脚本每次演示前重置整个数据库。DELETE FROM tb_checkin; DELETE FROM tb_order; UPDATE tb_room SET status AVAILABLE; INSERT INTO tb_room (room_number, room_type, floor, status, daily_price) VALUES (101, SINGLE, 1, AVAILABLE, 268.00), (102, SINGLE, 1, AVAILABLE, 268.00), (201, DOUBLE, 2, AVAILABLE, 388.00); INSERT INTO tb_order (order_no, customer_name, customer_phone, room_id, check_in_date, check_out_date, total_amount, status) VALUES (CONCAT(DEMO, DATE_FORMAT(NOW(), %Y%m%d%H%i%s)), 演示客人, 13800000000, 1, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 1 DAY), 268.00, CHECKED_IN); UPDATE tb_room SET status OCCUPIED WHERE id 1;这个脚本的价值不只是给演示用。每次对服务层代码做改动之后执行一遍这个脚本再跑一遍预订、入住、退房主流程数据库会回到已知状态任何因为脏数据导致的偶发问题都能快速复现。我更推荐把它再接一层自动化在 Spring Boot 的 test 环境里写一个SpringBootTest用例调用 createOrder、checkIn、checkOut用断言验证状态流转和金额字段。以后再有人改坏了逻辑一个mvn test就能发现问题不用依赖人工点点点。我自己的习惯是每次交付前先把 demo.sql 跑一遍然后把预订、入住、退房全流程截图放进论文的测试章节。这个习惯帮我挡掉过好几次现场演示的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表