基于C#的宾馆管理系统:数据库设计、事务与三层架构

📅 发布时间:2026/10/5 1:23:27
基于C#的宾馆管理系统:数据库设计、事务与三层架构
简介这是一套基于C#开发的小型宾馆管理系统完整项目面向计算机相关专业学生和C#初学者适用于课程设计、毕业设计或自学练手。系统基于Windows Forms构建用户界面利用ADO.NET连接SQL Server数据库实现了客房预订、入住登记、退房结账、客户信息管理等核心业务完整覆盖了从需求分析、界面设计到业务逻辑封装、数据持久化的开发流程。压缩包内共121个文件包体总大小9.84MB其中包含44个.cs源文件、15个.resx资源文件、12个.dll依赖库以及.sln解决方案、.sql数据库脚本、.doc实验报告、可执行exe文件等目录结构清晰便于直接打开运行与对照学习。目前已有210人学习下载。通过完整源码和配套实验报告读者可掌握WinForm控件布局、SQL语句编写、ADO.NET增删改查、分层架构等关键技能也能学习到项目排错与调试思路是一份实战性很强的C#入门与进阶参考资料。1. 基于C#的宾馆管理系统不只是课设一个能跑通的数据库入门闭环打开一个号称“基于C#的宾馆管理系统”的课设压缩包里面通常是一个.sln解决方案、一份数据库文件加一份实验报告。这类项目每年被下载无数次但真正把它跑通、讲清楚的人不多。它解决的是一件很接地气的事用最小成本把C#连接SQL Server的完整链路走一遍——登录校验、房间状态查询、入住退房的事务写入最后还能对着实验报告把每一步讲明白。适合正在做课程设计或毕业设计的学生也适合想在简历里写一个C#数据库项目的初学者。房间、订单、用户三张表几十个方法足够你踩一遍新手该踩的坑。2. 先读懂数据库文件三张核心表设计与MDF挂载方式拿到zip先别急着双击.sln先把数据库文件理清楚。完整的SQL Server数据库形态是 .mdf 主数据文件加 .ldf 事务日志文件有的包里还会带一个 .bak 备份。这套系统的业务很简单——管房间、管入住、管登录所以核心表就三张Rooms、Orders、Users。先把表结构看懂后面的C#代码才能对上号。2.1 房间表、订单表、用户表的字段设计每个字段为什么要存在先看房间表这是整个系统状态流转的中心字段类型说明RoomIdint 主键自增房间唯一标识程序中一切关联都走这个IdRoomNumbervarchar(10) 唯一约束房号如201业务上不允许重复RoomTypevarchar(20)房型标准间/大床房/套房Pricedecimal(10,2)门市价用decimal避免浮点误差Statusvarchar(10)空闲/占用程序里维护状态流转RoomNumber一定要加唯一约束否则界面显示两个201入住时谁也不知道该改哪条记录。Price用decimal而不用float是面试必背的点float和double是二进制近似存储算钱会出0.10.2不等于0.3的问题。Status用字符串不用bit是为了后续扩展“打扫中”“已停用”这类状态。订单表Orders是连接房间和客人的桥梁字段类型说明OrderIdint 主键自增订单号RoomIdint 外键指向Rooms.RoomId不直接存房间号CustomerNamevarchar(20)客人姓名Phonevarchar(20)联系电话CheckInTimedatetime入住时间默认GETDATE()CheckOutTimedatetime退房时间入住时为NULLTotalPricedecimal(10,2)结账金额退房时写入Statusvarchar(10)在住/已退房订单表存RoomId外键而不是RoomNumber文本是有讲究的房号是业务数据今天叫201明天改叫301都不奇怪RoomId是主键一旦分配不再变化。实验报告里的ER图画的就是这三张表外键关系一定要标出来。用户表Users最简单UserId、UserName、Password、Role四个字段Role可以区分管理员和前台初始数据至少插两条只插一条admin的包在多人演示时不好看。外键的删除策略这里有个边界经验Order和Room之间不要开级联删除。订单是历史记录一间房只要有入住记录就不该被DELETE删掉只能标记“停用”。报告里写一句“订单记录予以保留房间状态由程序维护”就能在答辩时少一个追问。2.2 附加MDF还是执行SQL脚本两种数据库初始化方式对比压缩包里的数据库文件常见两种形态一种是完整的 .mdf/.ldf用图形界面右键“附加”就能挂上另一种是 .sql 脚本双击执行自动建库建表。两者取舍如下方式优点缺点适用场景附加MDF带历史数据演示效果直观文件权限问题多换环境要重挂验收演示、本地开发执行SQL脚本可重复执行改动可追溯没有数据要手动造测试数据交付源码、代码评审如果走附加路线用T-SQL比SSMS右键更稳方便写进文档-- 附加数据库.mdf 和 .ldf 必须放在同一目录文件名必须匹配 CREATE DATABASE HotelDB ON (FILENAME ND:\HotelSystem\HotelDB.mdf) FOR ATTACH; GO这段SQL等价于SSMS里的附加操作执行成功后数据库节点下会出现HotelDB表、数据、约束全都带过来。注意FOR ATTACH要求两个文件都能被SQL Server服务账号读到文件单独拷到D盘这类普通目录比放在桌面或系统盘省事得多。如果包里只有.sql脚本用sqlcmd命令行执行最直接sqlcmd -S . -E -i D:\HotelSystem\init.sql-S . 表示本机默认实例-E 表示Windows身份认证-i 指定脚本路径。课设环境装的是SQL Server Express时实例名要写成 .\SQLEXPRESS这是新手最容易忽略的差异。执行完用SELECT * FROM Rooms验证一下有没有初始数据别等程序跑起来才发现表是空的。3. 用C#实现入住和退房登录校验、房态列表与事务写入数据库就绪后进Visual Studio。设计器拖控件不需要教但按钮背后的代码逻辑值得逐行看。整个程序的核心交互就三块登录进主界面、在房间列表里找空闲房、点入住或退房按钮。这三个场景恰好覆盖C#操作数据库最高频的三个知识点参数化查询、DataTable绑定、手动事务。3.1 登录模块的参数化SQL从拼接字符串到SqlParameter登录窗体的验证代码正确写法长这样private bool CheckLogin(string userName, string password) { // 连接串从 App.config 读取不要在代码里写死 string connStr ConfigurationManager.ConnectionStrings[HotelDB].ConnectionString; string sql SELECT COUNT(*) FROM Users WHERE UserName name AND Password pwd; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { // 参数化查询让 SQL Server 把 name 和 pwd 当作值处理 cmd.Parameters.AddWithValue(name, userName); cmd.Parameters.AddWithValue(pwd, password); conn.Open(); int count (int)cmd.ExecuteScalar(); return count 0; } }用COUNT(*)配合ExecuteScalar是因为我们只关心“有没有这个人”拿到第一行第一列的值就够不必拖一个DataTable回来。using语句保证连接和命令对象用完即释放这是C#操作数据库的固定习惯。最反面的教法是字符串拼接SQL。假如用户名的输入框里填了 OR 11拼出来的SQL变成WHERE UserName OR 11恒为真直接绕过登录。参数化查询等于让SQL Server把参数当普通值处理不参与语法解析从根上堵掉这类注入。实验报告的问题分析里写一段这个对比是老师爱看的点。需要说明的是Password字段以明文存储只是课设为了演示方便真实系统必须存哈希。报告里注明“本系统为教学演示采用明文生产环境应使用SHA-256加盐”比假装没这个问题体面得多。3.2 房态列表的DataGridView绑定选择空闲房间的正确姿势主界面的房间列表用DataAdapter把数据灌进DataTable再绑定private void LoadRoomList() { string sql SELECT RoomId, RoomNumber, RoomType, Price, Status FROM Rooms ORDER BY RoomNumber; DataTable table new DataTable(); using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { adapter.Fill(table); // 断开式查询Fill 之后连接就可以立即关闭 } dgvRooms.DataSource null; // 先置空再赋值强制控件刷新 dgvRooms.DataSource table; }这里用DataAdapter加DataTable而不是SqlDataReader原因是两种模式的行为不同SqlDataReader是连接式读取必须保持连接打开、边读边用读完才能关DataAdapter的Fill方法是一次性把结果全部装进内存表连接马上可以释放数据还在适合绑定给DataGridView。ORDER BY RoomNumber保证界面按房号排不会出现202跑到201前面的混乱。入住按钮拿到的是当前选中行的RoomId从DataGridView里取值是这样的先判断dgvRooms.CurrentRow是否为空再用Convert.ToInt32(row.Cells[RoomId].Value)取主键。取到Id之后调用入住方法不要在前台代码里拼SQL——这是分层意识的开端后面第5章会展开。3.3 入住登记的事务写法两条SQL必须同时成功入住这个动作要改两张表把Rooms的状态改成“占用”再往Orders插一条入住记录。这两条SQL必须同时成功或同时失败中间不能断电所以用手动事务private void CheckIn(int roomId, string customerName, string phone) { string sqlUpdateRoom UPDATE Rooms SET Status 占用 WHERE RoomId roomId AND Status 空闲; string sqlInsertOrder INSERT INTO Orders (RoomId, CustomerName, Phone, CheckInTime, Status) VALUES (roomId, customerName, phone, GETDATE(), 在住); using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction transaction conn.BeginTransaction(); // 开启事务 try { using (SqlCommand cmdRoom new SqlCommand(sqlUpdateRoom, conn, transaction)) { cmdRoom.Parameters.AddWithValue(roomId, roomId); int affected cmdRoom.ExecuteNonQuery(); if (affected 0) { // 影响行数为0房间已被占用或房间不存在 throw new Exception(房间已被占用请刷新列表后重试); } } using (SqlCommand cmdOrder new SqlCommand(sqlInsertOrder, conn, transaction)) { cmdOrder.Parameters.AddWithValue(roomId, roomId); cmdOrder.Parameters.AddWithValue(customerName, customerName); cmdOrder.Parameters.AddWithValue(phone, phone); cmdOrder.ExecuteNonQuery(); } transaction.Commit(); // 两条都成功才提交 } catch { transaction.Rollback(); // 任何一步失败全部回滚 throw; } } }把“房间是否已被占用”的校验放在UPDATE语句的WHERE条件里是这套代码里最巧妙的一步。两个前台同时给同一间房开单数据库层面只有一个人能拿到affected1另一个拿到0被挡在业务层外面。如果先SELECT再UPDATE两步之间有空窗并发场景就会翻车。事务代码有两条硬规矩第一BeginTransaction之后创建的每个SqlCommand都要把transaction对象传进构造函数否则执行时报“ExecuteNonQuery requires the command to have a transaction”第二catch块里必须Rollback并rethrow有人只弹了个MessageBox就继续走看上去界面没报错实际上连接关闭时事务自动回滚前面的订单根本写进库。退房逻辑是镜像操作先UPDATE Orders把Status改成“已退房”、写入CheckOutTime和TotalPrice再UPDATE Rooms把状态改回“空闲”同样包事务。价格按“退房时间减入住时间”取天数不足一天按一天算这种业务规则要写在注释里否则答辩时被问“住了三小时怎么算”会卡壳。4. 绕开六个容易翻车的点连接串、文件权限与数据刷新这一节是这类项目最常见的血泪经验每一条都在验收现场真实发生过。前三条是环境问题后三条是代码里的隐性问题。按“现象 → 原因 → 解决”的顺序写方便对照排查。4.1 连接字符串里的Data Source不能写死现象代码在自己电脑上跑得好好的发给别人打开就报“在建立到服务器的连接时发生错误”。原因App.config里写的是Data SourceDESKTOP-ABC123这台电脑的名字只有你自己有。换个环境实例名变了连接串当然失效。解决把Data Source改成.或者.\\SQLEXPRESS点号代表本机默认实例换电脑不用改。顺手确认三件事SQL Server服务有没有启动、实例名是不是默认、Windows防火墙有没有放行1433端口。这三件事按顺序查能解决九成连接失败。4.2 附加数据库报5120MDF文件的权限与路径问题现象右键附加时报错“无法打开物理文件‘...HotelDB.mdf’操作系统错误 5: 拒绝访问”错误码5120。原因MDF或LDF放在系统盘、Program Files这类受保护目录SQL Server服务账户没有NTFS读写权限。另一种可能是文件被标记只读或者正被某个程序占用。解决把 .mdf 和 .ldf 一起拷贝到普通目录比如 D:\HotelData右键属性 → 安全 → 给Users组完全控制权限再用sysadmin账号重新附加。注意附加时两个文件必须都在如果只选MDF而LDF丢了会报3154错误。这是文件权限问题不是代码问题实验报告的环境配置章节写一句解决方法能省下答辩时大量口舌。4.3 退房后房间状态没变事务没提交或被异常吞掉现象入住后房间状态一直是“空闲”或者退房成功但房态没变回住。原因代码里写了事务但最后忘了transaction.Commit()或者在catch里只做了提示没Rollback连接关闭时事务自动回滚前面的UPDATE全部白做。解决严格按第3章3.3的模板Commit在两条命令都成功后调用catch块Rollback后rethrow。另外一个小细节SqlCommand创建时没传transaction对象会报运行时错误有人以为是事务没开启其实是命令根本不知道自己在事务里。排查时先看构造函数再看有没有Commit。4.4 DataGridView刷新无反应DataSource要置null再赋值现象退房结账后调用LoadRoomList()界面还是显示旧的“占用”状态。原因DataGridView绑定的还是同一个DataTable对象引用控件只有在绑定关系变化时才刷新DataSource指同一块内存时不会触发重新读取。解决绑定前先dgvRooms.DataSource null;再赋新的DataTable。把这个操作写进LoadRoomList的固定开头以后再也不会遇到“点了刷新像没点”的诡异状况。如果项目用了BindingSource可以调用bindingSource.ResetBindings()但课设里直接操作DataGridView的占多数置null最省事。4.5 AddWithValue的隐式类型转换中文和特殊字符触发索引失效现象登录偶尔很慢或者输入带中文的用户名就查不到结果。原因字段类型是varchar(20)AddWithValue默认把字符串当nvarchar传。SQL Server要把列隐式转换成nvarchar才能比较表小的时候感觉不到表一大或列上有索引时索引就废了。解决显式声明参数类型和长度不让框架猜cmd.Parameters.Add(name, SqlDbType.VarChar, 20).Value userName;把SqlDbType和字段长度写死比AddWithValue靠谱。这个坑在实验报告里写出来是亮点很多同学根本没听过隐式转换。4.6 实验报告和代码对不上改完代码忘记同步文档现象答辩老师按实验报告翻到某一页问“这个功能在哪里”你在代码里找不到对应窗体。这是带实验报告的压缩包项目最尴尬的翻车。原因代码经过多次改动实验报告还是初版截图。写代码一时爽文档没人同步验收时就变成各说各话。解决交稿前花半小时做文档对齐。打开报告目录每个功能章节去代码里找到对应窗体和方法名不一致就二选一要么还原代码要么重新截图替换。再把“报告第X章对应项目里哪个Form”做成一页映射表附在报告最后。这看起来是文档问题本质是版本管理缺失答辩时手里有一张对应表比凭记忆硬撑稳得多。5. 把单机版改造成三层架构课设向简历项目迈进的重构方向很多交上来的代码把SQL全部堆在Form的按钮事件里一个窗体文件里既有界面又有数据库逻辑上千行挤在一起。能跑但一被问“换数据库怎么办”“怎么测试”就露馅。这里给出一个半小时能改完的三层化方案实体类、数据访问层DAL、业务层BLL。这套分层写法跟很多C#上位机项目的数据库模块是同一个套路学会一次到处能用。5.1 实体类用属性而不是public字段为绑定和序列化铺路public class Room { public int RoomId { get; set; } public string RoomNumber { get; set; } public string RoomType { get; set; } public decimal Price { get; set; } public string Status { get; set; } }为什么用属性而不是public字段DataGridView可以直接把属性名当作列名绑定序列化成JSON的时候反射依赖的是属性将来要给属性加MaxLength这类数据注解也只有属性有位置放。Price用decimal对应SQL Server的decimal/money如果这里写double从DataReader里读出来再赋值就会多一次转换埋下精度隐患。5.2 DAL数据访问层用using包住连接和命令的固定写法public class RoomDAL { private readonly string _connStr; public RoomDAL(string connStr) { _connStr connStr; } // 查询空闲房间返回DataTable由上层决定如何展示 public DataTable GetAvailableRooms() { string sql SELECT RoomId, RoomNumber, RoomType, Price FROM Rooms WHERE Status 空闲 ORDER BY RoomNumber; using (SqlConnection conn new SqlConnection(_connStr)) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { DataTable table new DataTable(); adapter.Fill(table); return table; } } }连接字符串从构造函数传进来DAL不关心它来自配置文件还是别的地方。DAL的职责只有SQL和参数不碰界面控件。这里有一个常被误解的点每次new SqlConnection再释放会不会性能很差不会。ADO.NET的连接池默认开启用的连接串相同就会复用池里的连接实际开销远小于想象。这也是为什么DataAdapter的Fill写法在大量数据访问场景依然能打。5.3 BLL业务层让界面只调用方法而不直接操作SqlCommandpublic class RoomManager { private readonly RoomDAL _roomDal; public RoomManager(string connStr) { _roomDal new RoomDAL(connStr); } // 给UI用的入口入住前的业务校验也放在这里 public DataTable GetAvailableRooms() { return _roomDal.GetAvailableRooms(); } }界面的调用方式变成RoomManager manager new RoomManager(connStr); DataTable rooms manager.GetAvailableRooms();UI不知道DAL的存在更不接触SqlCommand。将来要加“客人已有在住订单不能再开房”的规则在RoomManager里加一个方法就行不用改窗体。BLL里加业务规则后按钮事件变短出问题时在RoomManager里打断点定位比在按钮Click里翻几屏代码高效得多。这个层次结构就是面试官问“三层架构各是什么职责”时你能拿出手的实证。5.4 App.config的连接字符串环境变化只改配置文件把连接串从代码里挪到App.config是整套方案里性价比最高的一步configuration connectionStrings add nameHotelDB connectionStringData Source.;Initial CatalogHotelDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient/ /connectionStrings /configuration读取连接串的代码string connStr ConfigurationManager.ConnectionStrings[HotelDB].ConnectionString;App.config编译后会生成exe.config部署时改配置文件即可不用重新编译。注意两点.NET Framework项目要手动引用System.Configuration.dll才能用ConfigurationManager新项目如果跑在.NET 6以上就把ConfigrationManager换成Microsoft.Extensions.Configuration包去读appsettings.json思路一致只是包不同。Integrated SecurityTrue表示Windows身份登录适合课设本机演示如果SQL Server开了混合模式可以改成User IDsa;Passwordxxx但明文密码不建议写进交付文档。6. 验证与进阶演示之前必须走完的完整流程和我想改进的地方这套系统交付前我建议在干净环境里走一遍完整验证清单装好SQL Server Express附加MDF文件改配置文件里的Data Source指向当前实例名按顺序操作——登录、查看空闲房、办理入住、确认房态变“占用”、退房、确认房态变回“空闲”关闭程序重新打开确认订单还在。把这份清单打印出来验收时照着做比临时乱点可靠得多。进阶方向按性价比排序我第一个推荐改密码存储把Users表的Password字段改成binary(32)登录时用SHA-256算一遍再比对。课设用明文可以理解但想写进简历就必须改。第二个是批量导入业务要导入一批房间数据时SqlBulkCopy比逐条INSERT快一个数量级代码也不复杂。第三个是统计报表加一个窗体用GROUP BY算入住率、营业额这是展示数据库能力最直观的地方。我当年交这类课设时没做验证清单上台演示到一半数据库服务没启动只能对着黑屏讲PPT。后来养成的习惯是演示前先把数据库服务和连接串检查一遍并把检查清单写进文档。这套系统的代码量不大但把数据库设计、C#数据访问、事务一致性和文档同步都串起来了值得你花一个晚上把它跑透、改透。希望帮到你。本文还有配套的精品资源点击获取