简介:这是一份面向数据库课程设计学习者与初学者的酒店管理系统数据库设计案例文档,围绕总经理、财务、住宿、娱乐四个子系统展开,帮助读者理解如何将实际业务抽象为规范的数据模型。文档以中等规模酒店为背景,完整给出各子系统的功能划分、数据字典、数据结构与数据流设计,并附有职工信息表、部门信息表、收支登记表、财务汇总表、客人信息表、房间管理表、房间类别表及娱乐项目表等核心表结构,便于对照学习实体关系与字段设计思路。资源包共1个doc文件,约233KB,内容以文字方案与表格为主,适合直接阅读、摘录或作为课程设计参考模板。目前已有257人学习下载,可作为数据库原理、信息系统分析与设计等课程的配套案例,帮助读者快速掌握从需求分析到表结构落地的完整设计流程。
1. 酒店管理系统数据库设计:从入住到退房,一张表怎么串起全流程
酒店管理系统的数据库设计,核心就一句话:让“房间状态”和“客人账务”在任意时刻都对得上。前台半夜给客人换房,房态表、订单表、消费明细三处必须同步更新,漏一处第二天对账就翻车。这个案例要解决的就是这类问题——用一套可落地的表结构,把预订、入住、换房、消费、退房、夜审串成闭环。适合正在做课程设计的学生、刚接手酒店类项目的后端,以及想搞清“业务驱动表结构”到底怎么落地的开发者。数据库设计不是画完 ER 图就完事,真正难的是约束怎么加、状态怎么流转、历史怎么留痕。下面按“先立模型、再建表、后避坑”的顺序推一遍,每一步都给可抄的 SQL 和参数说明。
2. 先定业务边界:酒店管理系统和客户关系管理系统的区别在哪
2.1 别把 CRM 的字段塞进酒店库
很多人做酒店管理系统数据库设计时,习惯性把“客户画像、跟进记录、商机阶段”这些 CRM 字段一起建进来,结果表越写越臃肿。酒店管理系统和客户关系管理系统的区别,本质是交易态和关系态的区别:酒店系统关心“这间房今晚卖给谁、收了多少钱、几点退”,CRM 关心“这个客户半年内住了几次、偏好什么房型、下次怎么触达”。前者是强事务、强约束的 OLTP 场景,后者是弱事务、重分析的场景。
我的做法是:酒店库只保留guest(客人)基础身份和stay_history(入住历史)两张轻表,把偏好标签、营销触达放到独立的 CRM 库,通过guest_id关联。这样酒店库的写入路径短,夜审跑批不会被 CRM 的宽表拖慢。如果课程设计只要求单库,那也至少把 CRM 相关字段单独放一张guest_profile_ext扩展表,不要和订单主表混在一起。
2.2 核心实体与关系梳理
酒店业务的核心实体不超过八个:客人、房型、房间、订单、入住单、消费项、账务流水、操作日志。关系上,一个房型对应多个房间,一个订单可以包含多间房,一次入住对应一个房间和一个客人,消费项挂在入住单下,账务流水记录每一笔收付。
这里有个容易忽略的点:订单和入住单要分开。预订阶段生成订单,客人到店后由订单生成入住单。换房时改的是入住单的房间号,订单保持原始预订信息不变。这样既保留了预订承诺,又记录了实际入住轨迹。用order_id和stay_id两个主键分别管理,退房时按stay_id结算,按order_id统计渠道业绩。
2.3 状态机先画再建表
房态和订单状态是最容易出玄学 bug 的地方。我一般先把状态机写清楚再动手建表:
- 房间状态:
VC(空净房)、VD(空脏房)、OC(住客)、OOO(维修锁房)、OOS(停用) - 订单状态:
booked、checked_in、checked_out、cancelled、no_show - 入住单状态:
in_house、transferred、settled
状态流转必须用数据库约束或应用层事务卡死。比如VC只能由退房或清洁完成触发,OC只能由入住触发。下面建表时我会用CHECK约束把非法状态直接挡在数据库层。
3. 建表实操:八张核心表的 SQL 与参数说明
3.1 房间与房型表:把“可售”和“物理房”分开
-- 房型表:定义价格和容量,不关心具体房间号 CREATE TABLE room_type ( type_id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL COMMENT '如:大床房、双床房', base_price DECIMAL(10,2) NOT NULL COMMENT '门市价,实际售价可覆盖', max_occupancy TINYINT NOT NULL DEFAULT 2 COMMENT '最大入住人数', bed_count TINYINT NOT NULL DEFAULT 1, UNIQUE KEY uk_type_name (type_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房间表:物理房间,状态字段是房态核心 CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL COMMENT '如 8801', type_id INT NOT NULL, floor TINYINT NOT NULL, status CHAR(3) NOT NULL DEFAULT 'VC' CHECK (status IN ('VC','VD','OC','OOO','OOS')), lock_reason VARCHAR(100) DEFAULT NULL COMMENT '维修锁房原因', UNIQUE KEY uk_room_no (room_no), KEY idx_status (status), CONSTRAINT fk_room_type FOREIGN KEY (type_id) REFERENCES room_type(type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:room_type管价格和容量,room管物理房和实时状态。status用CHAR(3)而不是TINYINT,是为了日志和排查时肉眼可读,代价是索引空间略大,但房间表通常只有几百行,完全可接受。idx_status必须建,前台查“还有几间空净房”走的就是这个索引。
参数说明:base_price用DECIMAL(10,2)而不是FLOAT,金额计算不能有浮点误差。max_occupancy用TINYINT够用,酒店单房最多也就 4 人。lock_reason允许为空,维修锁房时才填。
3.2 订单与入住单:两张表管两段生命周期
-- 订单表:预订承诺,渠道来源在这里 CREATE TABLE booking_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, guest_id INT NOT NULL, channel VARCHAR(20) NOT NULL DEFAULT 'direct' COMMENT 'direct/walkin/ota/协议', check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, status VARCHAR(15) NOT NULL DEFAULT 'booked' CHECK (status IN ('booked','checked_in','checked_out','cancelled','no_show')), total_amount DECIMAL(12,2) NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_guest (guest_id), KEY idx_dates (check_in_date, check_out_date), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入住单:实际入住记录,换房改这里 CREATE TABLE stay ( stay_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, room_id INT NOT NULL, guest_id INT NOT NULL, actual_check_in DATETIME NOT NULL, actual_check_out DATETIME DEFAULT NULL, status VARCHAR(12) NOT NULL DEFAULT 'in_house' CHECK (status IN ('in_house','transferred','settled')), KEY idx_order (order_id), KEY idx_room_status (room_id, status), CONSTRAINT fk_stay_order FOREIGN KEY (order_id) REFERENCES booking_order(order_id), CONSTRAINT fk_stay_room FOREIGN KEY (room_id) REFERENCES room(room_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:booking_order记录预订信息,stay记录实际入住。换房时只更新stay.room_id并插入一条换房日志,订单表不动。idx_room_status这个联合索引很关键,夜审查“当前在住”时走它,比单列索引快一个量级。
参数说明:channel用字符串枚举而不是数字,方便直接出渠道报表。check_in_date和check_out_date用DATE而不是DATETIME,因为预订是按天算的,具体到店时间在stay.actual_check_in里记。total_amount用DECIMAL(12,2),支持连锁酒店的大额协议单。
3.3 消费与账务:每一笔钱都要能追溯到操作人
-- 消费明细:迷你吧、洗衣、赔偿等 CREATE TABLE consumption ( consume_id BIGINT PRIMARY KEY AUTO_INCREMENT, stay_id BIGINT NOT NULL, item_name VARCHAR(50) NOT NULL, amount DECIMAL(10,2) NOT NULL, qty SMALLINT NOT NULL DEFAULT 1, posted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, operator_id INT NOT NULL COMMENT '入账操作员', KEY idx_stay (stay_id), CONSTRAINT fk_consume_stay FOREIGN KEY (stay_id) REFERENCES stay(stay_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 账务流水:收、退、冲,全部留痕 CREATE TABLE folio ( folio_id BIGINT PRIMARY KEY AUTO_INCREMENT, stay_id BIGINT NOT NULL, trans_type VARCHAR(10) NOT NULL CHECK (trans_type IN ('payment','refund','adjust')), amount DECIMAL(10,2) NOT NULL COMMENT '正数收款,负数退款', pay_method VARCHAR(15) NOT NULL DEFAULT 'cash' COMMENT 'cash/card/wechat/alipay/room_charge', trans_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, operator_id INT NOT NULL, remark VARCHAR(200) DEFAULT NULL, KEY idx_stay (stay_id), CONSTRAINT fk_folio_stay FOREIGN KEY (stay_id) REFERENCES stay(stay_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:consumption记消费,folio记账务流水。退房结算时,SUM(consumption.amount)加上房费,减去SUM(folio.amount WHERE trans_type='payment'),差额就是应收。adjust类型用于冲账,不允许直接删流水,这是审计底线。
参数说明:amount在folio里允许负数,退款直接插负数记录,不要用refund类型加正数再靠应用层判断符号,那样对账时容易搞反。operator_id每张表都要有,夜审发现差异时能直接定位到人。
4. 避坑与排查:酒店库设计里最容易翻车的五件事
4.1 房态更新没加事务,换房换出“双开”
现象:客人从 8801 换到 8802,前台操作后 8801 显示空净,但 8802 也显示空净,系统里两间房都能卖。原因:更新旧房状态和更新新房状态分了两条 SQL,中间没有事务包裹,第二条失败后第一条已提交。解决:把换房逻辑写进一个事务,先锁room表相关行,再依次更新,最后写换房日志。用SELECT ... FOR UPDATE锁住两间房的行,避免并发换房撞车。
4.2 用 DATETIME 存日期,跨天夜审算错房费
现象:客人 1 月 1 日 23:50 入住,1 月 2 日 00:10 退房,系统算了一晚房费,但夜审报表里这单被算进 1 月 2 日。原因:check_in_date用了DATETIME,夜审按DATE(actual_check_in)分组时跨天切分错误。解决:预订日期用DATE,实际到店时间用DATETIME,夜审按stay.actual_check_in的酒店本地日期减掉凌晨 6 点前的算前一天,这个规则要写进存储过程或应用层,不要靠数据库默认行为。
4.3 外键没建索引,退房结算慢成蜗牛
现象:入住单到几千条后,退房时查消费明细要好几秒。原因:consumption.stay_id有外键但没建索引,InnoDB 外键会自动建索引,但如果后来手动删了索引只留约束,查询就全表扫。解决:每张从表的外键列必须显式建索引,SHOW INDEX FROM consumption确认idx_stay存在。另外退房结算的查询用stay_id精确匹配,不要用guest_id范围扫。
4.4 金额用 FLOAT,夜审对账差几分钱
现象:夜审报表总金额和前台实际收款差 0.01 到 0.03 元,查不出原因。原因:早期建表用了FLOAT存金额,累加时浮点误差累积。解决:所有金额字段改DECIMAL(10,2)或DECIMAL(12,2),已经上线的库用ALTER TABLE ... MODIFY COLUMN改类型,改之前先备份。改完后跑一遍历史对账,确认差额归零。
4.5 状态字段用数字,日志里全是“3 变 5”
现象:排查问题时看操作日志,房态从3变成5,完全不知道什么意思。原因:状态用TINYINT存,日志直接记数字。解决:状态字段用CHAR或VARCHAR存可读枚举,或者在日志表里额外记一列status_desc。我一般直接在room.status用CHAR(3),牺牲一点存储换排查效率,酒店房间表数据量小,这笔账划算。
5. 进阶技巧:用生成列和触发器把“可售房数”做成实时视图
5.1 生成列统计当日可售房,避免每次 COUNT
前台最频繁的查询是“今天还有几间空净房”。如果每次都SELECT COUNT(*) FROM room WHERE status='VC',房间多了以后虽然不慢,但高并发下没必要。可以在room_type上加一个生成列,或者建一张汇总表用触发器维护。我倾向用触发器,因为生成列不能跨表统计。
-- 汇总表:按房型和日期统计可售房数 CREATE TABLE inventory_daily ( type_id INT NOT NULL, biz_date DATE NOT NULL, total_rooms SMALLINT NOT NULL DEFAULT 0, sold_rooms SMALLINT NOT NULL DEFAULT 0, PRIMARY KEY (type_id, biz_date), CONSTRAINT fk_inv_type FOREIGN KEY (type_id) REFERENCES room_type(type_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入住时增加已售,退房时减少 DELIMITER $$ CREATE TRIGGER trg_stay_after_insert AFTER INSERT ON stay FOR EACH ROW BEGIN DECLARE v_type INT; SELECT type_id INTO v_type FROM room WHERE room_id = NEW.room_id; INSERT INTO inventory_daily (type_id, biz_date, sold_rooms) VALUES (v_type, DATE(NEW.actual_check_in), 1) ON DUPLICATE KEY UPDATE sold_rooms = sold_rooms + 1; END$$ DELIMITER ;逻辑说明:inventory_daily按房型和营业日统计,入住触发器加一,退房触发器减一。ON DUPLICATE KEY UPDATE保证当天没有记录时自动插入。这样前台查可售房直接SELECT total_rooms - sold_rooms,不用扫room表。
参数说明:biz_date用营业日而不是自然日,凌晨 6 点前算前一天,这个逻辑在触发器里用DATE(NEW.actual_check_in)简化了,实际项目要在应用层算好再传入。触发器只做最简维护,复杂逻辑不要塞进触发器,否则夜审批量操作时容易锁表。
5.2 验证方法:用三条 SQL 自检数据一致性
设计完表结构后,我习惯跑三条自检 SQL,确认没有脏数据:
-- 1. 检查是否有房间同时存在多条在住记录 SELECT room_id, COUNT(*) FROM stay WHERE status = 'in_house' GROUP BY room_id HAVING COUNT(*) > 1; -- 2. 检查订单状态和入住单状态是否矛盾 SELECT o.order_id, o.status, s.status FROM booking_order o JOIN stay s ON s.order_id = o.order_id WHERE (o.status = 'checked_out' AND s.status != 'settled') OR (o.status = 'booked' AND s.status = 'in_house'); -- 3. 检查账务流水是否与消费明细匹配 SELECT s.stay_id, SUM(c.amount) AS consume_total, SUM(f.amount) AS folio_total FROM stay s LEFT JOIN consumption c ON c.stay_id = s.stay_id LEFT JOIN folio f ON f.stay_id = s.stay_id WHERE s.status = 'settled' GROUP BY s.stay_id HAVING ABS(IFNULL(consume_total,0) - IFNULL(folio_total,0)) > 0.01;第一条查双开,第二条查状态矛盾,第三条查账务差异。这三条 SQL 我每次改完表结构都会跑一遍,比事后对账省事得多。酒店库设计最怕的就是“看起来能跑,一上生产就乱”,自检 SQL 就是后悔药。
5.3 一个习惯:所有状态变更必须写日志表
最后说个血泪经验:不管多小的状态变更,都要写operation_log。表结构可以很简单——log_id、table_name、record_id、old_status、new_status、operator_id、log_at。前台换房、夜审锁房、经理冲账,全部留痕。出问题时直接查日志,不用去猜“谁在什么时候改了什么”。这个习惯让我在三次生产事故里都十分钟内定位到根因。希望帮到你。
本文还有配套的精品资源,点击获取