C#酒店客房管理系统课程设计:源码解析与SQL Server数据库实战
2026/9/23 10:43:34 网站建设 项目流程

简介:基于C#的酒店客房管理系统课程设计完整包,面向高校计算机相关专业学生与C#入门开发者。系统实现登录验证、用户管理、房间类型与房间管理、物品管理、预订查询、入住登记、退房结算、员工设置等完整流程,采用C#与SQL Server 2005协同开发,结构清晰,适合课程设计或毕业设计参考。压缩包共369个文件,核心为cs源码与resx界面资源,配合xsd数据集定义、mdf/ldf数据库文件、sln解决方案及doc设计报告,包体大小仅3.08MB,轻量易得。已有36人学习浏览。内含可编译工程与数据库脚本,既可对照报告梳理业务逻辑,也能直接运行演示;模块划分有助于二次开发,是学习WinForm分层编程与酒店管理信息系统的实用素材。

1. C# 酒店客房管理系统课程设计:这套源码能复现到什么程度

不少朋友下载 C# 酒店客房管理系统源码,是为了课程设计答辩或者快速补一个能跑的 Demo。这套基于 C# + SQL Server 2005 的客房管理系统,功能覆盖了登录、用户添加、房间与房间类型管理、物品管理、预订查询、客户入住、出租方式管理、退房以及员工设置,典型的中小规模 WinForms 课设项目。拿到手之后最关心的通常是三件事:数据库能不能挂上、界面能不能跑起来、核心业务逻辑能不能讲清楚。这篇文章会从项目结构、数据层设计、入住退房流程到常见坑位逐层拆解,帮你把源码变成真正能讲解、能演示的东西。

2. 读懂项目结构:从文件清单反推架构

2.1 文件清单里藏着的信息

解压后你会看到宾馆管理.csprojapp.config宾馆管理DataSet17.Designer.cs宾馆管理DataSet18.Designer.cs等文件。csproj是 Visual Studio 的项目文件,双击就能用 VS 打开;app.config是配置文件,连接字符串就写在里面;而DataSet17DataSet18这两个强类型数据集文件暴露了系统的数据访问方式——不是手写SqlConnection到处 new,而是通过 Visual Studio 的 DataSet 设计器生成强类型数据集和 TableAdapter。

为什么课程设计普遍这么做?因为强类型 DataSet 有智能提示,字段名写错会在编译期报错而不是运行期抛异常,答辩时打开DataSet.Designer.cs能直接看到每张表的映射关系,比贴一堆 SQL 字符串好看得多。代价是项目结构里会有大量自动生成的代码,不建议手改,改错了整个数据层都可能崩。

<connectionStrings> <add name="宾馆管理.Properties.Settings.宾馆管理ConnectionString" connectionString="Data Source=.;Initial Catalog=HotelDB;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings>

上面是app.config里典型的连接字符串配置。Data Source=.指向本机默认数据库实例,Initial Catalog是数据库名称,Integrated Security=True表示用 Windows 身份验证登录 SQL Server。如果你的机器装的是命名实例(比如SQLEXPRESS),需要把Data Source改成.\SQLEXPRESS,否则运行时会报“建立到服务器的连接时发生错误”。

2.2 数据库核心表设计与关系

系统功能点对应到数据库层面,至少需要这几张表:房间表(Room)、房间类型表(RoomType)、客人表(Guest)、入住记录表(CheckIn)、预订记录表(Reservation)、物品表(Item)、用户表(Users)、员工表(Employee)。其中房间表和房间类型表通过类型编号关联,入住记录表通过房间号与房间表关联、通过客人编号与客人表关联。

CREATE TABLE RoomType ( TypeId INT PRIMARY KEY IDENTITY(1,1), TypeName NVARCHAR(50) NOT NULL, Price DECIMAL(10,2) NOT NULL, Description NVARCHAR(200) ); CREATE TABLE Room ( RoomId INT PRIMARY KEY, RoomNo NVARCHAR(20) NOT NULL, TypeId INT FOREIGN KEY REFERENCES RoomType(TypeId), Status INT DEFAULT 0, Remark NVARCHAR(200) ); CREATE TABLE CheckIn ( CheckInId INT PRIMARY KEY IDENTITY(1,1), RoomId INT FOREIGN KEY REFERENCES Room(RoomId), GuestName NVARCHAR(50) NOT NULL, IdCard NVARCHAR(18), CheckInDate DATETIME DEFAULT GETDATE(), CheckOutDate DATETIME, TotalAmount DECIMAL(10,2) );

Room.Status字段建议用整数状态机:0 空闲、1 已入住、2 脏房待打扫、3 维修中。这样在 C# 代码里就是一个switchenum,比存字符串好维护。入住时把房间状态从 0 改成 1,退房时改成 2(待清洁)。这个设计在课程设计答辩里属于加分项,因为它在数据层面把业务流程落下来了,而不是只在界面上做个样子。

2.3 用强类型 DataSet 还是手写 SQL

打开DataSet17.Designer.cs能看到 TableAdapter 自动生成的查询方法,比如GetDataByStatus(int status),这对应房间状态查询;InsertCheckIn(...)对应入住记录的插入。强类型数据集的好处是 UI 层直接this.checkInTableAdapter.Fill(this.hotelDBDataSet.CheckIn)就能把数据灌进DataGridView

但也有一个实际的坑:如果数据库里改了表结构(比如加了字段),VS 里必须右键数据集设计器选择“配置”重新执行向导,否则运行时会报“列名无效”。最常见的翻车现场是机房电脑装的 SQL Server 版本和你本机不一致,附加数据库后字段类型对不上,填表时直接抛强类型数据集校验异常。我的习惯是拿到源码后先不急着看窗体代码,而是先打开数据集设计器确认每张表的查询语句,再去看界面。

3. 登录、用户与员工管理:权限模块的通用写法

3.1 登录窗体判断逻辑

登录界面的实现思路基本一致:接收TextBox输入的用户名和密码,拼 SQL 去用户表查询,返回行数大于 0 就放行,否则提示“用户名或密码错误”。课程设计源码里很多是字符串拼接,但建议换成参数化查询,在答辩时能主动说出“这里用了参数化查询防止 SQL 注入”,属于亮点回答。

private void btnLogin_Click(object sender, EventArgs e) { string username = txtUsername.Text.Trim(); string password = txtPassword.Text.Trim(); if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) { MessageBox.Show("用户名和密码不能为空"); return; } string connStr = ConfigurationManager.ConnectionStrings["HotelDB"].ConnectionString; string sql = "SELECT COUNT(*) FROM Users WHERE Username=@u AND Password=@p"; using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@u", username); cmd.Parameters.AddWithValue("@p", password); conn.Open(); int count = (int)cmd.ExecuteScalar(); if (count > 0) { this.DialogResult = DialogResult.OK; this.Close(); } else { MessageBox.Show("用户名或密码错误"); } } }

几个关键点:ExecuteScalar()返回首行首列,正是COUNT(*)的结果;AddWithValue是参数化查询最常见的写法,注意前后不能加单引号,否则参数化就失去意义了。这段代码在课设里基本可以原样照搬,但建议改成直接调用数据集里的UsersTableAdapter,风格更统一。

3.2 用户添加与员工设置

用户添加功能通常放在管理员菜单里,需要做的操作是:检查用户名是否重复、密码是否为空、确认密码是否一致,然后INSERT INTO Users。源码里密码大多是明文存储,这里可以做一个很轻量的升级:用MD5哈希后存储。

private string GetMd5Hash(string input) { using (MD5 md5 = MD5.Create()) { byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); StringBuilder sb = new StringBuilder(); foreach (byte b in bytes) { sb.Append(b.ToString("x2")); } return sb.ToString(); } } private void btnAddUser_Click(object sender, EventArgs e) { string username = txtNewUser.Text.Trim(); string password = txtNewPassword.Text.Trim(); string confirm = txtConfirmPassword.Text.Trim(); if (password != confirm) { MessageBox.Show("两次密码不一致"); return; } // 检查用户名是否已存在 string connStr = ConfigurationManager.ConnectionStrings["HotelDB"].ConnectionString; string checkSql = "SELECT COUNT(*) FROM Users WHERE Username=@u"; // 这里省略了连接代码,逻辑与上面登录一致 // 插入,密码存哈希值 string insertSql = "INSERT INTO Users(Username, Password, RoleId) VALUES(@u, @p, @r)"; // 执行时 @p 传入 GetMd5Hash(password) }

MD5 在真正安全要求高的场景里已经被判了死刑,但作为课程设计里展示“不是明文存密码”的意识足够了。员工设置功能本质上是同一个表的增删改查,只是多加了一个角色字段,比如RoleId=1管理员、RoleId=2前台。源码里如果员工设置是独立窗体,大概率是同一套模板复制出来的,重点看它有没有做权限判断——有些课设源码里普通用户也能进员工设置界面,这是逻辑漏洞,答辩时被问到容易卡壳。

4. 入住与退房:整个系统的核心业务闭环

4.1 房间状态与出租方式

入住操作的前提是理解出租方式管理。出租方式通常指全日租、钟点房、长包房等,每种方式对应不同的计费规则。系统里通常单独维护一张RentType表,字段包括RentTypeIdTypeNamePriceRate,比如钟点房按小时计费、全日租按天计费。房间类型表里的Price是基础价格,出租方式的PriceRate是倍率,实际费用 = 房间价格 × 倍率 × 时长。

这个设计在数据流上很顺:入住时选择房间号 → 系统自动带出房间类型价格 → 再选择出租方式 → 前台输入预计入住天数 → 系统计算预收款。源码里如果出租方式编辑是独立完成的模块,说明作者在刻意区分“房间本身的价格”和“售卖方式的价格策略”,这在答辩时可以主动提一句逻辑分层。

4.2 入住流程:一个事务里完成多表写入

入住操作涉及三件事:往入住记录表插一条数据、把房间状态改成已入住、把客人信息写入客人表。这三件事不能分开执行,否则会出现“记录写了但房间状态没变”的不一致情况。正确做法是放在一个 SQL 事务里。

private bool CheckIn(int roomId, string guestName, string idCard, int rentTypeId, int days, decimal prepay) { string connStr = ConfigurationManager.ConnectionStrings["HotelDB"].ConnectionString; using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction transaction = conn.BeginTransaction(); try { // 1. 查询房间类型价格和出租方式倍率 string queryPrice = "SELECT rt.Price * rent.PriceRate FROM Room r " + "JOIN RoomType rt ON r.TypeId = rt.TypeId " + "CROSS JOIN RentType rent WHERE r.RoomId=@roomId AND rent.RentTypeId=@rentTypeId"; SqlCommand cmdPrice = new SqlCommand(queryPrice, conn, transaction); cmdPrice.Parameters.AddWithValue("@roomId", roomId); cmdPrice.Parameters.AddWithValue("@rentTypeId", rentTypeId); decimal unitPrice = (decimal)cmdPrice.ExecuteScalar(); // 2. 插入入住记录 string insertCheckIn = "INSERT INTO CheckIn(RoomId, GuestName, IdCard, CheckInDate, RentTypeId, TotalAmount, Prepay) " + "VALUES(@roomId, @guestName, @idCard, GETDATE(), @rentTypeId, @totalAmount, @prepay)"; SqlCommand cmdInsert = new SqlCommand(insertCheckIn, conn, transaction); cmdInsert.Parameters.AddWithValue("@roomId", roomId); cmdInsert.Parameters.AddWithValue("@guestName", guestName); cmdInsert.Parameters.AddWithValue("@idCard", idCard); cmdInsert.Parameters.AddWithValue("@rentTypeId", rentTypeId); cmdInsert.Parameters.AddWithValue("@totalAmount", unitPrice * days); cmdInsert.Parameters.AddWithValue("@prepay", prepay); cmdInsert.ExecuteNonQuery(); // 3. 更新房间状态为已入住 string updateRoom = "UPDATE Room SET Status=1 WHERE RoomId=@roomId"; SqlCommand cmdUpdate = new SqlCommand(updateRoom, conn, transaction); cmdUpdate.Parameters.AddWithValue("@roomId", roomId); cmdUpdate.ExecuteNonQuery(); transaction.Commit(); return true; } catch { transaction.Rollback(); return false; } } }

这段代码回答了一个很多初学者问的问题:为什么需要事务?因为入住这个动作一旦中途出错(比如插入记录成功但更新状态失败),数据就不一致了。BeginTransaction()之后所有SqlCommand都要带上同一个transaction对象,提交时用transaction.Commit(),出错时Rollback()。注意最后using块会自动释放连接,所以不需要手动conn.Close()

4.3 退房流程与费用结算

退房是入住的逆向操作:从入住记录表里找到未退房的记录 → 计算实际费用 → 更新入住记录的退房时间和实收金额 → 把房间状态改为待清洁。计算费用时注意一个细节:如果入住时收了预收款,退房时要算出应退差价。

private decimal CheckOut(int checkInId, int roomId, int actualDays) { string connStr = ConfigurationManager.ConnectionStrings["HotelDB"].ConnectionString; decimal refundAmount = 0; using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction transaction = conn.BeginTransaction(); try { // 查询入住记录的预付金额和单价 string query = "SELECT Prepay, TotalAmount / DATEDIFF(DAY, CheckInDate, GETDATE()) FROM CheckIn WHERE CheckInId=@id"; SqlCommand cmdQuery = new SqlCommand(query, conn, transaction); cmdQuery.Parameters.AddWithValue("@id", checkInId); SqlDataReader reader = cmdQuery.ExecuteReader(); decimal prepay = 0; decimal unitPrice = 0; if (reader.Read()) { prepay = (decimal)reader[0]; unitPrice = (decimal)reader[1]; } reader.Close(); decimal actualAmount = unitPrice * actualDays; refundAmount = prepay - actualAmount; // 更新入住记录 string updateCheckIn = "UPDATE CheckIn SET CheckOutDate=GETDATE(), ActualAmount=@amount WHERE CheckInId=@id"; SqlCommand cmdUpdate = new SqlCommand(updateCheckIn, conn, transaction); cmdUpdate.Parameters.AddWithValue("@amount", actualAmount); cmdUpdate.Parameters.AddWithValue("@id", checkInId); cmdUpdate.ExecuteNonQuery(); // 房间状态改为待清洁 string updateRoom = "UPDATE Room SET Status=2 WHERE RoomId=@roomId"; SqlCommand cmdRoom = new SqlCommand(updateRoom, conn, transaction); cmdRoom.Parameters.AddWithValue("@roomId", roomId); cmdRoom.ExecuteNonQuery(); transaction.Commit(); } catch { transaction.Rollback(); } } return refundAmount; }

这里用DATEDIFF(DAY, CheckInDate, GETDATE())反推实际住的天数,是最粗粒度的做法,存在跨天时间边界问题。实际课设里可以接受,但你应该心里有数:如果客人是凌晨 1 点入住、当天中午退房,这个算法会算出 0 天。更合理的做法是在界面上让操作员手动选择退房日期,或者按小时计算分段收费。源码里多数采用手动输入天数的方案,这也不算错,反而在答辩时你可以主动说出这个边界情况,展示思考深度。

5. 避坑手册:SQL Server 2005 环境下的常见问题

5.1 附加数据库失败,提示“版本高于当前实例”

现象:附加.mdf文件时,SQL Server Management Studio 报错“数据库版本高于当前实例支持的版本”。

原因:源码里的.mdf是用高版本 SQL Server(如 2012 或 2016)生成的,而你的机器上装的是 SQL Server 2005 Express,低版本实例无法直接加载高版本数据库文件。

解决:不要试图降级数据库文件,正确做法是在高版本 SQL Server 上附加数据库后,用“生成脚本”功能把表结构和数据导出成.sql文件,再到 2005 实例里执行脚本重建库。实际操作中更省事的方案是:装一个 SQL Server 2019 Developer 版,直接在 2019 上附加,然后检查连接字符串指向的实例名是否匹配。作为课程设计环境,装新版不丢人,运行效果反而更稳定。

5.2 连接字符串实例名不匹配

现象:程序一启动就报“建立到服务器的连接时发生错误”,或者“在建立与服务器的连接时出错”。

原因:app.config里的Data Source=.指向默认实例,但你的机器上 SQL Server 是命名实例(常见的是.\SQLEXPRESS),两者对不上。机房电脑尤其容易出现——很多课件里让学生装 Express 版,实例名默认带SQLEXPRESS后缀。

解决:打开app.config,把连接字符串的Data Source改成你的实际实例名。不知道怎么查实例名的话,打开 SQL Server Management Studio 登录窗口,服务器名称下拉框里显示的内容就是你要填的。如果是本机默认实例且安装了最新版,直接写Data Source=localhost也行。

5.3 “文件正在使用中”无法附加或还原数据库

现象:附加数据库时提示“无法打开物理文件,拒绝访问”或“文件被占用”,重启 SQL Server 服务也不行。

原因:.mdf文件被另一个 SQL Server 实例进程占用,或者文件权限不足。常见场景是你先前用管理工具附加过一次,关闭工具后进程没释放锁。

解决:先用如下命令确认数据库状态,然后强制离线再附加:

EXEC sp_detach_db 'HotelDB';

然后复制.mdf.ldf到你自己有完全控制权的目录(比如C:\Data\),右键设置文件安全权限,给 SQL Server 服务账户读取和写入权限,再重新附加。这个坑非常玄学,有时候明明路径没问题就是附加不上,十有八九是权限问题。代码里也可以预建数据库,直接从CREATE DATABASE脚本起步,绕过附加过程。

5.4 DataGridView 显示数据后修改不生效

现象:程序运行后能正常显示房间列表,但直接在DataGridView单元格里改数据、点保存按钮后,数据库里没有变化。

原因:很多课设源码里的 DataGridView 采用只读绑定,或者保存按钮只调用了dataGridView.Update()而没有调用 TableAdapter 的Update方法。Update()方法只是刷新界面显示,不会把改动写回数据库。真正写回需要调用生成的 TableAdapter 更新方法,并且确保数据集里每张表有主键。

解决:不要试图在 UI 事件里逐个单元格写更新 SQL,直接使用设计器生成的绑定方法:

private void btnSaveRoom_Click(object sender, EventArgs e) { this.roomTableAdapter.Update(this.hotelDBDataSet.Room); MessageBox.Show("保存成功"); }

使用前提:Room表必须定义了主键,否则 TableAdapter 无法生成UPDATE语句,保存时会报“对于不返回任何键列信息的 SELECT 语句,不支持动态 SQL 生成”。这是把 DataSet 拖进设计器时没有正确配置主键导致的,需要回到数据源管理器里检查表定义。

5.5 查询时当天入住记录查不到

现象:预订查询里输入当天日期,查询结果为空,但数据库里明明有数据。

原因:日期比较时直接用了CheckInDate = @date,而数据库里CheckInDateDATETIME类型,包含了时分秒。前端传入的时间通常是“2024-06-01 00:00:00”,实际存储的是“2024-06-01 14:23:11”,等值比较永远查不出来。

解决:查询条件改成日期范围,或者用CONVERT把字段转成短日期格式再比较。推荐用范围写法,可以走索引:

SELECT * FROM CheckIn WHERE CheckInDate >= @start AND CheckInDate < DATEADD(DAY, 1, @end);

这个坑在入住、退房、记录查询里几乎都会出现,血泪经验是凡是涉及日期查询的代码,都默认按范围查询来写,不要用等号。源码里如果原本用的是等号,建议全部替换掉。

6. 进阶落地:把系统迁到新版 SQL Server 并写一份能演示的测试脚本

拿到了这套源码,光让它跑起来不算本事,需要展示出“我真正调试过、验证过”的痕迹。最推荐的操作是完成两件事:把数据库从 SQL Server 2005 迁移到 Express 或 Developer 最新版,以及写一段自动化测试脚本验证核心流程。因为新机器上装 2005 往往不兼容 Windows 10/11,而且新版 Management Studio 对初学者友好得多,答辩演示时不容易出幺蛾子。

先做数据库迁移。步骤很简单:在高版本 SQL Server 上附加数据库文件,右键数据库选择“任务 → 生成脚本”,勾选“包含数据”,生成一个完整的.sql文件,然后在目标实例里执行这个脚本。执行成功后打开app.config把连接字符串改成目标实例名。到这里数据库环境就已经换好了,不需要改任何一行 C# 代码——因为数据访问层走的是 TableAdapter 生成的标准SqlClient,协议层面完全兼容。

接着写一段简单的验证脚本,用Console项目或直接在 SQL Server 里跑一段多步骤 SQL,模拟“添加用户 → 创建房型 → 入住 → 退房 → 查询记录”的完整链路:

BEGIN TRAN; INSERT INTO RoomType(TypeName, Price) VALUES ('标准间', 268.00); DECLARE @typeId INT = SCOPE_IDENTITY(); INSERT INTO Room(RoomId, RoomNo, TypeId, Status) VALUES (1, '1001', @typeId, 0); INSERT INTO CheckIn(RoomId, GuestName, IdCard, CheckInDate, RentTypeId, TotalAmount, Prepay) VALUES (1, N'张三', '110101199001011234', GETDATE(), 1, 536.00, 600.00); UPDATE Room SET Status = 1 WHERE RoomId = 1; SELECT * FROM CheckIn WHERE GuestName = N'张三'; ROLLBACK;

包在ROLLBACK里是为了让测试数据不污染正式库,反复执行也不会产生垃圾记录。这段脚本的价值是:你可以很清楚地展示“入住时房间状态从 0 变成 1,退房时从 1 变成 2”的状态流转过程。在答辩现场,用数据来证明程序逻辑比口头描述更有说服力。

最后说一个我自己踩过的坑。以前拿到类似的课设源码,第一反应是打开 Visual Studio 直接按 F5,结果报错一脸懵。后来学乖了,拿到任何源码包,第一步永远是先打开app.config确认数据库连接,第二步打开数据表设计器检查主键与关系,第三步跑一遍数据库脚本,最后才运行程序。从那以后我每次接手别人的 C# 项目都强制走一遍这个流程,五分钟内就能判断这套源码值不值得深入看。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询