基于Spring Boot与MySQL的酒店客房管理系统数据库设计与实现

发布时间:2026/10/31 17:54:24
基于Spring Boot与MySQL的酒店客房管理系统数据库设计与实现

简介:这份资源是面向高校计算机相关专业学生的数据库课程设计参考包,聚焦酒店管理系统中的客房管理模块,涵盖客房、客户、订单、入住与退房记录等核心实体的数据建模,以及基于关系型数据库的表结构设计、主外键约束与完整性维护,适合正在完成课程设计或需要理解数据库技术如何落地于实际业务场景的学习者。压缩包共30个文件,以18个class编译文件与10个java源码为主,另附2张jpg图片,整体约404KB,源码可直接导入IDE运行调试,便于对照理解预订、入住、查询等功能的实现逻辑。目前已有1877人学习下载,读者可从中获得从需求分析、表结构设计到SQL增删改查与前端界面联动的完整实践思路,并参考测试场景与安全性考量,快速搭建自己的课程设计框架。

1. 从一张房态表说起:这个酒店客房管理系统到底能干什么

做过数据库课程设计的人大概都有过这种体验:需求文档写得像模像样,真到建表的时候,客房、订单、入住记录三张表的关系就开始打架,改一处牵动三处。这个酒店管理系统(客房管理)就是冲着这个痛点来的——它把「房态」当成整个系统的核心状态机,围绕房间从空闲、预订、入住、退房到清洁的流转,把客户、订单、账单、员工几条线串起来。适合正在做数据库课设、想找一个结构完整又能跑通的参考项目的同学,也适合想拿它当模板改造成民宿、公寓管理系统的开发者。整套东西不追求花哨,重点在表结构设计和 SQL 的落地,属于那种「看得懂、跑得起来、改得动」的类型。

2. 技术栈与目录结构:先看清这套系统由哪些部分组成

2.1 技术选型:为什么是这套组合

这类课程设计项目,技术栈通常不会太激进,核心诉求是「数据库能体现设计功底,界面能演示业务流程」。常见的做法是后端用 Java(Spring Boot)或 Python(Flask/Django)配 MySQL,前端用简单的 HTML + 模板引擎或者 Vue 单页。我一般会推荐 Spring Boot + MyBatis + MySQL 这套,原因很实际:MyBatis 的 XML 里能直接写复杂 SQL,客房查询、订单联表、账单统计这些操作写起来直观,答辩时老师一眼能看懂你的 SQL 功底;而 Spring Boot 的自动配置省掉了大量环境搭建时间,把精力留给数据库设计本身。

数据库选 MySQL 8.x,字符集用 utf8mb4,存储引擎 InnoDB,这样外键约束和事务都能用上——客房预订和入住涉及并发改房态,事务是绕不开的。前端如果不想折腾,Thymeleaf 模板足够;想界面好看点,Vue3 + Element Plus 也行,但要注意别把课设做成前端秀,重点还是表结构和业务逻辑。

2.2 目录结构:一份能照着建的工程骨架

下面是我按这个项目场景整理的典型目录,实际拿到源码时结构可能略有出入,但模块划分基本一致:

hotel-management/ ├── src/main/java/com/hotel/ │ ├── controller/ # 接口层:房间、订单、客户、账单 │ │ ├── RoomController.java │ │ ├── OrderController.java │ │ └── BillController.java │ ├── service/ # 业务层:房态流转、订单校验 │ │ ├── RoomService.java │ │ └── OrderService.java │ ├── mapper/ # 数据访问层(MyBatis 接口) │ │ ├── RoomMapper.java │ │ └── OrderMapper.java │ ├── entity/ # 实体类,与表一一对应 │ │ ├── Room.java │ │ ├── Customer.java │ │ └── Order.java │ └── config/ # 数据源、事务、拦截器配置 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML,复杂 SQL 都在这 │ │ ├── RoomMapper.xml │ │ └── OrderMapper.xml │ ├── static/ # 前端静态资源 │ ├── templates/ # 页面模板 │ └── application.yml # 数据库连接、端口配置 ├── sql/ │ ├── schema.sql # 建表语句 │ └── data.sql # 初始化测试数据 └── pom.xml # Maven 依赖

这个结构的关键在于sql/目录单独放建表和初始化脚本,答辩演示时直接执行schema.sql就能把库建起来,比手动在 Navicat 里点半天靠谱得多。mapper/下 XML 和接口分离,是 MyBatis 的标准玩法,复杂查询写在 XML 里,简单 CRUD 用注解也行。

2.3 核心表设计:五张表撑起整个业务

客房管理系统的表不用多,但关系要理清。核心是这几张:

表名作用关键字段
room客房信息room_id, room_no, room_type, price, status
customer客户信息customer_id, name, phone, id_card
orders订单/预订order_id, customer_id, room_id, check_in, check_out, status
bill账单bill_id, order_id, amount, pay_status
room_type房型字典type_id, type_name, base_price

room.status是整套系统的灵魂字段,用枚举值表示房态:0 空闲、1 已预订、2 已入住、3 清洁中、4 维修。所有业务操作本质上都是在改这个字段,改之前必须校验当前状态是否允许这次流转——比如「已入住」的房间不能再被预订,这就是后面要讲的状态机校验。

3. 核心模块拆解:房态流转、订单校验与账单生成

3.1 建表脚本:把约束写进 DDL 里

先看建表,约束能交给数据库的就别交给代码,这是血泪经验。下面这段是room和orders的核心 DDL:

CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE COMMENT '房间号,如 801', room_type INT NOT NULL COMMENT '房型ID,关联 room_type', price DECIMAL(10,2) NOT NULL COMMENT '当前房价', status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲 1已预订 2已入住 3清洁 4维修', floor INT COMMENT '楼层', CONSTRAINT fk_room_type FOREIGN KEY (room_type) REFERENCES room_type(type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, room_id INT NOT NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待确认 1已确认 2已入住 3已完成 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id), CONSTRAINT fk_order_room FOREIGN KEY (room_id) REFERENCES room(room_id), CONSTRAINT chk_date CHECK (check_out > check_in) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个细节值得说。room_no加了 UNIQUE,房间号重复是低级错误但真有人犯;chk_date这个 CHECK 约束保证退房日期晚于入住日期,MySQL 8.0.16 之后才真正生效,老版本会忽略,这点要注意。status用 TINYINT 而不是 VARCHAR,省空间且查询快,但代价是可读性差,所以注释必须写清楚每个值的含义——不然过两周自己都忘了 3 是清洁还是维修。

3.2 房态流转:一个方法管住所有状态变更

房态变更如果散落在各个 Controller 里,迟早出乱子。我一般会收口到一个 Service 方法,用状态机的方式校验:

@Service public class RoomService { @Autowired private RoomMapper roomMapper; // 允许的状态流转:当前状态 -> 可流转到的目标状态集合 private static final Map<Integer, Set<Integer>> TRANSITIONS = Map.of( 0, Set.of(1, 2, 4), // 空闲 -> 可预订/直接入住/维修 1, Set.of(2, 0), // 已预订 -> 入住/取消回空闲 2, Set.of(3), // 已入住 -> 退房后清洁 3, Set.of(0), // 清洁完成 -> 空闲 4, Set.of(0) // 维修完成 -> 空闲 ); @Transactional public void changeStatus(Integer roomId, int targetStatus) { Room room = roomMapper.selectById(roomId); if (room == null) { throw new BizException("房间不存在"); } Set<Integer> allowed = TRANSITIONS.getOrDefault(room.getStatus(), Set.of()); if (!allowed.contains(targetStatus)) { throw new BizException( String.format("房态非法流转:%d -> %d", room.getStatus(), targetStatus)); } roomMapper.updateStatus(roomId, targetStatus); } }

这段代码的价值在于把「哪些流转合法」显式定义成一张表,而不是写一堆 if-else。TRANSITIONS用Map.of声明,读起来一目了然:空闲房可以直接入住(散客),也可以先预订;已入住的房只能退房进清洁,不能直接回空闲——因为中间必须经过打扫。@Transactional保证查状态和改状态在同一个事务里,避免并发下两个请求同时把一间房改成不同状态。参数targetStatus传目标状态值,方法内部负责校验,调用方不用关心规则。

3.3 订单校验:防止同一间房被重复预订

预订环节最容易翻车的是日期重叠。两个订单订同一间房,日期有交集就是冲突。SQL 层面这样查:

<!-- OrderMapper.xml:查询指定房间在给定日期区间内的有效订单数 --> <select id="countConflictOrders" resultType="int"> SELECT COUNT(*) FROM orders WHERE room_id = #{roomId} AND status IN (0, 1, 2) <!-- 待确认、已确认、已入住都算占用 --> AND check_in &lt; #{checkOut} <!-- 已有订单的入住日 < 新订单退房日 --> AND check_out &gt; #{checkIn} <!-- 已有订单的退房日 > 新订单入住日 --> </select>

日期重叠的判断逻辑是「区间相交」:两个区间[a1, a2]和[b1, b2]相交的条件是a1 < b2 AND a2 > b1。这里用严格小于和大于,是因为退房当天房间可以再入住,不算冲突。status IN (0,1,2)排除了已取消和已完成的订单,这两个状态不占用房间。Service 层拿到冲突数大于 0 就直接拒绝下单,并提示具体冲突日期,用户体验会好很多。

3.4 账单生成:退房时一次算清

退房时生成账单,涉及房费、可能的加时费、押金退还。核心计算逻辑:

public Bill generateBill(Order order) { Room room = roomMapper.selectById(order.getRoomId()); // 按实际入住天数计费,退房当天不计 long days = ChronoUnit.DAYS.between(order.getCheckIn(), order.getCheckOut()); if (days <= 0) days = 1; // 钟点房或当天退房按 1 天算 BigDecimal roomFee = room.getPrice().multiply(BigDecimal.valueOf(days)); Bill bill = new Bill(); bill.setOrderId(order.getOrderId()); bill.setAmount(roomFee); bill.setPayStatus(0); // 未支付 billMapper.insert(bill); return bill; }

ChronoUnit.DAYS.between算天数比手动减日期靠谱,跨月跨年都不会错。days <= 0的兜底是给当天入住当天退的情况准备的,不然账单金额为 0 就闹笑话了。金额用BigDecimal而不是 double,这是财务计算的铁律,浮点误差在账单上会被放大。

4. 跑起来:环境准备、建库与启动验证

4.1 环境清单与版本对齐

动手之前先把版本对齐,版本不匹配是新手最常见的翻车点。需要准备:JDK 17(Spring Boot 3.x 要求)、Maven 3.8+、MySQL 8.0+。如果源码是 Spring Boot 2.x,JDK 8 或 11 也能跑,看pom.xml里的spring-boot-starter-parent版本决定。MySQL 一定要 8.0 以上,前面提到的 CHECK 约束和 utf8mb4 默认排序规则都依赖这个版本。

4.2 建库与初始化数据

拿到源码后第一步不是急着启动,而是先把库建好:

# 登录 MySQL mysql -u root -p # 创建数据库,指定字符集 CREATE DATABASE hotel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 执行建表和初始化脚本 mysql -u root -p hotel_db < sql/schema.sql mysql -u root -p hotel_db < sql/data.sql

执行完用SHOW TABLES;确认五张表都在,再SELECT * FROM room LIMIT 5;看看测试数据有没有进去。如果data.sql报外键错误,多半是插入顺序不对——先插room_type和customer,再插room和orders,被引用的表必须先有数据。

4.3 改配置与启动

打开application.yml,把数据库连接改成自己的:

spring: datasource: url: jdbc:mysql://localhost:3306/hotel_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver server: port: 8080

serverTimezone=Asia/Shanghai这个参数别漏,不然日期字段可能差 8 小时,订单日期对不上会让你怀疑人生。改完执行mvn spring-boot:run或直接跑主类,控制台看到Started ... in x seconds就说明起来了。浏览器访问http://localhost:8080,能看到房间列表页就算跑通。

4.4 验证核心流程

跑通不等于跑对,按这条链路走一遍:新增客户 → 选一间空闲房下单预订 → 房态变「已预订」 → 办理入住 → 房态变「已入住」 → 退房 → 生成账单、房态变「清洁」 → 清洁完成回「空闲」。每一步都去数据库SELECT status FROM room WHERE room_id = ?核对,状态对得上才算真跑通。中间任何一步状态没变,先看 Service 有没有被正确调用,再看事务有没有回滚。

5. 避坑与排查:那些让我返工的坑

5.1 房态改了但页面没变

现象:退房操作后数据库里 status 已经是 3,但列表页还显示「已入住」。原因通常是前端缓存了旧数据,或者查询接口没加ORDER BY导致分页错乱。解决:接口返回前强制刷新,前端请求加时间戳参数绕过缓存;分页查询统一按room_no排序,保证结果稳定。

5.2 并发预订导致超卖

现象:两个人同时订同一间房,都成功了。原因:先查冲突再插入,两步之间没有锁。解决:在countConflictOrders查询上加FOR UPDATE行锁,或者给orders表加唯一索引兜底(同一房间同一入住日唯一),让数据库来拦。课设演示并发场景不多,但答辩老师爱问这个。

5.3 日期差一天

现象:1 号入住 3 号退房,账单算成 3 天。原因:用了check_out - check_in + 1的算法,把退房当天也算进去了。解决:退房当天不计费,直接用ChronoUnit.DAYS.between,不加 1。这个坑我在两个项目里都踩过,后来统一封装成工具方法。

5.4 外键导致删数据失败

现象:想删一个客户,报外键约束错误。原因:该客户还有关联订单。解决:这是设计意图,不该硬删。业务上做软删除,给customer加is_deleted字段,查询时过滤;真要物理删除,先处理关联订单。

5.5 中文乱码

现象:客户姓名存进去变成问号。原因:数据库、连接、表三处字符集不一致。解决:建库用 utf8mb4,连接串加characterEncoding=utf8,表也确认是 utf8mb4。三处对齐,乱码自然消失。

6. 二次开发:从课设到能用的改造思路

拿到这套代码,多数人不会止步于演示,总想加点东西让它更像回事。我按改造价值排个序,从易到难说。

第一层是补全基础功能。原项目通常只有房间和订单,可以加「员工管理」和「操作日志」两张表,员工表记录前台、保洁角色,日志表记录每次房态变更的操作人和时间。日志表结构简单:log_id, room_id, from_status, to_status, operator, op_time,在changeStatus方法里插一条就行。这一层改动小、收益大,答辩时能讲「可追溯」。

第二层是优化查询。房间列表页如果数据量上来,SELECT *加全表扫描会明显变慢。给room(status)和orders(room_id, check_in, check_out)加联合索引,查询计划立刻不一样。可以用EXPLAIN对比加索引前后的type字段,从ALL变成ref就是效果。这个演示在答辩里很加分。

第三层是引入缓存或定时任务。房态是高频读低频写的数据,用 Redis 缓存房间列表,写操作时删缓存,能明显降低数据库压力。再配一个定时任务,每天凌晨把「清洁中」超过 24 小时的房间自动置为「空闲」,避免保洁忘了点完成导致房间一直不可用。定时任务用 Spring 的@Scheduled就够,cron 表达式0 0 2 * * ?表示每天凌晨 2 点执行。

改造完怎么验证没改坏?我的习惯是保留一份原始schema.sql和data.sql,每次改完重建库跑一遍完整流程,再对比关键表的记录数和状态分布。另外给核心 Service 方法补几个单元测试,尤其是changeStatus的非法流转用例,改状态机时能第一时间发现回归。

说个我自己的教训:早期做这类系统,我总想一步到位把表设计得很全,结果字段加了一堆,业务逻辑根本用不上,反而让关联查询变复杂。后来我改成「先跑通最小闭环,再按需加表」,每次加字段前先问自己「哪个页面或哪个操作会用到它」,用不上就不加。从那以后我每次动表结构前都强制走一遍这个自问,省了不少返工。希望这套拆解能帮你把课设顺利跑起来,也留出足够的改造空间。

本文还有配套的精品资源,点击获取