ARTICLE DETAIL

资讯详情

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

SQL Server宾馆管理系统课设:从ER模型到存储过程的完整实践

SQL Server宾馆管理系统课设:从ER模型到存储过程的完整实践 简介一份面向软件工程专业数据库课程的宾馆房间管理系统课程设计报告结合SQL Server 2000与C#.NET完整走完课程设计全过程。内容按教材式结构展开第1章明确课程设计目的、环境与参考资料第2章为核心数据库设计涵盖需求分析、业务流程图与数据流程图绘制、数据字典定义、概念与逻辑设计、物理设计、数据库实现再到应用程序的概要设计与C#.NET程序实现涉及登录验证、客房种类管理、客房信息管理、客户入住登记、客户查询与结账等功能。第3章为课程设计总结文末附参考文献。报告完整展示了宾馆客房管理信息系统的数据库建模思路和编码衔接方式适合软件工程、计算机等相关专业的学生参考课程设计流程、撰写报告或准备答辩。包内含1个doc文档大小约293KB目前已有139人学习可作为学习数据库课程设计时的典型实例。1. 为什么这个课设题库里的老题反而不容易做好做 SQL 数据库课程设计十个题目里有八个绕不开宾馆房间管理系统。很多人觉得它简单无非是房间表、顾客表、订单表再配几个增删改查页面一星期就能交差拿到成绩却只有及格线。反直觉的地方在于这个系统真正的难点根本不在增删改查而在“房间状态”的一致性上——一间房从空闲到预订、从入住到退房中间任何一个环节漏更新一张表报表就会对不上账演示现场就会被老师一句“你这数据是怎么变的”问住。这篇笔记就围绕这套题目展开按课设文档的推进顺序讲先说业务规则和 ER 模型怎么定再给可直接照抄的 T-SQL 建库建表脚本然后实现入住、退房、日志和报表的存储过程与视图最后聊课设验收时最常见的一组翻车场景。适合正在做课设的学生也适合刚接触数据库开发、想找一套规范写法作参照的初级工程师。下面进入正题。2. 从状态机到 ER 模型先画对状态流转再动手建表2.1 房间状态是整张数据库的重心先画状态机宾馆管理系统业务上最核心的实体不是顾客而是“房间的状态变化”。一间房从营业开始通常会经历这么几个状态空闲、已预订、占用、清理中、维修中。这五个状态之间是有严格迁移规则的空闲房可以被预订变成已预订已预订房在顾客到达后办理入住变成占用占用房办理退房后变成清理中清理完成回到空闲空闲房也可以直接进入维修中维修完成再回到空闲。反过来占用房不能被直接预订维修中的房间也不能办理入住——这些规则不是靠前端按钮控制的而是要在数据库的表结构、约束和存储过程里兜住的。我的习惯是先把状态迁移画出草图确认每个箭头都有对应的业务动作再开始设计表。因为状态迁决定了两件事room 表上状态字段的取值范围以及订单表、入住记录表在什么时机被写入。很多课设翻车都是因为状态字段类型和取值没定好最后业务代码里到处写魔法数字报表统计时还要靠猜。2.2 最小实体集六张表怎么连成一张网围绕状态机最少需要六张表房型表、房间表、顾客表、预订订单表、入住记录表、账单表。如果还想展示触发器和日志再加一张操作日志表凑成七张。它们的关联关系是表名记录什么与谁关联关系基数room_type房型与基准价格room1 : Nroom房间号、房型、状态room_type, checkin_detail1 : Ncustomer顾客身份与联系方式reservation1 : Nreservation预订单customer, roomN : 1checkin_detail每次实际入住记录room, customerN : 1bill账单明细checkin_detail1 : N预订订单和入住记录为什么要分开因为现实里存在“预订后未入住”和“没预订直接入住”两种情况。把两张表合并成一张订单表退房结账逻辑就会被各种为空的状态干扰。拆开之后reservation 管预约承诺checkin_detail 管实际占用两张表通过 room_id 和 customer_id 关联边界非常清楚。2.3 范式的边界价格快照属于有意的冗余做课设时老师通常会在文档里要求按第三范式设计。但宾馆系统里有一处必须“违反”第三范式reservation 表里要冗余一份当时的房间价格甚至把顾客姓名也冗余进来。原因很简单房型价格会调整顾客可能改名但订单成交时的价格和顾客身份是历史事实。如果不做快照三个月后再查这张订单只能连表到房型表取最新价格对出来的财务数据全是错的。这个冗余不是设计失误而是业务上的“历史快照”。在课程设计文档里主动写一句“此处有意冗余以保证订单价格的历史可追溯性”比被老师提问时支支吾吾要体面得多。ER 图里把这条关系标注清楚这一步做好了后面建表才有底气。3. 建库与建表把约束直接写进 DDL别留给业务代码3.1 建库脚本文件路径、文件增长和排序规则一次到位拿到 SQL Server 后先不要急着 CREATE TABLE第一件事是把数据库文件放到非系统盘并显式设置增长参数。我一般用这套建库脚本CREATE DATABASE HotelMS ON PRIMARY ( NAME NHotelMS, FILENAME ND:\Data\HotelMS.mdf, SIZE 8MB, MAXSIZE UNLIMITED, FILEGROWTH 8MB ) LOG ON ( NAME NHotelMS_log, FILENAME ND:\Data\HotelMS_log.ldf, SIZE 4MB, MAXSIZE 2048MB, FILEGROWTH 10% ) COLLATE Chinese_PRC_CI_AS;把文件放在数据盘而不是默认的 C 盘是防止系统盘空间紧张导致数据库写入失败。FILEGROWTH 的设定有个细节数据文件按固定 MB 增长日志文件按百分比增长。日志按百分比是 SQL Server 的常见做法因为日志增长与事务量相关写死固定大小容易频繁触发自动增长反而拖慢性能。COLLATE 使用 Chinese_PRC_CI_AS这是简体中文、不区分大小写的排序规则如果选错成英文排序规则后续查询中文条件时会出现诡异的排序和匹配问题。如果学校指定使用达梦或 GBase 这类国产数据库建库语法和这里的参数含义基本一致管理工具换一下就行思维模型完全通用。3.2 核心表 DDL状态字段的取值范围由 CHECK 约束兜底下面是五张核心业务表的建表脚本。注意我使用的字段命名风格和约束方式这是课设文档里最值得模仿的部分CREATE TABLE dbo.room_type ( type_id INT IDENTITY(1,1) PRIMARY KEY, type_name NVARCHAR(40) NOT NULL UNIQUE, base_price DECIMAL(10,2) NOT NULL CHECK (base_price 0) ); CREATE TABLE dbo.room ( room_id INT IDENTITY(1,1) PRIMARY KEY, room_no CHAR(4) NOT NULL UNIQUE, type_id INT NOT NULL REFERENCES dbo.room_type(type_id), room_status TINYINT NOT NULL DEFAULT 0, remark NVARCHAR(200) NULL, CONSTRAINT ck_room_status CHECK (room_status IN (0, 1, 2, 3, 4)) ); CREATE TABLE dbo.customer ( customer_id INT IDENTITY(1,1) PRIMARY KEY, customer_name NVARCHAR(40) NOT NULL, id_card CHAR(18) NOT NULL UNIQUE, phone VARCHAR(20) NULL, CONSTRAINT ck_id_card CHECK (id_card LIKE [0-9][0-9]%) ); CREATE TABLE dbo.reservation ( reservation_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL REFERENCES dbo.room(room_id), customer_id INT NOT NULL REFERENCES dbo.customer(customer_id), price_snapshot DECIMAL(10,2) NOT NULL, arrival_date DATE NOT NULL, leave_date DATE NOT NULL, order_status TINYINT NOT NULL DEFAULT 0, create_time DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), CONSTRAINT ck_order_date CHECK (leave_date arrival_date), CONSTRAINT ck_order_status CHECK (order_status IN (0, 1, 2)) ); CREATE TABLE dbo.checkin_detail ( checkin_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL REFERENCES dbo.room(room_id), customer_id INT NOT NULL REFERENCES dbo.customer(customer_id), reservation_id INT NULL REFERENCES dbo.reservation(reservation_id), checkin_time DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), checkout_time DATETIME2(0) NULL );参数说明room_status 用 TINYINT 而不是 CHAR(10)因为状态在程序里只会做等值判断数值型占用空间小且索引效率高。0 到 4 分别对应空闲、已预订、占用、清理中、维修中这个映射关系要在设计文档里写清楚。customer 表的 id_card 用 CHAR(18)因为身份证长度固定用 CHAR 比 VARCHAR 更省空间且符合语义。CHECK 约束里写 LIKE 模式只是为了演示格式校验实际项目里身份证还有校验位算法课设做到位可以补充说明但不必过度设计。checkin_detail 表的 reservation_id 允许 NULL这是为了支持“无预订直接入住”的场景。如果把它设为 NOT NULL店外客就无法入住这个设计会卡住真实业务。3.3 索引策略报表查询还没写索引先建出来很多课设脚本只有主键和外键等到写报表查询时才发现全表扫描慢得离谱。我一般在建表完成后直接加三条索引CREATE INDEX ix_reservation_arrival ON dbo.reservation(arrival_date, room_id); CREATE INDEX ix_checkin_room_time ON dbo.checkin_detail(room_id, checkin_time); CREATE UNIQUE INDEX ux_customer_idcard ON dbo.customer(id_card);第一条索引支撑“按日期查当日预订”这类高频报表第二条支撑“查某间房的历史入住记录”第三条是唯一索引身份证号本身有唯一性要求。这三条已经覆盖课设里 90% 的查询场景。不要为了显得专业给每一列都建索引索引过多会拖慢 INSERT 和 UPDATE房间入住退房这种高频操作反而变慢老师问起来也解释不清楚。4. 让业务自动流转视图、存储过程与触发器4.1 一个入住动作要更新四张表为什么要用存储过程封装入住是一个典型的多表联动动作把 room 的状态从已预订改成占用在 checkin_detail 插入一条入住记录把 reservation 的订单状态改成已入住再写一条操作日志。如果这些逻辑散落在前端代码里等于把数据库的一致性交给客户端自觉业务代码里任何一步遗漏房间状态就会卡死。用存储过程封装有两个直接好处一是事务边界明确要么全部成功要么全部回滚二是演示和答辩时老师让你现场跑一遍入住流程你在查询分析器里调一个存储过程就能讲清楚“这一条命令背后做了什么”说服力比贴十行前端代码强得多。4.2 入住和退房两个存储过程事务、锁与错误处理这是整套系统里我建议优先完成的核心过程。入住过程的完整写法如下CREATE OR ALTER PROCEDURE dbo.usp_CheckIn RoomID INT, CustomerID INT, ReservationID INT NULL, ActualPrice DECIMAL(10,2) OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; BEGIN TRAN; BEGIN TRY -- 锁住房间行防止并发下同一间房被两次入住 SELECT ActualPrice rt.base_price FROM dbo.room r WITH (UPDLOCK, ROWLOCK) JOIN dbo.room_type rt ON r.type_id rt.type_id WHERE r.room_id RoomID; IF ActualPrice IS NULL BEGIN RAISERROR(N房间不存在, 16, 1); RETURN; END; IF NOT EXISTS (SELECT 1 FROM dbo.room WHERE room_id RoomID AND room_status 1) AND NOT EXISTS (SELECT 1 FROM dbo.room WHERE room_id RoomID AND room_status 0) BEGIN RAISERROR(N房间当前不可入住, 16, 1); RETURN; END; UPDATE dbo.room SET room_status 2 WHERE room_id RoomID; INSERT INTO dbo.checkin_detail(room_id, customer_id, reservation_id) VALUES (RoomID, CustomerID, ReservationID); IF ReservationID IS NOT NULL UPDATE dbo.reservation SET order_status 1 WHERE reservation_id ReservationID; COMMIT; END TRY BEGIN CATCH ROLLBACK; THROW; END CATCH; END;逻辑说明第一步用 WITH (UPDLOCK, ROWLOCK) 锁住目标房间的行这是防止并发下“一房两卖”的关键没有这行锁两个前台同时办理入住时房间状态可能被覆盖。后续检查房间状态时我同时允许状态 0空闲和状态 1已预订办理入住分别对应散客直接入住和预订客人到店入住。事务结束后通过输出参数 ActualPrice 把实际价格返回给调用方便于前端展示。退房过程与之对称注意的坑在于计算房价和更新房间状态必须放在同一个事务里CREATE OR ALTER PROCEDURE dbo.usp_CheckOut CheckinID INT, TotalPrice DECIMAL(10,2) OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE RoomID INT; DECLARE CheckinTime DATETIME2(0); BEGIN TRAN; BEGIN TRY SELECT RoomID room_id, CheckinTime checkin_time FROM dbo.checkin_detail WITH (UPDLOCK) WHERE checkin_id CheckinID AND checkout_time IS NULL; IF RoomID IS NULL BEGIN RAISERROR(N无效的入住记录, 16, 1); RETURN; END; SET TotalPrice DATEDIFF(HOUR, CheckinTime, SYSDATETIME()) * 1.0 / 24 * (SELECT base_price FROM dbo.room_type rt JOIN dbo.room r ON r.type_id rt.type_id WHERE r.room_id RoomID); UPDATE dbo.checkin_detail SET checkout_time SYSDATETIME() WHERE checkin_id CheckinID; UPDATE dbo.room SET room_status 3 WHERE room_id RoomID; COMMIT; END TRY BEGIN CATCH ROLLBACK; THROW; END CATCH; END;这里使用 DATEDIFF 按小时计费再除以 24 换算成天数乘以每日房价这是宾馆计费的常见口径。实际项目中房费计算往往还涉及钟点房、延时退房等规则但课设做到按天计费已经能把事务、存储过程、约束这些考点全部覆盖。注意退房成功后房间状态直接改成 3清理中而不是回到 0空闲这一步是状态机设计里最容易漏的。4.3 视图与窗口函数当日占用报表和最新入住记录一次查出来报表查询是课设文档里最能体现 SQL 功力的部分。一个“当前所有占用房间”的视图就可以写成这样CREATE OR ALTER VIEW dbo.v_CurrentOccupancy AS SELECT r.room_no, rt.type_name, c.customer_name, ci.checkin_time, DATEDIFF(DAY, ci.checkin_time, SYSDATETIME()) AS stay_days FROM dbo.checkin_detail ci JOIN dbo.room r ON r.room_id ci.room_id JOIN dbo.room_type rt ON rt.type_id r.type_id JOIN dbo.customer c ON c.customer_id ci.customer_id WHERE ci.checkout_time IS NULL;这个视图直接对应前台“当前在住客人”的实时展示是演示时的亮点功能。稍微进阶一点如果需要“每个房间最近一次入住记录”用 GROUP BY 很难表达“最近一条”而 SQL Server 的窗口函数 ROW_NUMBER 就是为此设计的SELECT room_no, customer_name, checkin_time FROM ( SELECT r.room_no, c.customer_name, ci.checkin_time, ROW_NUMBER() OVER (PARTITION BY ci.room_id ORDER BY ci.checkin_time DESC) AS rn FROM dbo.checkin_detail ci JOIN dbo.room r ON r.room_id ci.room_id JOIN dbo.customer c ON c.customer_id ci.customer_id ) AS t WHERE t.rn 1;窗口函数是近几年数据库课设的高频考点在文档里写一段这样的查询作为“进阶实现”比满篇 SELECT * 更让老师信服。这里先用内层查询给每个房间的入住记录按时间倒序编号再取 rn 1语义非常清晰。需要注意 PARTITION BY 后面跟的是 ci.room_id如果只写 r.room_no因为 room_no 也是唯一的结果相同但语义上不严谨。4.4 触发器用一张日志表记录状态变更但不要让它无限递归存储过程处理了业务触发器负责审计。我建议用一个 AFTER UPDATE 触发器记录 room 表状态变化这是课设里最安全的触发器的用法CREATE TABLE dbo.room_status_log ( log_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL, old_status TINYINT NOT NULL, new_status TINYINT NOT NULL, change_time DATETIME2(0) NOT NULL DEFAULT SYSDATETIME() ); CREATE OR ALTER TRIGGER dbo.trg_room_status_change ON dbo.room AFTER UPDATE AS BEGIN SET NOCOUNT ON; IF NOT EXISTS (SELECT 1 FROM inserted i JOIN deleted d ON i.room_id d.room_id WHERE i.room_status d.room_status) RETURN; INSERT INTO dbo.room_status_log(room_id, old_status, new_status) SELECT d.room_id, d.room_status, i.room_status FROM inserted i JOIN deleted d ON i.room_id d.room_id WHERE i.room_status d.room_status; END;触发器里最忌讳的操作是在触发器内再次 UPDATE 同一张表这会造成触发器递归触发直到达到嵌套层数上限报错。这里的实现只做 INSERT 审计日志不去反向修改任何业务表很安全。嵌套层级相关的服务器配置 RECURSIVE_TRIGGERS 保持默认关闭状态即可不必动它。日志表的数据量会不断增长课设演示期间体量很小索引暂时不需要额外建。5. 从设计到交付课设验收最常见的六条踩坑记录5.1 外键被删除时直接报错没有理清引用关系就删主表现象想删掉一间房或一个房型DELETE 语句报外键冲突老师点了一下“为什么删不掉”当场答不上来。原因room_type 表被 room 表的外键引用checkin_detail 又引用 room。只要历史入住记录存在房间行就和这些记录绑在一起直接 DELETE 当然被拒这是数据库引用完整性的正常保护。解决业务上房间不能物理删除应该把 room 表加一个 is_active 标志位做逻辑删除。房型表如果确实需要清理必须先删除关联的房间再删除房型并且这个操作要在事务里执行。课设文档里写明“采用逻辑删除而非物理删除”是标准答案。5.2 触发器循环触发导致事务爆掉现象给 room 表写了一个 UPDATE 触发器执行退房存储过程时报表立刻报错错误信息提示触发器嵌套超出最大层数。原因退房过程先 UPDATE room 设置状态这触发日志触发器如果日志触发器里顺手又 UPDATE 了 room 表就形成 A 触发 B、B 又触发 A 的无限循环。解决触发器只做跨表写入不要回写自身表确有必要回写时使用 IF UPDATE(column) 配合条件判断让触发器只在特定列变化时才继续执行。最稳妥的做法是像 4.4 节那样触发器只用 inserted 和 deleted 表做 INSERT 操作。5.3 参数拼串导致 SQL 注入演示当天被现场报错打脸现象登录或查询功能用字符串拼接 SQL 实现输入一个带单引号的顾客姓名页面报语法错误如果输入的是恶意构造的语句还可能直接绕过查询条件。原因前端传过来的值被原样拼进 SQL 字符串等于把语法控制权交给了输入者。这是数据库开发里最典型的安全反面教材。解决存储过程内部使用参数化查询外部调用也用 EXEC 或 ADO.NET 的 SqlParameter 传参永远不使用拼接。课设文档的“安全性分析”一节把这个写进去比写十行冠冕堂皇的“系统安全性高”要实在得多。5.4 并发下同一间房被订两次忘记加锁现象两张相同房间的入住记录同时存在checkin_detail 里出现同一 room_id 的两条未退房记录。原因两个会话同时执行入住逻辑都先在内存里读到房间状态为空闲随后各自执行 INSERT 和 UPDATE互相覆盖数据库层面没有任何保护。解决入住存储过程里对房间行加 UPDLOCK 锁如 4.2 节代码所示。这也是事务隔离级别解决不了的因为问题出在“先读后写”之间的间隙。演示时可以用 SSMS 开两个查询窗口同时跑证明第二次调用会被阻塞或报错这本身就是课设答辩的一个亮点环节。5.5 日期字段类型选错当天退房怎么都算一天房费现象入住时间是当天的 23:50退房是第二天的 00:20房费被算成两天或者入住时传入了带时间的值查询“当日入住”时因时间干扰导致统计结果错误。原因checkin_detail 的时间用 DATE 类型存储丢失了具体时刻或者相反预订订单的日期用了 DATETIME导致按天分组统计时把跨天的数据错误归组。解决区分业务语义——预订日期用 DATE入住退房时刻用 DATETIME2(0)。计算房费时按 DATEDIFF(HOUR) 换算结果更合理统计“今日入住”时使用 CONVERT(DATE, checkin_time) 之后再比较或者直接按 checkin_time 当天零点 来过滤。5.6 报告文档只有建表语句没有运行说明老师没法复现现象交上来的课设报告里贴了大量 CREATE TABLE却没有数据库初始化顺序、测试数据脚本、账号说明老师在自己电脑上根本跑不起来。原因文档只记录了设计结果没记录运行环境和使用方法。对于以 .doc 形式提交的课程设计报告运行说明和测试用例几乎和代码同等重要。解决在文档中固定包含三部分内容——创建数据库的顺序先建库、再建表、再建索引、再写存储过程一组可重复执行的测试数据脚本至少五个测试场景预订、入住、退房、报表、非法状态修改的操作步骤和预期结果。做到这一步报告的完成度远超平均水平。6. 验收前的最后一次排演演示数据、查询计划与答辩思路课设答辩前一天的排演比写代码更值得花时间。第一件事是检查演示数据不要用 1、2、3 这种编号当顾客名和房间号建 20 间房、12 个顾客房间号用 101、102、201、202顾客用真实感强的中文姓名并让入住记录的时间跨度覆盖过去十天。这样报表视图一眼看去像真实系统老师对数据可信度产生好感之后提问也会更友好。第二件事是验证存储过程是否走了索引。在 SSMS 里执行 EXEC dbo.usp_CheckIn 后点击“显示估计的执行计划”看是否有表扫描。如果出现 Table Scan说明 4.2 节建索引时漏了字段补上即可。这一项也直接回应了“慢 SQL 优化”这个话题——课设数据的量很小索引优化体现不出性能差异但执行计划里能看到 Seek 和 Scan 的区别这就足够在答辩时讲清楚索引的价值。第三件事是准备一套固定的演示脚本顺序建议这样先打开 v_CurrentOccupancy 视图展示当前数据概览然后调用 usp_CheckIn 演示从预订到入住的完整流程特意展示房间状态从 1 变成 2再调用 usp_CheckOut 演示退房和房费计算最后查询 room_status_log 表展示触发器自动生成的日志。这套顺序是顺着状态机走下来的老师全程不需要打断你就能看到系统的完整闭环。最后讲一下 .doc 报告本身的排版细节。网上流传的课设模板大多要求粘贴代码截图但在 Word 里直接插入文本代码并切换为等宽字体如 Consolas才是最稳妥的做法一是文字可复制二是打印出来清晰不糊。ER 图和数据流图务必用清晰的矢量格式导出不要截图后压缩到模糊。所有代码按“库表结构—数据初始化—存储过程—视图触发器—测试用例”的顺序排列这与本笔记的章节顺序一致老师评审时按图索骥印象分会稳很多。我做过几年课设评审见过太多实现得不错、却因为演示前半小时手改数据库导致现场翻车的例子。我自己也犯过这个错——把 room_status 手工改错之后忘记改回来视频演示录了三遍才通过。从那以后我养成一个习惯演示前不直接手改数据所有状态变更一律走存储过程最后再跑一遍验收脚本确认所有状态机分支都正常。希望这个习惯和这篇笔记能帮到你把课设做成一门真正属于自己的拿得出手的数据库作品。本文还有配套的精品资源点击获取
返回列表