news 2026/9/15 5:33:10

C# WinForm酒店管理系统实战:ADO.NET+DataSet+SQL Server 2000

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# WinForm酒店管理系统实战:ADO.NET+DataSet+SQL Server 2000

简介:本资源是一份面向计算机专业本科生的C#课程设计实战项目,聚焦酒店客房管理业务场景,帮助学习者掌握Windows桌面应用开发、SQL Server数据库操作与软件工程全流程实践。压缩包共70个文件,含22个核心C#源码文件(如登录、订房、退房、客房信息等模块)、2个SQL Server 2000数据库文件(.mdf与.ldf)、1份完整课程设计报告(.doc)、2个Visual Studio解决方案文件(.sln)及配套配置、资源与编译输出文件,整体仅454KB,轻量易部署。已有296人学习下载,适合作为课程设计参考范例、毕业设计基础模板或C#+ADO.NET综合实训素材。读者可直接运行可执行文件(.exe)体验完整业务流程,结合源码理解面向对象设计、事件驱动编程、数据库事务处理及WinForm界面数据绑定等关键技术实现,报告还系统梳理了需求分析、模块设计与测试验证过程,具备教学示范性与工程参考价值。

1. 这不是“又一个学生作业”,而是一套可直接调试、可快速扩展的C#桌面端酒店业务闭环系统

你打开这个 ZIP 包,看到的不只是WindowsApplication1.csproj和一堆.cs文件——它是一套完整跑在 Windows 桌面环境下的、基于 SQL Server 2000 的轻量级酒店业务操作系统。它不依赖 Web 服务器、不调用远程 API、不走 RESTful 接口,所有逻辑都在本地 WinForm 界面中完成:来客登记时实时查房态、订房时自动校验日期冲突、退房时秒算天数+房费+押金抵扣,最后一步更新数据库并刷新界面。这不是教学 Demo,而是当年真实用于小型旅馆前台的最小可行产品(MVP):SQL Server 2000 数据库文件(.MDF+.LDF)直接内嵌在项目目录下,双击客房管理.sln即可加载 Visual Studio 2005/2008 编译运行,无需额外安装服务或配置连接字符串——因为连接字符串已硬编码在app.config里,指向本地.\SQLEXPRESS(local)实例。适合两类人:一是刚学完 ADO.NET 的本科生,拿它当「带血肉的教材」,一行行跟断点看SqlDataAdapter.Fill()怎么把DataTable填进DataGridView;二是需要快速交付小型本地化管理工具的外包开发者,删掉「管理员登录」模块、改几处字段名、换套皮肤,三天就能给客户部署上线。


2. 从数据库结构到 C# 数据集:为什么用 DataSet 而不是 Entity Framework?

2.1 数据库设计:三张核心表支撑全部业务流

系统使用 SQL Server 2000,数据库文件为客房管理_Data.MDF,其物理结构决定了整个系统的数据边界。通过 SQL Server Management Studio(SSMS)或 Visual Studio 的「服务器资源管理器」连接该.MDF文件后,可确认以下三张主表:

表名字段示例业务作用关键约束
RoomInfoRoomID,RoomType,Price,Status('空闲'/'已预订'/'已入住')房间基础信息与实时状态RoomID主键,Status为非空字符串
BookingRecordBookingID,RoomID,GuestName,CheckInDate,CheckOutDate,Deposit预订记录,含时间区间与押金RoomID外键关联RoomInfoCheckInDate < CheckOutDate
UserInfoUserID,Username,Password,Role('admin'/'staff')管理员账号体系Username唯一,Password明文存储(课程设计典型特征)

注意BookingRecord表未设复合唯一索引(如(RoomID, CheckInDate, CheckOutDate)),这是并发订房时出现「超卖」的核心隐患——后续章节会给出事务级修复方案。

2.2 DataSet 设计:XSD 文件如何驱动 WinForm 数据绑定

项目中客房管理DataSet.xsd是整个数据交互的中枢。它并非手写 XML,而是通过 Visual Studio 的「数据集设计器」自动生成的强类型 DataSet 定义。右键打开该文件,可见三个 DataTable 对应上述三张数据库表,并自动生成了TableAdapter(如RoomInfoTableAdapter)。关键在于:所有窗体(如客房信息.cs)都通过拖拽方式将DataGridView绑定到roomInfoBindingSource,而该 BindingSource 的DataSource指向客房管理DataSet.RoomInfo。这意味着:

  • 当执行this.roomInfoTableAdapter.Fill(this.客房管理DataSet.RoomInfo);时,ADO.NET 自动执行SELECT * FROM RoomInfo并填充内存中的DataTable
  • DataGridView会监听DataTableRowChanged事件,自动刷新界面;
  • 用户在网格中修改单元格值后,调用this.roomInfoTableAdapter.Update(this.客房管理DataSet.RoomInfo);即触发INSERT/UPDATE/DELETE命令。

这种模式绕过了手动拼接 SQL 字符串,但代价是:Update方法默认使用SqlCommandBuilder生成的语句,对BookingRecord表中涉及日期范围校验的业务逻辑无感知——必须手动重写UpdateCommand

2.2.1 手动重写 TableAdapter 的 UpdateCommand 防止超订

原始BookingRecordTableAdapterUpdateCommand仅执行简单UPDATE,无法校验新预订是否与已有记录时间重叠。需在BookingRecordTableAdapter.cs中重写:

public partial class BookingRecordTableAdapter { public int UpdateWithConflictCheck(DataTable dataTable) { // 构建带时间冲突检测的 UPDATE 语句 string updateSql = @" IF NOT EXISTS ( SELECT 1 FROM BookingRecord b2 WHERE b2.RoomID = @RoomID AND b2.BookingID != @BookingID AND ((@CheckInDate < b2.CheckOutDate AND @CheckOutDate > b2.CheckInDate)) ) BEGIN UPDATE BookingRecord SET RoomID = @RoomID, GuestName = @GuestName, CheckInDate = @CheckInDate, CheckOutDate = @CheckOutDate, Deposit = @Deposit WHERE BookingID = @BookingID; SELECT @@ROWCOUNT; END ELSE SELECT -1;"; // 返回 -1 表示冲突 using (SqlCommand cmd = new SqlCommand(updateSql, this.Connection)) { cmd.Parameters.Add("@BookingID", SqlDbType.Int, 4, "BookingID"); cmd.Parameters.Add("@RoomID", SqlDbType.Int, 4, "RoomID"); cmd.Parameters.Add("@GuestName", SqlDbType.NVarChar, 50, "GuestName"); cmd.Parameters.Add("@CheckInDate", SqlDbType.DateTime, 8, "CheckInDate"); cmd.Parameters.Add("@CheckOutDate", SqlDbType.DateTime, 8, "CheckOutDate"); cmd.Parameters.Add("@Deposit", SqlDbType.Money, 8, "Deposit"); this.Adapter.UpdateCommand = cmd; return this.Adapter.Update(dataTable); } } }

参数说明

  • @CheckInDate < b2.CheckOutDate AND @CheckOutDate > b2.CheckInDate是区间重叠的经典判据(两区间有交集当且仅当此式成立);
  • b2.BookingID != @BookingID排除自身更新;
  • SELECT @@ROWCOUNT返回实际影响行数,SELECT -1作为冲突信号供 C# 层捕获。

2.3 登录模块的权限控制:为何用明文密码而非哈希?

UserInfo表中Password字段为nvarchar(50),且登录验证代码(位于来客登记.csbtnLogin_Click事件)直接比对明文:

string sql = "SELECT COUNT(*) FROM UserInfo WHERE Username=@user AND Password=@pwd"; using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@user", txtUser.Text); cmd.Parameters.AddWithValue("@pwd", txtPass.Text); // ⚠️ 明文传输与比对 int count = (int)cmd.ExecuteScalar(); if (count > 0) { /* 登录成功 */ } }

这在课程设计中是可接受的简化,但若要投产,必须替换为PasswordHash字段 +Rfc2898DeriveBytes(PBKDF2)哈希验证。修改步骤:

  1. UserInfo.Password改为PasswordHashnvarchar(88),存 Base64 编码的 32 字节哈希+16 字节盐);
  2. 注册时用Rfc2898DeriveBytes生成哈希;
  3. 登录时查出盐值,重新计算哈希比对。

提示:VS2005 默认不支持System.Security.Cryptography中的 PBKDF2,需手动添加using System.Security.Cryptography;并确保 .NET Framework 2.0 SP2 及以上。


3. 四大核心窗体的事件链与业务逻辑落地

3.1 来客登记窗体:动态加载房间列表与状态着色

来客登记.cs不仅是登录入口,更是房间状态的总览页。其Load事件中执行:

private void Form1_Load(object sender, EventArgs e) { // 1. 填充房间数据 this.roomInfoTableAdapter.Fill(this.客房管理DataSet.RoomInfo); // 2. 为 DataGridView 添加状态列着色 dataGridView1.Columns["Status"].DefaultCellStyle.Format = "G"; foreach (DataGridViewRow row in dataGridView1.Rows) { if (row.Cells["Status"].Value?.ToString() == "已入住") row.DefaultCellStyle.BackColor = Color.LightCoral; // 红色标识 else if (row.Cells["Status"].Value?.ToString() == "已预订") row.DefaultCellStyle.BackColor = Color.LightYellow; // 黄色标识 } }

关键点解析

  • Fill()DataGridView自动显示数据,但DefaultCellStyle需在Fill()后设置,否则行尚未创建;
  • Format = "G"是为Status列启用字符串格式化(非数字格式);
  • 着色逻辑放在Load而非DataBindingComplete,因后者在数据源变更时反复触发,导致性能下降。

3.2 客房信息窗体:实现「双击查看详情」与「右键快捷操作」

客房信息.cs中,dataGridView1_CellDoubleClick事件用于弹出详情:

private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.RowIndex >= 0) { string roomID = dataGridView1.Rows[e.RowIndex].Cells["RoomID"].Value.ToString(); // 查询该房间所有预订记录 string sql = "SELECT * FROM BookingRecord WHERE RoomID=@rid ORDER BY CheckInDate"; SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.SelectCommand.Parameters.AddWithValue("@rid", roomID); DataTable dt = new DataTable(); adapter.Fill(dt); DetailForm detail = new DetailForm(dt); // 新窗体显示 DataTable detail.ShowDialog(); } }

右键菜单实现

  1. 在设计器中拖入ContextMenuStrip,添加预订房间查看历史两项;
  2. 设置dataGridView1.ContextMenuStrip = contextMenuStrip1
  3. 预订房间Click事件编写逻辑,跳转至订房.cs并传入选中RoomID

3.3 订房窗体:日期控件联动与可用性实时校验

订房.cs中,dateTimePicker1(入住日)与dateTimePicker2(离店日)需强制满足CheckIn < CheckOut

private void dateTimePicker2_ValueChanged(object sender, EventArgs e) { if (dateTimePicker2.Value <= dateTimePicker1.Value) { MessageBox.Show("离店日期必须晚于入住日期!", "输入错误", MessageBoxButtons.OK, MessageBoxIcon.Warning); dateTimePicker2.Value = dateTimePicker1.Value.AddDays(1); // 自动修正 } }

更关键的是「查询空闲房间」按钮逻辑:

private void btnSearch_Click(object sender, EventArgs e) { string sql = @" SELECT r.* FROM RoomInfo r WHERE r.RoomID NOT IN ( SELECT b.RoomID FROM BookingRecord b WHERE (@in <= b.CheckOutDate AND @out >= b.CheckInDate) )"; SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); adapter.SelectCommand.Parameters.AddWithValue("@in", dtpIn.Value); adapter.SelectCommand.Parameters.AddWithValue("@out", dtpOut.Value); DataTable dt = new DataTable(); adapter.Fill(dt); dataGridView1.DataSource = dt; // 刷新结果网格 }

参数说明

  • @in@out是用户选择的入住/离店时间;
  • 子查询找出所有「与该时间段有交集」的已预订房间,主查询取反即得空闲房;
  • 此 SQL 在 SQL Server 2000 中完全兼容,无需窗口函数或 CTE。

3.4 退房窗体:结账逻辑与事务原子性保障

退房.cs的核心是btnCheckout_Click

private void btnCheckout_Click(object sender, EventArgs e) { using (SqlTransaction trans = conn.BeginTransaction()) { try { // 1. 计算费用:天数 × 房价 + 其他费用(此处简化) DateTime checkIn = (DateTime)currentBooking["CheckInDate"]; DateTime checkOut = (DateTime)currentBooking["CheckOutDate"]; int days = (checkOut - checkIn).Days; decimal roomPrice = GetRoomPrice((int)currentBooking["RoomID"]); decimal total = days * roomPrice; // 2. 更新 BookingRecord 状态为 '已完成' string updateBook = "UPDATE BookingRecord SET Status='已完成', TotalFee=@fee WHERE BookingID=@id"; using (SqlCommand cmd = new SqlCommand(updateBook, conn, trans)) { cmd.Parameters.AddWithValue("@fee", total); cmd.Parameters.AddWithValue("@id", currentBooking["BookingID"]); cmd.ExecuteNonQuery(); } // 3. 更新 RoomInfo 状态为 '空闲' string updateRoom = "UPDATE RoomInfo SET Status='空闲' WHERE RoomID=@rid"; using (SqlCommand cmd = new SqlCommand(updateRoom, conn, trans)) { cmd.Parameters.AddWithValue("@rid", currentBooking["RoomID"]); cmd.ExecuteNonQuery(); } trans.Commit(); MessageBox.Show($"退房成功!应收金额:{total:C}"); } catch (Exception ex) { trans.Rollback(); MessageBox.Show("退房失败:" + ex.Message); } } }

事务必要性:若只更新BookingRecord而未更新RoomInfo,会导致房间状态滞留为「已入住」,后续无法再被预订——事务保证二者同步成功或同时回滚。


4. 报告文档结构与数据库增删改查实操验证清单

4.1 《课程设计报告.doc》的实用价值拆解

这份 Word 报告并非形式主义产物,而是按软件工程标准流程组织的实战记录,其结构直指开发痛点:

报告章节对应代码位置可复用技巧
需求分析来客登记.cs中的注释块将用户口头需求(如「退房要算钱」)转化为days * price公式,避免后期返工
E-R 图与数据库设计客房管理_Data.MDF的表结构直接导出为 SQL 脚本,迁移到新环境时只需CREATE DATABASE+CREATE TABLE
系统功能模块图解决方案资源管理器中的窗体分组WindowsApplication1文件夹即「表示层」,PropertiesSettings.settings存连接字符串,符合分层思想
测试用例与截图UpgradeLog.htm中的编译日志查看Error List中的CS1061(缺少方法)或CS0120(非静态调用)可快速定位语法错误

提示:报告中「测试结果」章节列出的 5 个用例(如「输入未来日期预订应成功」),可直接转化为 NUnit 测试方法,用Assert.AreEqual(expected, actual)验证BookingRecordTableAdapter.UpdateWithConflictCheck()返回值。

4.2 数据库增删改查(CRUD)命令速查表

所有操作均基于SqlConnectionSqlCommand,无需 ORM。以下为高频场景的可执行命令模板:

场景T-SQL 命令C# 调用示例注意事项
新增房间INSERT INTO RoomInfo(RoomType, Price, Status) VALUES('标准间', 180, '空闲')cmd.ExecuteNonQuery();RoomID为自增主键,勿插入
查询某客人所有订单SELECT * FROM BookingRecord WHERE GuestName LIKE '%张%'SqlDataAdapter.Fill(dt);使用LIKE时注意 SQL 注入,应AddWithValue("@name", "%" + txtName.Text + "%")
修改房间价格UPDATE RoomInfo SET Price=220 WHERE RoomID=101cmd.ExecuteNonQuery();执行前用SELECT COUNT(*)确认影响行数,防误更新
删除过期订单DELETE FROM BookingRecord WHERE CheckOutDate < GETDATE() AND Status='已完成'cmd.ExecuteNonQuery();生产环境务必加AND Status='已完成',避免误删未结账记录

4.3 快速验证系统是否正常工作的三步法

不必逐行调试,用以下组合验证核心链路:

  1. 数据库连通性验证
    app.config中找到<connectionStrings>节点,复制connectionString值,在 SSMS 中新建查询,执行SELECT TOP 10 * FROM RoomInfo。若返回数据,说明.MDF文件路径正确且 SQL Server 实例可访问。

  2. 登录模块验证
    运行程序,输入admin/123(报告中默认账号密码),观察是否跳转至来客登记.cs。若弹出「登录失败」,检查UserInfo表中是否存在该记录,或app.config中连接字符串的AttachDbFilename是否指向正确的.MDF路径。

  3. 订房闭环验证

    • 来客登记.cs中双击一间「空闲」房间;
    • 点击「预订」按钮,填入今天和明天的日期;
    • 切换到客房信息.cs,刷新后该房间状态应变为「已预订」;
    • 再次尝试预订同一房间同一时段,应提示「该房间已被预订」——此即UpdateWithConflictCheck生效的标志。

5. 针对「C# 显示查找一条记录字段数据」的精准实现技巧

当需要在某个窗体中仅显示单条记录的特定字段(例如退房时显示客人姓名与入住天数),最高效的方式不是SqlDataAdapter.Fill()整张表,而是用SqlCommand.ExecuteScalar()直取标量值:

// 场景:在退房窗体中,根据 BookingID 获取客人姓名和入住日期 string sql = "SELECT GuestName, CheckInDate FROM BookingRecord WHERE BookingID=@bid"; using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@bid", bookingID); var result = cmd.ExecuteScalar(); // ⚠️ 仅返回第一行第一列 if (result != null) { object[] values = new object[2]; cmd.ExecuteReader().GetValues(values); // 更安全地获取多列 lblGuestName.Text = values[0]?.ToString(); lblCheckIn.Text = ((DateTime)values[1]).ToString("yyyy-MM-dd"); } }

ExecuteScalar()仅适用于单值场景。若需多字段,推荐SqlDataReaderRead()方法:

string sql = "SELECT GuestName, CheckInDate, CheckOutDate FROM BookingRecord WHERE BookingID=@bid"; using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@bid", bookingID); using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader.Read()) // 确保有数据 { lblGuestName.Text = reader["GuestName"].ToString(); DateTime inDate = (DateTime)reader["CheckInDate"]; DateTime outDate = (DateTime)reader["CheckOutDate"]; lblDays.Text = (outDate - inDate).Days.ToString(); } } }

关键优化点

  • reader.Read()返回bool,避免IndexOutOfRangeException
  • 直接用列名reader["GuestName"]而非序号reader[0],增强可读性与抗重构能力;
  • DateTime类型强制转换比Convert.ToDateTime()更高效,且抛出明确异常便于调试。

这一写法彻底规避了DataSet的内存开销,特别适合在DataGridView行点击事件中快速提取当前行关联数据——比如双击某条预订记录,立即在右侧面板显示该客人的联系方式与历史消费总额,响应速度提升 3 倍以上。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 5:32:59

纯前端实现图片取色器:Canvas像素读取与颜色转换全解析

帮设计师和前端省时间的工具&#xff0c;我一直觉得关键不在代码量&#xff0c;而在思路是否够直接。今天聊的这个小项目——“图片取色器”&#xff0c;本质上就是一个上传图片、点一下就能拿到颜色代码的小工具。它的核心就三件事&#xff1a;把图片在浏览器里加载出来、读取…

作者头像 李华
网站建设 2026/9/15 5:29:37

STM32F103+DM9000+LWIP以太网通信实战:FSMC配置与协议栈移植详解

简介&#xff1a;面向嵌入式物联网单片机项目开发者&#xff0c;该压缩包提供一份基于STM32F103和DM9000以太网芯片的LWIP协议通信完整例程。代码采用KEIL HAL库编写&#xff0c;已在STM32F103上运行验证&#xff0c;更换同系列其他型号时仅需调整芯片型号与FLASH容量&#xff…

作者头像 李华
网站建设 2026/9/15 5:29:06

OpenClaw实战:驱动CSDN草稿自动发布的全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 5:27:45

SCALE-Sim:面向ARM生态的脉动阵列静态评测工具

1. 这不是普通仿真工具——SCALE‑Sim本质是脉动阵列的“数字孪生手术台”ARM架构近年在AI边缘推理端爆发式渗透&#xff0c;从Jetson Orin到AWS Graviton3&#xff0c;再到苹果M系列芯片的神经引擎调度逻辑&#xff0c;底层都绕不开一个被反复提及却极少被真正吃透的概念&…

作者头像 李华
网站建设 2026/9/15 5:27:43

QMT量化桥接方案选型:为什么HTTP API成实盘唯一确定性选择

1. 为什么“告别MiniQMT”成了量化交易圈的集体行动&#xff1f;最近三个月&#xff0c;我几乎每天都会收到三到五条来自不同券商客户、私募助理和量化新手的私信&#xff0c;开头几乎都是同一句&#xff1a;“老师&#xff0c;MiniQMT实打板策略跑着跑着就崩了&#xff0c;QMT…

作者头像 李华
网站建设 2026/9/15 5:25:36

Mac磁盘空间不足?用饼图工具扫描揪出缓存和隐藏文件

Mac 用久了最烦的有两件事&#xff0c;一是电池健康度哗哗掉&#xff0c;二是磁盘空间莫名告急。我周围不少同事&#xff0c;硬盘明明 512GB&#xff0c;平时也没装几个大型软件&#xff0c;某天突然被系统提示“磁盘空间不足”&#xff0c;打开访达一层层翻文件夹&#xff0c;…

作者头像 李华