ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库课设客房管理:四态房态与存储过程实战指南

数据库课设客房管理:四态房态与存储过程实战指南 简介一份面向数据库课程设计的酒店管理系统客房管理实现适合高校计算机专业学生完成课设或复习数据库原理时参考。资源围绕客房预订、入住登记、退房结算等核心流程展示了从数据表设计、外键关联到JDBC数据库访问与Java界面开发的完整思路。压缩包共30个文件包含10个Java源文件、18个编译后的class文件及2张JPG界面示意图整体仅404KB结构轻量但覆盖了菜单、账单、客户管理等多个模块。目前已有1875人学习下载可用于对照练习或在此基础上扩展功能。通过阅读源码与运行示例可直观理解关系型数据库在实际业务中的建模方法并掌握使用Java Swing或类似GUI组件操作MySQL等数据库的常见实现方式适合作为课程设计起步模板。1. 数据库课设选了客房管理先想清楚这题目到底在考什么每年数据库课程设计总有一大批人栽在「酒店管理系统」这个看起来人畜无害的题目上。你说它难它无非就是几张表、几个页面、几条 SQL你说它简单偏偏有人交上去被问一句「你的客房状态是怎么维护的」「两单同时开同一间房怎么办」当场哑火。这门课设真正要检验的不是你写了多少行代码而是你有没有把一个现实业务问题拆成关系模型再用 SQL、视图、存储过程把业务规则固化下来。客房管理恰恰是整张设计里最值得深挖的一条主线它跨着预订、入住、换房、退房四个阶段房态流转天然适合用数据库约束和事务来表达。换个角度说选这个题目是聪明的。酒店管理系统领域你是熟的前台开单、房态展示、押金结算业务链条短客户全是老师验收场景固定非常适合把范式、事务、权限这些课设采分点全部展示出来。本篇文章就按我做这类课设的完整路径来拆先定数据模型再写建表脚本然后把「查房→排房→入住→换房→退房」这条主线写进存储过程最后给你一份验收前自查清单。你不一定要照抄这些代码但照这个思路走至少不会在答辩时被问住。2. 数据模型先立住从需求到表结构要知道的四件事2.1 客房是主体还是从属先分清主表和从表的粒度酒店管理系统里最容易犯的第一个错是把「房间」和「房型」塞进同一张表。表面看省了一张表实际上你没法回答「标准间一共有几种价格」「这栋楼哪些房间是双床」这类统计问题更致命的是改价要逐行更新直接违反第二范式。常规做法是把房型拆出来做主数据房间表只存物理信息和一个外键指向房型。我一般这样设计房型表管房价、床型、面积、可住人数房间表管楼层、房号、朝向、所属房型以及最重要的一个字段——当前房态。房态字段是客房管理系统的灵魂。设计时不要只存「空闲/占用」两个值至少要有空净已打扫可售、空脏退房未打扫、占用、维修这四态有条件再加一个预留态。空净和空脏分开是因为它直接影响排房逻辑前台查可用房时只能排空净房而保洁阿姨看的是空脏房清单。这四态也不要用中文硬编码建议用 char(1) 或 tinyint 存码值界面层再做映射这样后续写条件查询和状态统计都干净。表结构定完下一步是搞清楚每个状态在什么操作下跳到哪里。你把这些流转规则写成一小段注释放在建表脚本顶部比如「入住成功空净→占用退房结账占用→空脏打扫完成空脏→空净报修登记任何非占用状态→维修」。这段注释不能少它既是你的设计文档也是后面写存储过程和触发器时的依据。2.2 预订、入住、换房到底拆几张表从一笔订单的生命周期倒推第二个容易踩的坑是把「预订」「入住」「换房」的所有信息塞进一张「订单主表」然后靠一堆可空字段区分当前处于哪个阶段。结果就是查询某天有哪些在住客人你得写WHERE 状态字段 3 AND 日期范围覆盖今天索引失效统计口径还要靠记忆维护。更合理的做法是把每一次业务事件拆成独立流水。常见方案是预订表reservation存客人信息和预订日期区间入住表stay存实际入住记录包括房号、入住时间、预离时间、押金换房表room_change或者直接在入住表上留一个「当前房号」 一张变更日志。换房建议做成独立日志表记录旧房号、新房号、操作时间、操作人答辩时老师问「换房会不会破坏财务数据」你就可以直接用这张表证明每一笔变更都有迹可循。这里要树立一个观念数据库课设的评分点不在表多而在表之间的粒度是否正确。退房结账时你只需要 join 入住表和房型表就能算出房费不必去订单表里捞一堆无关字段查某间房一个月内的入住率也只需要对入住表做日期区间聚合而不是在单表里反复用 LIKE 过滤。粒度对了后面的 SQL 全都顺。2.3 外键和约束怎么设宁可建表时啰嗦不要业务层补救很多课设项目为了偷懒外键一律不建关系全靠业务代码维护结果就是删了客人的时候留下孤儿订单统计报表数字对不上。数据库课程设计考的就是数据库所以外键、CHECK、UNIQUE 这些约束能用尽量用。外键至少覆盖这几处房间表→房型表入住表→房间表入住表→客人表预订表→房型和客人表。删除策略选ON DELETE RESTRICT不许删有入住记录的房间和房型。CHECK 约束值得在房态字段上写一条状态值只能在 {0,1,2,3} 四态内。再加一条「入住时间必须早于预离时间」。UNIQUE 也很关键同一间房同一晚不能同时存在两条有效预订。这个约束用普通 UNIQUE 不好表达得靠后面存储过程的逻辑加锁或者用 MySQL 8.0 的生成列配合唯一索引来兜底。你先记着这个需求后面专门讲并发坑的时候再展开设计表阶段心里有数就行。2.4 客房管理里必须用视图的三个场景统计、权限、简化查询视图不是摆设。客房管理里至少有三个场景我用视图承包第一个是房态总览视图把房间、房型、当前状态 join 在一起前台页面每次加载只需要SELECT * FROM v_room_status不用在业务代码里拼七拐八弯的查询第二个是住客列表视图把入住表中未结账的记录关联客人表和房间表退房页面直接基于它清单化操作第三个是收入统计视图按日和房型聚合营业额这一步能帮你把「统计类查询」从「业务类查询」中分离出来答辩时直接演示报表页来自视图比现场写聚合 SQL 稳妥得多。有个细节视图里字段命名要带前缀或统一风格比如room_id、guest_name、stay_balance不要出现一个视图里同时有id和no这种让人分不清是房间号还是客人编号的字段。视图是你的对外接口命名就是接口规范。连视图都命名混乱的项目老师一眼看穿你的工程能力。3. 建库建表与视图授权一份可直接抄的 SQL 骨架3.1 建库与建表核心表定义与字段注释代码这块放到工位上写顺手不空谈。下面这一段是建库建表骨架支持 MySQL 5.7字符集选 utf8mb4。表结构按我上面的设计房型表、房间表、客人表、入住表、预订表、换房日志表共六张。核心字段的关键约束都写在注释里你照着敲完整个模型就立住了。-- 创建数据库开发环境直接utf8mb4别再用utf8 CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4; USE hotel_db; -- 房型表管房价和基础属性 CREATE TABLE room_type ( type_id TINYINT UNSIGNED AUTO_INCREMENT COMMENT 房型ID, type_name VARCHAR(20) NOT NULL UNIQUE COMMENT 房型名称标准间/大床房/套房, price DECIMAL(10,2) NOT NULL COMMENT 门市价, bed_type VARCHAR(20) COMMENT 床型说明, max_guests TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT 可住人数, PRIMARY KEY (type_id) ) ENGINEInnoDB COMMENT房型表; -- 房间表物理房间 当前房态 CREATE TABLE room ( room_id INT UNSIGNED AUTO_INCREMENT COMMENT 房间ID, room_no VARCHAR(10) NOT NULL COMMENT 房号如 1208, floor_no TINYINT UNSIGNED NOT NULL COMMENT 楼层, type_id TINYINT UNSIGNED NOT NULL COMMENT 所属房型, status CHAR(1) NOT NULL DEFAULT 0 COMMENT 0空净 1空脏 2占用 3维修, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (room_id), UNIQUE KEY uk_room_no (room_no), CONSTRAINT fk_room_type FOREIGN KEY (type_id) REFERENCES room_type(type_id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT chk_room_status CHECK (status IN (0,1,2,3)) ) ENGINEInnoDB COMMENT房间表; -- 客人表只存基础档案不掺订单 CREATE TABLE guest ( guest_id INT UNSIGNED AUTO_INCREMENT COMMENT 客人ID, guest_name VARCHAR(50) NOT NULL COMMENT 姓名, id_card VARCHAR(18) NOT NULL UNIQUE COMMENT 身份证号, phone VARCHAR(20) COMMENT 联系电话, PRIMARY KEY (guest_id) ) ENGINEInnoDB COMMENT客人表; -- 入住表一次入住一条记录 CREATE TABLE stay ( stay_id INT UNSIGNED AUTO_INCREMENT COMMENT 入住记录ID, guest_id INT UNSIGNED NOT NULL COMMENT 客人ID, room_id INT UNSIGNED NOT NULL COMMENT 房间ID, check_in_time DATETIME NOT NULL COMMENT 入住时间, expect_leave DATETIME NOT NULL COMMENT 预离时间, deposit DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 押金, status CHAR(1) NOT NULL DEFAULT 0 COMMENT 0在住 1已退, PRIMARY KEY (stay_id), KEY idx_stay_room (room_id), KEY idx_stay_status (status), CONSTRAINT fk_stay_room FOREIGN KEY (room_id) REFERENCES room(room_id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT fk_stay_guest FOREIGN KEY (guest_id) REFERENCES guest(guest_id) ON UPDATE CASCADE ON DELETE RESTRICT, CONSTRAINT chk_stay_time CHECK (expect_leave check_in_time) ) ENGINEInnoDB COMMENT入住记录表;这段脚本里有两个点值得你注意。第一个是ON DELETE RESTRICT有入住记录的房间不允许删除有入住记录的客人也不允许直接删档案这是保护历史数据不被误操作。第二个是status CHAR(1)加 CHECK 约束把房态和入住状态都限定在合法取值集合业务层写错了数据库直接报错而不是留到报表阶段才发现数据脏了。还有id_card上的 UNIQUE防止同一个人被录入两次这个约束未来查询也是走索引的。3.2 房态总览视图把最常用的查询固化下来视图我是这样建的。这里不追求把所有字段都铺开只求前台页面每次取数都稳定、快。房态总览视图最重要的产出是「可售状态计算」——因为前台查空房时只关心空净所以视图中直接算一个saleable字段值是CASE WHEN room.status 0 THEN 1 ELSE 0 END。-- 房态总览前台首页和排房操作的唯一数据源 CREATE OR REPLACE VIEW v_room_status AS SELECT r.room_id, r.room_no, r.floor_no, rt.type_name, rt.price, r.status, CASE WHEN r.status 0 THEN 1 ELSE 0 END AS saleable, rt.max_guests FROM room r JOIN room_type rt ON r.type_id rt.type_id; -- 在住客人总览退房页面直接用 CREATE OR REPLACE VIEW v_stay_guest AS SELECT s.stay_id, g.guest_name, g.id_card, g.phone, r.room_no, rt.type_name, s.check_in_time, s.expect_leave, s.deposit, TIMESTAMPDIFF(DAY, s.check_in_time, s.expect_leave) AS stay_days FROM stay s JOIN guest g ON s.guest_id g.guest_id JOIN room r ON s.room_id r.room_id JOIN room_type rt ON r.type_id rt.type_id WHERE s.status 0;视图建好后试一下SELECT * FROM v_room_status WHERE salable 1确认结果里只有空净房。用TIMESTAMPDIFF算出预住天数这个字段在退房结算时可以直接复用不用到处重复写日期间隔逻辑。视图字段不多但每列都是前台点得到的字段越少越容易维护。3.3 用户与权限数据库层面的最小权限是你答辩的加分项不少课设项目全程用一个 root 账号连接数据库页面里写死 root 密码老师一看到就皱眉。你只要多花十分钟建两个账号答辩观感完全不同前台操作账号只有增删改查权限统计账号只有 SELECT 权限。思路是「最小权限原则」——前台开单不需要 DROP 权限保洁标记房态也不需要 UPDATE 房型表价格。-- 前台账号只操作业务数据无 DDL 权限 CREATE USER hotel_applocalhost IDENTIFIED BY HotelApp2024; GRANT SELECT, INSERT, UPDATE, DELETE ON hotel_db.stay TO hotel_applocalhost; GRANT SELECT, INSERT, UPDATE, DELETE ON hotel_db.guest TO hotel_applocalhost; GRANT SELECT, UPDATE ON hotel_db.room TO hotel_applocalhost; GRANT SELECT ON hotel_db.v_room_status TO hotel_applocalhost; GRANT SELECT ON hotel_db.v_stay_guest TO hotel_applocalhost; -- 统计账号只读 CREATE USER hotel_reportlocalhost IDENTIFIED BY Report2024; GRANT SELECT ON hotel_db.v_room_status TO hotel_reportlocalhost; GRANT SELECT ON hotel_db.v_stay_guest TO hotel_reportlocalhost; FLUSH PRIVILEGES;两个账号的权限边界放得太细容易绑手绑脚但至少要保证前台账号动不了表结构统计账号动不了数据。GRANT SELECT ON view这种授权方式顺带展示了你在权限设计上的考虑——视图作为只读接口提供给报表侧业务侧基表权限单独控制。这一小节内容写在文档里答辩时老师问「你的系统怎么防止有人恶意删数据」直接指这一页就行。4. 把查房、排房、换房、退房写进存储过程三个必调参数与一个边界4.1 为什么业务规则要下沉到存储过程而不是写在 Java/PHP 里很多同学习惯把逻辑写在业务层数据库只用来存取数据。但客房管理不同查房、排房、换房、退房每一步都涉及「读状态 → 判断 → 改状态」多个操作员同时操作时数据库事务是唯一的可靠防线。如果你在业务层用「先 SELECT 再 UPDATE」的方式写两个人同时抢同一间空房时极大概率两层都读到 status 0然后各自 UPDATE 成功房间就超售了。把判断和更新写进一个存储过程用事务和锁把临界区收窄才是正确做法。存储过程另一个实际好处是答辩时老师让你现场演示「排一间已经被占用的房」你只需要调用一次过程看到报错信息就能说明规则是数据库层面拦截的而不是页面层 JS 挡的。这个演示效果比任何口头解释都强。要注意没有标准库可用所有判断就得自己在过程体里写清楚。4.2 排房核心过程查房号、试锁定、落订单三步排房这步本质上是「事务性领取」——把一间空净房的状态从 0 改成 2同时插入一条入住记录。整个过程必须在一个事务里完成中间任何一步失败都要回滚。我把这个过程命名为sp_check_in入参是客人姓名、电话、房号、入住时间、预离时间、押金。这里先不处理身份证查重重点演示房态判断和事务控制。DROP PROCEDURE IF EXISTS sp_check_in; DELIMITER $$ CREATE PROCEDURE sp_check_in( IN p_guest_name VARCHAR(50), IN p_id_card VARCHAR(18), IN p_phone VARCHAR(20), IN p_room_no VARCHAR(10), IN p_check_in DATETIME, IN p_expect_leave DATETIME, IN p_deposit DECIMAL(10,2) ) BEGIN DECLARE v_room_id INT; DECLARE v_status CHAR(1); START TRANSACTION; -- 用FOR UPDATE锁住房间行防止并发重复入住 SELECT room_id, status INTO v_room_id, v_status FROM room WHERE room_no p_room_no FOR UPDATE; IF v_status 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该房间当前不可入住房态非空净; END IF; -- 插入或查找客人为演示简化为有则取ID无则插入 INSERT INTO guest (guest_name, id_card, phone) VALUES (p_guest_name, p_id_card, p_phone) ON DUPLICATE KEY UPDATE guest_id LAST_INSERT_ID(guest_id); SET v_guest_id LAST_INSERT_ID(); -- 写入住单 INSERT INTO stay (guest_id, room_id, check_in_time, expect_leave, deposit) VALUES (v_guest_id, v_room_id, p_check_in, p_expect_leave, p_deposit); -- 房态置为占用 UPDATE room SET status 2 WHERE room_id v_room_id AND status 0; -- 影响行数为0说明被并发抢走 IF ROW_COUNT() 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 房态已被其他事务修改请重试; END IF; COMMIT; END$$ DELIMITER ;这个过程中两个必调参数最值得讲解p_room_no是业务键用它查room_id而不是让客户端传 ID是为了防止客户端传入不存在的 ID 检查不到p_deposit直接入库不做隐式默认前台没填就传 NULL 让数据库报错比业务层偷偷填 0 要诚实。FOR UPDATE是这里的排他锁关键MySQL 会在事务提交前锁住该房间行第二个会话来执行时阻塞等待等锁释放后重新读到最新的status然后因条件不满足直接走ROLLBACK分支。边界在哪里这个版本不支持同人连开多间、不支持团队订房只覆盖「一人一房一晚」的课设范围。老师追问这个限制时你就说客房预订单、团队单需要另表设计目前版本聚焦核心单房流程。这个回答反而能体现你心里有扩展空间。4.3 换房过程状态机不乱转先把「旧房退还」做完换房的业务语义是客人从 A 房搬到 B 房A 房变空脏B 房从空净变占用同时换房记录要写进日志。很多同学在这里直接写两条 UPDATE状态跳得乱七八糟一查历史就是「A 房从占用直接变成空净中间没有空脏这一步」——保洁都不知道要不要去打扫。正确的顺序是三步先锁定 B 房确认空净再更新入住表的 room_id最后把 A 房置为空脏、B 房置为占用、写换房日志。三步在一个事务里。DROP PROCEDURE IF EXISTS sp_change_room; DELIMITER $$ CREATE PROCEDURE sp_change_room( IN p_stay_id INT UNSIGNED, IN p_new_room_no VARCHAR(10), IN p_operator VARCHAR(20) ) BEGIN DECLARE v_old_room_id INT; DECLARE v_new_room_id INT; DECLARE v_new_status CHAR(1); START TRANSACTION; -- 先查当前入住记录锁定它避免并发退房 SELECT room_id INTO v_old_room_id FROM stay WHERE stay_id p_stay_id AND status 0 FOR UPDATE; IF v_old_room_id IS NULL THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 入住记录不存在或已退房; END IF; -- 锁定新房间检查是否空净 SELECT room_id, status INTO v_new_room_id, v_new_status FROM room WHERE room_no p_new_room_no FOR UPDATE; IF v_new_status 0 THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 目标房间不可用; END IF; -- 更新入住单指向新房 UPDATE stay SET room_id v_new_room_id WHERE stay_id p_stay_id; -- 旧房标记空脏需要打扫新房标记占用 UPDATE room SET status 1 WHERE room_id v_old_room_id; UPDATE room SET status 2 WHERE room_id v_new_room_id; -- 换房日志答辩时证明操作可追溯 INSERT INTO room_change (stay_id, old_room_id, new_room_id, operator, change_time) VALUES (p_stay_id, v_old_room_id, v_new_room_id, p_operator, NOW()); COMMIT; END$$ DELIMITER ;这个过程的参数设计有个容易忽略的坑p_operator不是业务必需字段但只要有日志表就建议传否则你不知道哪次换房是哪个账号操作的。将来出问题追责时这条日志是命根子。另一个细节是两把FOR UPDATE的位置先锁旧单再锁新房顺序固定。如果两个会话同时做两笔换房可能出现互相持锁等待的环但因为课设并发量极低这点风险可以接受答辩被问到就说「为降低死锁概率统一先锁入住表再锁房间表」。换房过程写完你会发现状态机逻辑只要落成「旧房→空脏、新房→占用」就非常清晰这也是为什么我在第 2.1 节强调四态设计——如果只存 0 和 1换房根本没法表达「等待保洁」这个中间态。4.4 退房结账按日计价参数与方法配置退房结账的公式不复杂应收房费 房价 × 住宿天数 延时费 - 已收押金。难点在「天数怎么算」和「延迟退房怎么收费」。我按业界最常见的规则处理中午 12 点前退房按一晚算超过 12 点未满 18 点加收半天房费超过 18 点按全天房费。这套规则你写进存储过程参数表可以单独设计成 rate_rule 表方便改动但过程体内部也可以先写死常量减少不必要的表关联。DROP PROCEDURE IF EXISTS sp_checkout; DELIMITER $$ CREATE PROCEDURE sp_checkout( IN p_stay_id INT UNSIGNED, IN p_checkout_time DATETIME ) BEGIN DECLARE v_room_id INT; DECLARE v_type_price DECIMAL(10,2); DECLARE v_check_in DATETIME; DECLARE v_deposit DECIMAL(10,2); DECLARE v_days INT DEFAULT 0; DECLARE v_late_fee DECIMAL(10,2) DEFAULT 0; DECLARE v_total DECIMAL(10,2); START TRANSACTION; SELECT s.room_id, s.check_in_time, s.deposit, rt.price INTO v_room_id, v_check_in, v_deposit, v_type_price FROM stay s JOIN room r ON s.room_id r.room_id JOIN room_type rt ON r.type_id rt.type_id WHERE s.stay_id p_stay_id AND s.status 0 FOR UPDATE; IF v_room_id IS NULL THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该入住记录不存在或已结账; END IF; -- 按日期差计算基本住宿天数不足一天按一天 SET v_days DATEDIFF(p_checkout_time, v_check_in); IF v_days 1 THEN SET v_days 1; END IF; -- 延迟退房费用离店时间超过预离当天12:00且未超过18:00加半天 IF TIME(p_checkout_time) 12:00:00 AND TIME(p_checkout_time) 18:00:00 THEN SET v_late_fee v_type_price * 0.5; ELSEIF TIME(p_checkout_time) 18:00:00 THEN SET v_late_fee v_type_price; END IF; SET v_total v_type_price * v_days v_late_fee; -- 写结账单 INSERT INTO checkout (stay_id, room_id, checkout_time, total_amount, deposit, late_fee) VALUES (p_stay_id, v_room_id, p_checkout_time, v_total, v_deposit, v_late_fee); -- 更新入住状态为已退 UPDATE stay SET status 1 WHERE stay_id p_stay_id; -- 房间置为空脏 UPDATE room SET status 1 WHERE room_id v_room_id; COMMIT; END$$ DELIMITER ;这段代码里 v_days 的取值策略值得单独说DATEDIFF返回的是整数天差值如果客人当天入住当天退差值 0 天业务上也要算一天房费所以有IF v_days 1 THEN SET v_days 1这一兜底。延迟规则直接写在过程中好处是规则和约束在一起坏处是以后改成「14 点前退房不加钱」时要改存储过程。课设阶段别过度设计规则表直接写常量就好但要在注释里标明「规则可扩展」。结账单单独成表不要把金额字段塞进入住表——入住表保留标准入住信息金额放结账单职责分离。5. 客房管理系统避坑指南并发预订、脏房标记与对账差异5.1 并发开房导致超售同一间房被抢两次只靠应用层判断无效现象模拟两个前台同时给两拨客人开同一间 1208 房时两个页面都显示「空闲」两次操作都提示成功数据库里出现两条在住记录指向同一间房。排房前查询时状态明明是 0为什么提交后没有报错。原因业务层代码是「先查状态再插入入住单再改房态」三步应用层查询之间没有锁两个请求都能读到旧状态然后各自执行后面的写入。MySQL 的普通 SELECT 不会锁行只靠 WHERE status 0 的 UPDATE 和插入之间有时间窗口。解决所有「判断更新」复合操作收进存储过程并且用SELECT ... FOR UPDATE先锁行。见我上面sp_check_in过程体里的做法事务开始后第一时间锁房间行阻塞并发会话提交前再校验状态实现真正的原子排房。另外给 stay 表加一个联合唯一索引字段为(room_id, status, check_in_date)虽然状态含 0/1 会导致同一房间在住期间无法插第二条在住记录这个索引也能做物理兜底。软硬兼施才能在答辩时讲清楚。5.2 换房后旧房变「空净」导致保洁白扫一遍现象换房操作后旧房间状态直接置为「空闲」前台立刻把它排给下一位客人。新客人进房后发现房间没打扫投诉。查日志发现换房时旧房状态被误写成空闲。原因换房时把业务语义做成「A 房释放 → 清空状态位」没有区分「空净」和「空脏」。旧房被客人住过物理上已经脏了应该进入待打扫队列而不是直接可售。这类错误会连锁导致卫生检查全盘失灵。解决应用四态设计换房时旧房状态置为1空脏由保洁端操作改为0空净。同时在前台排房页面只展示saleable 1的房也就是状态 0 的空净房。查询视图v_room_status里的saleable字段会自动过滤空脏房所以只要数据层不写错UI 就不会出问题。5.3 退房改了状态但押金和房费对不上账现象某个订单退了房房间状态也变回空脏但结账报表里金额和押金数字对不上退房订单查不到结账记录。翻看数据发现有入住记录状态被改成「已退」但 checkout 明细表里根本没有对应行。原因退房过程只更新了 stay 表状态没有保证结账单插入成功。如果业务代码分两步提交第一步插结账单失败被吞掉异常第二步照样改状态账就平不了。本质是缺少事务边界。解决退房全过程必须放进一个事务INSERT checkout和UPDATE stay、UPDATE room要么都成功要么都失败。请检查你最终的存储过程里有没有把结账插入放在 COMMIT 之前——如果分步写请你务必合并。另外可以在结账表上给stay_id加 UNIQUE 索引这样重复退房操作会被数据库拒掉不会产生两笔结账记录。5.4 状态字段用中文可读值导致统计 SQL 和条件索引失效现象房间状态字段直接存「空闲」「占用」等中文列表页按状态筛选时用字符串匹配数据量一大就卡。还有开发同学写了个WHERE status 空闲的统计视图结果状态值有的叫「空闲」有的叫「空闲房」口径对不上。原因中文可读值不适合作为存储层枚举因为它没有码表约束写错一个字查询就断裂。而且可变长度字符串索引效率低于定长 CHAR。解决全部舍中文存码值如 0/1/2/3在应用层做码值到文案的映射。数据库层面用CHAR(1)CHECK约束不允许不合法值出现。统计 SQL 全部基于码值写展示页面再映射逻辑统一掐在数据层。6. 验收前自测清单五个查询场景与一组「后悔药」设计最后一章不说空话直接给一张可以用命令行跑的自测表。老师验收时无非问你这些问题能不能查到可售房能不能开一间房换房后历史还在不在退房后账对不对并发抢房会不会超售。按下面这张清单把所有场景跑一遍每一行都是可验证的输入输出跑通一遍基本就稳了。场景执行操作预期结果初始房态查询 v_room_status所有房间 saleable 1状态 0开房成功调用 sp_check_in 入参房号 1208返回成功1208 状态变 2v_stay_guest 出现该客人重复开房再次调用 sp_check_in 同一房号报错「该房间当前不可入住」事务回滚换房操作调用 sp_change_room 换到 12091208 状态变 11209 变 2room_change 表多一条历史退房结账调用 sp_checkout生成结账单房费金额正确1209 状态变 1跑完这五个场景再补一个并发测试开两个终端同时执行sp_check_in抢同一间房第二个终端应该阻塞或报错最终只有一条成功。这一步能验证锁是否生效也是答辩中「数据库保证一致性」的最有力证据。最后提一个我自己的习惯在入住表、结账表、换房日志表都保留created_at时间戳并且不允许应用层覆盖它。哪怕课程设计阶段没有审计需求留下创建时间出了问题排查基线会从容很多。很多同学等到数据乱了才想起来没有时间戳那就是后悔药都找不到了。顺手再做一件小事把这六个存储过程和两个视图的 SQL 脚本导出成独立文件命名按001_schema.sql、002_data.sql、003_procedure.sql排序。这个习惯在答辩前整理文档时会帮你省下大量的时间而且文件摆出来就有种「工程化」的印象分。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进