简介:这是一套基于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 房间表、订单表、用户表的字段设计:每个字段为什么要存在
先看房间表,这是整个系统状态流转的中心:
| 字段 | 类型 | 说明 |
|---|---|---|
| RoomId | int 主键自增 | 房间唯一标识,程序中一切关联都走这个Id |
| RoomNumber | varchar(10) 唯一约束 | 房号如201,业务上不允许重复 |
| RoomType | varchar(20) | 房型,标准间/大床房/套房 |
| Price | decimal(10,2) | 门市价,用decimal避免浮点误差 |
| Status | varchar(10) | 空闲/占用,程序里维护状态流转 |
RoomNumber一定要加唯一约束,否则界面显示两个201,入住时谁也不知道该改哪条记录。Price用decimal而不用float,是面试必背的点:float和double是二进制近似存储,算钱会出0.1+0.2不等于0.3的问题。Status用字符串不用bit,是为了后续扩展“打扫中”“已停用”这类状态。
订单表Orders是连接房间和客人的桥梁:
| 字段 | 类型 | 说明 |
|---|---|---|
| OrderId | int 主键自增 | 订单号 |
| RoomId | int 外键 | 指向Rooms.RoomId,不直接存房间号 |
| CustomerName | varchar(20) | 客人姓名 |
| Phone | varchar(20) | 联系电话 |
| CheckInTime | datetime | 入住时间,默认GETDATE() |
| CheckOutTime | datetime | 退房时间,入住时为NULL |
| TotalPrice | decimal(10,2) | 结账金额,退房时写入 |
| Status | varchar(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 = N'D:\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 '1'='1,拼出来的SQL变成WHERE UserName = '' OR '1'='1',恒为真,直接绕过登录。参数化查询等于让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条件里,是这套代码里最巧妙的一步。两个前台同时给同一间房开单,数据库层面只有一个人能拿到affected=1,另一个拿到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 Source=DESKTOP-ABC123,这台电脑的名字只有你自己有。换个环境实例名变了,连接串当然失效。
解决:把Data Source改成.或者.\\SQLEXPRESS,点号代表本机默认实例,换电脑不用改。顺手确认三件事:SQL Server服务有没有启动、实例名是不是默认、Windows防火墙有没有放行1433端口。这三件事按顺序查,能解决九成连接失败。
4.2 附加数据库报5120:MDF文件的权限与路径问题
现象:右键附加时报错“无法打开物理文件‘...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业务层:让界面只调用方法而不直接操作SqlCommand
public 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 name="HotelDB" connectionString="Data Source=.;Initial Catalog=HotelDB;Integrated Security=True" providerName="System.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 Security=True表示Windows身份登录,适合课设本机演示;如果SQL Server开了混合模式,可以改成User ID=sa;Password=xxx,但明文密码不建议写进交付文档。
6. 验证与进阶:演示之前必须走完的完整流程和我想改进的地方
这套系统交付前,我建议在干净环境里走一遍完整验证清单:装好SQL Server Express,附加MDF文件;改配置文件里的Data Source指向当前实例名;按顺序操作——登录、查看空闲房、办理入住、确认房态变“占用”、退房、确认房态变回“空闲”;关闭程序重新打开,确认订单还在。把这份清单打印出来,验收时照着做,比临时乱点可靠得多。
进阶方向按性价比排序,我第一个推荐改密码存储:把Users表的Password字段改成binary(32),登录时用SHA-256算一遍再比对。课设用明文可以理解,但想写进简历就必须改。第二个是批量导入业务,要导入一批房间数据时,SqlBulkCopy比逐条INSERT快一个数量级,代码也不复杂。第三个是统计报表,加一个窗体用GROUP BY算入住率、营业额,这是展示数据库能力最直观的地方。
我当年交这类课设时没做验证清单,上台演示到一半数据库服务没启动,只能对着黑屏讲PPT。后来养成的习惯是演示前先把数据库服务和连接串检查一遍,并把检查清单写进文档。这套系统的代码量不大,但把数据库设计、C#数据访问、事务一致性和文档同步都串起来了,值得你花一个晚上把它跑透、改透。希望帮到你。
本文还有配套的精品资源,点击获取