简介:面向C#初学者的酒店管理系统项目源码,基于WinForm界面框架实现,覆盖用户管理、房客管理、客房管理和出入管理四大核心模块,适合用于课程设计、毕业设计或入门企业级桌面应用开发。资源压缩包共54个文件,整体仅159KB,以25个.cs源码文件为主,并包含.resx/.resources界面资源、Access数据库文件及.sln/.csproj工程配置,可运行exe也随包附赠,目录按BLL、DAL、UI、Entity分层组织,便于对照学习。系统使用Access作为默认数据库,围绕入住登记、退房处理、客房状态跟踪、用户登录与权限分配等真实业务展开,完整展示了事件驱动编程、SQL的增删改查以及ADO.NET数据库连接的典型写法。通过阅读源码可以同时掌握C#面向对象设计、WinForm控件布局、分层架构思想以及数据库迁移到SQL Server、MySQL的扩展思路,而非止于简单抄写代码。已有2958人学习下载,资源体量虽小但功能链路完整,适合希望快速跑通酒店管理项目并深入理解各模块交互逻辑的开发者。
1. 这个标题背后是一套能跑的前台系统,不是玩具项目
“C# WinForm 酒店管理系统项目源码”这个标题,搜索量一直不小。搜它的人大概分两类:一类是拿它做课程设计、毕业设计的学生,另一类是想给自家小旅馆或民宿找一套能改的桌面管理系统的从业者。这套系统说白了就是把酒店前台最核心的两件事——房态管理和客人账目——搬到 Windows 桌面上。用 C# 的 WinForm 技术栈,配合 SQL Server 或 SQLite 做数据落盘,再把预订、入住、退房、结账这些流程串成一套可操作的程序。它的价值不在于代码量有多大,而在于它把数据库设计、界面交互、报表打印和异常处理这些工程问题都压缩进了一个能看见、能点到的窗口里。适合想练手 C# 桌面开发的新手,也适合需要一套可改可用的管理系统做二次开发的小型酒店。这个方向值得做,但先说一句实话:标题里的“源码”通常是给你改的起点,不是让你直接部署的成品。
2. 把系统拆成数据库、窗体和业务逻辑三层,先立住骨架
2.1 数据库先落地:五张表就能撑起一个前台
酒店管理系统的核心不在界面,在数据模型。物业、房型、客房、订单、客户这几张表是最小可行集合。房型表(RoomType)按房间类型定价格和床位,客房表(Room)挂到房型下,订单表(Order)记录每笔预订和入住的流水,客户表(Customer)存身份证和联系方式,再用一张操作日志表(OperationLog)把关键操作留下来。
表要精但字段要够用。我用 SQL Server 做过一套,表结构大概长这样:
CREATE TABLE RoomType ( TypeId INT PRIMARY KEY IDENTITY(1,1), TypeName NVARCHAR(50) NOT NULL, Price DECIMAL(10, 2) NOT NULL, BedCount INT DEFAULT 1, Description NVARCHAR(200) ); CREATE TABLE Room ( RoomId INT PRIMARY KEY IDENTITY(1,1), RoomNo NVARCHAR(10) NOT NULL UNIQUE, TypeId INT FOREIGN KEY REFERENCES RoomType(TypeId), Status TINYINT DEFAULT 0, -- 0 空房, 1 已入住, 2 已预订, 3 维修 FloorNo INT, Remark NVARCHAR(200) );这里把Status设计成TINYINT而不是字符串,是为了前端转换方便。0代表空房,1代表入住,2代表预订,3代表维修。房态图刷新时直接按这个数字去查字典,界面显示和数据库解耦。Price用DECIMAL(10,2)而不是FLOAT,是为了避免浮点精度造成的账目误差——这一点做结账模块时会反复碰到。
订单表是整系统的枢纽,字段设计比房型表复杂得多:
CREATE TABLE [Order] ( OrderId INT PRIMARY KEY IDENTITY(1,1), OrderNo NVARCHAR(20) NOT NULL, CustomerId INT FOREIGN KEY REFERENCES Customer(CustomerId), RoomId INT FOREIGN KEY REFERENCES Room(RoomId), CheckInDate DATETIME, CheckOutDate DATETIME, ActualCheckOut DATETIME, OrderStatus TINYINT DEFAULT 0, -- 0 预订, 1 入住, 2 已结账, 3 已取消 TotalAmount DECIMAL(10, 2) DEFAULT 0, Deposit DECIMAL(10, 2) DEFAULT 0, PayMethod NVARCHAR(20) );ActualCheckOut专门留出来,是为了应对顾客提前退房或延时退房的情况。标准退房时间字段和实际退房时间字段分开存,报表才能算清楚“早退扣款”和“延时加收”。这套设计不复杂,但比很多教材里只有一张订单表的方案要抗造得多。
2.2 WinForm 三层架构:UI层、业务层和数据层的分工
很多 WinForm 项目翻车的起点,就是所有代码全写窗体的button_Click事件里。一个窗体几百行代码,后面连维护的勇气都没有。我一般会按三层来分:
- UI 层:窗体、控件、事件绑定,只负责交互和显示
- 业务层(BLL):接收 UI 层的指令,处理业务规则,比如入住时检查房态、退房时计算费用
- 数据层(DAL):封装所有 SQL 和数据库连接,只提供
GetRoomList()、CreateOrder()这种语义化方法
举个直观的例子。DAL 层的连接管理可以统一封装:
public static class DbHelper { private static string connStr = ConfigurationManager.ConnectionStrings["HotelDb"].ConnectionString; public static DataTable Query(string sql) { using (SqlConnection conn = new SqlConnection(connStr)) { SqlDataAdapter da = new SqlDataAdapter(sql, conn); DataTable dt = new DataTable(); da.Fill(dt); return dt; } } }using保证连接用完即关,不手动调Close()也不容易泄漏连接。DataTable在这种桌面管理系统中比实体对象的适用度更高——绑定到DataGridView时天然带列名和行结构,省去手工拼对象的中间层。如果你偏好强类型,可以换成Dapper映射到Room和Order实体,但核心思想不变:数据库操作只在一个文件里出现。
业务层负责把“能不能入住”这种规则集中到一处:
public class RoomService { private RoomDal dal = new RoomDal(); public bool CheckIn(int roomId, int customerId, DateTime checkInDate) { // 业务规则:只有空房才能办理入住 var room = dal.GetRoomById(roomId); if (room.Status != 0) { throw new Exception("该房间当前不是空房状态,无法入住"); } return dal.UpdateRoomStatus(roomId, 1) > 0; } }这里把状态判断放在业务层而不是窗体里,是因为入住和预订都会修改房态,如果两端各自写一套判断,后期改规则时很容易漏改一处。业务层的价值不在于让代码变多,而在于让规则有唯一的归属地。
2.3 登录与权限:从最简单的登录到角色区分
登录模块看起来简单,但有几个容易被忽略的细节。密码不能明文存,建议至少存SHA256哈希;登录成功后要把当前用户对象存到一个全局静态类里,后续所有操作记录日志要用到它;窗体之间传用户信息不要用Tag属性或全局变量到处塞,容易在跳转时丢失。
我习惯写一个静态会话类:
public static class UserSession { public static int UserId { get; set; } public static string UserName { get; set; } public static string Role { get; set; } }登录窗体校验成功后,只负责赋值,不做跳转。由主窗体在Load事件里判断角色,决定哪些按钮可点、哪些菜单可见。如果只做管理员和前台操作员两种角色,用一个Role字符串就够;如果还要分店长、财务,就得引入权限表了。对新手来说,先做角色判断,再演化成权限表,比一开始就设计一张五关联的权限表要可行得多。
登录界面还有一个容易忽略的点:回车键提交。给密码框设置KeyDown事件,按回车触发登录按钮的逻辑,体验会好很多。别小看这个细节,前台服务员要盯着键盘输密码,再腾出右手去点鼠标,一天几百次操作下来效率差别很明显。
3. 前台和后台的实操:房态图、预订入住、退房结账一条龙
3.1 房态图:用 DataGridView 做还是自绘?
房态图是酒店管理系统最直观的门面。常见做法有三种:用Button动态布局、用DataGridView改单元格样式、或者直接自绘UserControl。对新手最友好的是DataGridView,省去自己处理鼠标事件和绘制边界的麻烦。
我用DataGridView做过一版,大致思路是这样:第一列显示楼层,后面的列按房号排列,每个单元格底色表示房态。关键是设置单元格的Tag属性存RoomId,点击时才能反查房间。
private void LoadRoomGrid() { dgvRooms.Rows.Clear(); dgvRooms.Columns.Clear(); dgvRooms.Columns.Add("Floor", "楼层"); DataTable dt = roomDal.GetRoomStatusList(); // 返回 RoomNo, Status, FloorNo var floors = dt.AsEnumerable().Select(r => r["FloorNo"].ToString()).Distinct().OrderBy(f => f); foreach (var floor in floors) { dgvRooms.Columns.Add("F" + floor, floor + "F"); } foreach (var floor in floors) { DataGridViewRow row = new DataGridViewRow(); row.HeaderCell.Value = floor + "F"; dgvRooms.Rows.Add(row); } }这段逻辑看着简单,但有个坑:如果每层房间数不一样,横竖坐标对不齐,房号会错位。所以更稳妥的做法是直接在一个单元格里画出一个房间,用Button或者自绘UserControl,按房间坐标排布,不受楼层房间数不一致的影响。
DataGridView做房态图的隐藏问题在于滚动条。房间多了以后,横向滚动条一拖,房号和房间位置的对应关系容易让人看花眼。我做过一次翻车后换成了动态生成Button的方案:
// 动态生成房态按钮 foreach (DataRow dr in dt.Rows) { Button btn = new Button(); btn.Text = dr["RoomNo"].ToString(); btn.Size = new Size(80, 60); btn.Tag = dr["RoomId"]; // 关键:把主键存到 Tag btn.Click += RoomBtn_Click; // 按楼层和房间序号计算位置 int rowIndex = Convert.ToInt32(dr["FloorNo"]) - 1; int colIndex = roomIndexInFloor; // 每层房间从左到右编号 btn.Location = new Point(20 + colIndex * 90, 20 + rowIndex * 75); panelRooms.Controls.Add(btn); }动态按钮的好处是控件即实体,点击事件可以直接拿到RoomId,跳转到操作窗体时不用再去查一次房号。在几百个房间的规模内,这个方案性能完全够用。滚动容器用Panel不够顺滑,可以换成FlowLayoutPanel或自定义滚动面板。
3.2 预订与入住的代码流程
预订和入住是同一个流程的不同入口,核心都是往Order表写一条记录,并更新Room.Status。区别只在于:预订先不收款,状态置为 2(已预订);入住要收款或登记押金,状态置为 1(已入住)。
这两个操作必须放在一个数据库事务里,否则会出现“订单建成功了但房态没改”或者反过来。事务用SqlTransaction手动控制就行:
public bool CreateOrder(Order order, int newRoomStatus) { using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction tx = conn.BeginTransaction(); try { SqlCommand cmd = new SqlCommand(@" INSERT INTO [Order] (OrderNo, CustomerId, RoomId, CheckInDate, CheckOutDate, OrderStatus, Deposit) VALUES (@OrderNo, @CustomerId, @RoomId, @CheckInDate, @CheckOutDate, @OrderStatus, @Deposit)", conn, tx); cmd.Parameters.AddWithValue("@OrderNo", order.OrderNo); cmd.Parameters.AddWithValue("@CustomerId", order.CustomerId); cmd.Parameters.AddWithValue("@RoomId", order.RoomId); cmd.Parameters.AddWithValue("@CheckInDate", order.CheckInDate); cmd.Parameters.AddWithValue("@CheckOutDate", order.CheckOutDate); cmd.Parameters.AddWithValue("@OrderStatus", order.OrderStatus); cmd.Parameters.AddWithValue("@Deposit", order.Deposit); cmd.ExecuteNonQuery(); SqlCommand cmd2 = new SqlCommand( "UPDATE Room SET Status = @Status WHERE RoomId = @RoomId", conn, tx); cmd2.Parameters.AddWithValue("@Status", newRoomStatus); cmd2.Parameters.AddWithValue("@RoomId", order.RoomId); cmd2.ExecuteNonQuery(); tx.Commit(); return true; } catch { tx.Rollback(); return false; } } }注意两点:所有命令都挂同一个SqlTransaction对象;必须conn.Open()之后才能开启事务。AddWithValue顺手但偶尔会有小坑——如果传入null,它会当成DBNull处理,写DateTime字段时最好显式检查空值。
订单号生成不要用自增 ID 直接展示给客户,会暴露业务量。我一般用DateTime.Now.ToString("yyyyMMddHHmmss") + 房间号后三位拼一个可读性强的单号。前台打印时,客人和财务都看得明白。
3.3 退房结账与账单打印
退房是整个系统中业务规则最密集的部分。延时退房要加收费用;提前退房要不要退款、退多少;押金是冲抵房费还是单独退;有没有迷你吧消费——规则一多,代码就乱。先把计算逻辑独立出来,放到业务层:
public decimal CalcStayAmount(DateTime checkIn, DateTime actualCheckOut, decimal price) { // 不足 4 小时按半天算,超过 4 小时按全天算,这个规则因店而异 TimeSpan ts = actualCheckOut - checkIn; double days = ts.TotalDays; if (days <= 0.5) return price * 0.5m; if (days <= 1) return price; return price * (decimal)Math.Ceiling(days); }Math.Ceiling(days)会把 2.1 天变成 3 天,按整天收费。这个规则一定要画个表格给前台确认,不同酒店差异很大。代码里写死某个规则前,先问清楚老板的收费口径。
结账完成后,要把OrderStatus改成 2(已结账),Room.Status改回 0(空房),这两步同样要开事务。打印小票可以用PrintDocument,直接调用默认打印机。Windows 打印系统是出了名的“玄学”,字体、边距、打印机驱动都有影响。靠谱的做法是提前用PrintPreviewDialog预览,把客户姓名、房间号、入住日期、离店日期、消费明细、实收金额这几项排进一个DrawString模板里,不要画复杂的表格线,打印机的 100 像素以内偏移就能毁掉整个美学。
4. 避坑:WinForm 酒店管理系统最常见的五个翻车点
4.1 翻车点一:DataGridView 绑定后列名显示成英文
现象:把DataTable直接绑到DataGridView.DataSource上,表头显示的列名是TypeId、RoomNo这种英文,或者绑定后想改列名,一刷新又变回去了。
原因:DataGridView自动生成的列名来自DataTable的ColumnName,手动改HeaderText只改显示,不改数据源。当你重新设置DataSource时,列会被重新生成。
解决:在DataGridView上关闭AutoGenerateColumns,手工定义列,把DataPropertyName指向数据源的列名,HeaderText写成中文显示名。后面的DataSource不管怎么刷新,列头都不会变。
dgvRooms.AutoGenerateColumns = false; dgvRooms.Columns.Clear(); dgvRooms.Columns.Add(new DataGridViewTextBoxColumn() { DataPropertyName = "RoomNo", HeaderText = "房号", Width = 80 });4.2 翻车点二:SQL 拼接导致崩溃与注入
现象:查询条件用了文本框输入的房间号,用户输入' OR '1'='1,程序直接返回全部数据,严重时整表被删。
原因:用了字符串拼接 SQL 的方式,用户输入被当成了 SQL 的一部分。酒店前台电脑通常开着外网,这个漏洞属于高危级别。
解决:所有动态条件都用参数化查询。连接查询条件时用WHERE RoomNo = @no这种形式,不要拼字符串。DAL 层养成一个习惯:任何用户输入进 SQL 都得走参数,除非那个字段是硬编码的常量。
4.3 翻车点三:房态图刷新闪屏与卡顿
现象:前台点一下“刷新房态”,整个窗体白屏一闪,按钮重新布局时肉眼可见的跳变。房间超过 100 间,刷新要卡两三秒。
原因:动态生成按钮的方案在每次刷新时把panelRooms.Controls.Clear(),所有按钮销毁重建,Windows 的绘制压力大;另外查询语句没有只查需要的数据,全表扫描。
解决:把Controls.Clear()和Controls.Add()批量操作包进BeginUpdate / EndUpdate模式。更高阶的改法是复用 Button,只更新Text和BackColor,不需要销毁重建。数据库查询加WHERE Status IN (0,1,2)限制范围,别把三年历史订单翻出来。
panelRooms.SuspendLayout(); // 挂起布局,避免多次重绘 panelRooms.Controls.Clear(); // 重新生成按钮的循环代码 panelRooms.ResumeLayout();SuspendLayout对Panel很有用,能明显改善闪屏。如果还不行,把整个窗体DoubleBuffered打开,能进一步减少绘制闪烁。
4.4 翻车点四:日期格式与结算金额精度
现象:退房时计算“住了几晚”结果是小数的循环值,5 天零 6 小时算出来的金额跟财务手工算的对不上;打印账单时DateTime输出成2024/1/5 0:00:00这种带时间的格式,被前台嫌弃。
原因:TimeSpan.TotalDays返回的是double,1.2345 天的概念在酒店结账逻辑里无法精确表达;DateTime的ToString()不指定格式就跟随系统区域设置。
解决:日期统一用DateTime.Now.Date存入,去掉时间部分;计算天数先转整数分钟再换算;金额用decimal累加,不做任何隐式转换;显示格式统一用ToString("yyyy-MM-dd")。结算模块写几个单元测试,把(23:50入住,次日 08:10 退房)这种边界场景跑一遍。
4.5 翻车点五:窗口跳转混乱与资源泄漏
现象:从主窗体打开入住窗体,关掉后再次打开,报错“无法访问已释放的对象”;或者打开 20 次就会内存暴涨,最后程序卡死。
原因:窗体在Show()之后没有持有引用,GC 回收后控件里的资源还挂在上面;或者用了ShowDialog()但没有Dispose()。
解决:打开子窗体用统一封装,关闭时释放资源:
using (CheckInForm form = new CheckInForm(roomId)) { form.ShowDialog(); }这样无论正常关闭还是异常关闭,窗体都会被Dispose()。另外,主窗体的FormClosed事件里要把Application.ExitThread()和释放托盘图标等操作补上,很多系统是关了主窗体但进程还驻留在任务管理器里的。
5. 让源码真正可交付:界面美化、权限细化与验收清单
5.1 界面美化的三个低成本动作
WinForm 的默认外观在 2024 年确实显得陈旧,但“低成本美化”和“重构界面库”是两回事。三个改动就能让观感显著提升:
第一,统一字体。微软雅黑 9pt 起步,标题用 12pt 加粗。第二,按钮统一尺寸和间距,布局用TableLayoutPanel或分区域GroupBox分组。第三,给关键状态上色:空房绿色、入住红色、预订橙色、维修灰色。状态配色一上去,整个系统立刻有了“产品感”,而不是练习稿。
5.2 把列表中的 0 和 1 显示成复选框
前台和管理员操作中,“是否已交押金”“是否已开发票”这类列,用 0/1 文本显示很难扫一眼看出状态。DataGridViewCheckBoxColumn配合TrueValue / FalseValue可以解决:
DataGridViewCheckBoxColumn chk = new DataGridViewCheckBoxColumn(); chk.DataPropertyName = "IsPayDeposit"; chk.TrueValue = 1; chk.FalseValue = 0; dgvOrders.Columns.Add(chk);这里有个关键细节:DataGridViewCheckBoxColumn默认把TrueValue和FalseValue当字符串匹配,数据源是int类型时只要类型一致就行。如果发现复选框显示成空白的,先检查数据源该列是不是DBNull——数据库的可空字段会导致复选框既不勾选也不取消。
5.3 验收清单:交付前逐项打勾
一套源码只有通过验收才谈得上真正可用。我给自己定的验收清单是:所有窗口能连续打开关闭 20 次不报错;断网时系统不无响应而是弹出友好提示;断电重开后房态与数据库一致;两名前台同时操作同一间房不出现重复入住。最后一件事很多人忽略——并发。单机版无所谓,但如果是局域网多人用,必须给Room表的Status更新语句加WHERE Status = @oldStatus条件,防止两个人同时把同一间房办入住。
照着这个方向做,把源码从“能跑”推到“能交付”并不难,难的是把前面那些边界情况当成必修课而不是选修课。我早期做这类系统时吃过一次大亏:交付当天刷新不出房态,差点让前台回到手写登记表。事后定位就是窗体没有Dispose()导致一堆连接挂起。现在我把这条写进了自己的检查清单里,每次验收前先跑一遍窗口开关循环测试。做酒店管理系统源码这件事,代码量本身不大,真正值钱的是这些换过血淋淋教训的经验都能沉淀下来,让人少踩一遍。希望这套拆解思路能帮你在自己的版本上少走几步弯路。
本文还有配套的精品资源,点击获取