简介这份资源是一套基于 C# WinForm 与 SQLServer 实现的酒店管理系统完整项目源码面向计算机相关专业的在校学生、教师及企业开发者尤其适合作为毕业设计、课程设计、作业或项目初期立项演示的参考方案。压缩包共 170 个文件约 2.1MB以 63 个 cs 源码文件为核心配合 17 个 resx 与 resources 资源文件、3 个 csproj 工程文件、1 个 sln 解决方案及 1 个 sql 数据库脚本另有 png、jpg 界面素材与 dll 依赖库整体结构清晰便于按 BLL、DAL 等分层模块检索学习。项目代码均经过测试运行成功答辩评审平均分达到 96 分已有 89 人学习关注。读者可从中获得完整的系统实现思路、数据库建表脚本、分层架构组织方式以及可运行的工程配置既能直接用于学习进阶也可在现有代码基础上修改扩展实现更多业务功能。1. 从一张房态表说起WinFormSQLServer 做酒店管理系统的真实门槛很多人第一次接酒店管理这类需求脑子里浮现的是不就是增删改查。真上手才发现最难的从来不是写窗体而是那张房态表——同一间房今天在住、明天预订、后天待清扫状态一变账、房、客三张表全得跟着动。用 C# 的 WinForm 配 SQLServer 做一套能跑起来的酒店管理系统核心要解决的就是房态驱动业务这件事前台开房、换房、退房、结账每一步都要保证数据库里房间状态和订单状态一致不能出现房已退但订单还挂着这种脏数据。这套方案适合两类人一是刚学完 C# 基础、想找一个完整项目练手的在校生或转行者二是需要给中小型酒店做轻量管理工具的一线开发者。它不追求高并发和分布式胜在结构清晰、部署简单一台装了 SQLServer 的普通机器加一个 exe 就能跑。下面我按先立数据模型、再搭窗体、最后抠细节的顺序把这条路径拆开讲清楚中间会给出可直接抄的建表语句、连接封装和关键业务代码。2. 数据模型先立住五张表撑起整个酒店业务2.1 为什么是这五张表而不是更多酒店管理系统的表设计有个常见误区一上来就建十几张表把权限、日志、配置全塞进去结果主业务还没跑通自己先被外键绕晕。我的经验是最小可用版本只需要五张核心表——房间表、房型表、客户表、订单表、账单表。房型表存价格和床位信息房间表存物理房间号和当前状态客户表存入住人信息订单表记录预订和入住账单表管消费和结算。这样拆的好处是职责单一房间表只管这间房现在能不能住订单表只管谁在什么时候住了哪间房账单表只管这笔钱收没收。三者通过房间号和订单号关联任何一方状态变更都能追溯到源头。等这套跑顺了再往上加权限、日志、会员积分都不迟。2.2 建表语句与字段含义下面这段 SQL 可以直接在 SQLServer 里执行建库建表一次到位。注意房间状态用整型枚举而不是字符串查询和判断都更快。-- 创建数据库 CREATE DATABASE HotelDB; GO USE HotelDB; GO -- 房型表定义价格和床位 CREATE TABLE RoomType ( TypeId INT IDENTITY(1,1) PRIMARY KEY, TypeName NVARCHAR(50) NOT NULL, -- 如标准间大床房 Price DECIMAL(10,2) NOT NULL, -- 每晚单价 BedCount INT NOT NULL DEFAULT 1 ); -- 房间表物理房间与实时状态 CREATE TABLE Room ( RoomId INT IDENTITY(1,1) PRIMARY KEY, RoomNo NVARCHAR(10) NOT NULL UNIQUE, -- 房间号如8301 TypeId INT NOT NULL, RoomState INT NOT NULL DEFAULT 0, -- 0空闲 1已住 2预订 3待清扫 FOREIGN KEY (TypeId) REFERENCES RoomType(TypeId) ); -- 客户表 CREATE TABLE Customer ( CustId INT IDENTITY(1,1) PRIMARY KEY, CustName NVARCHAR(50) NOT NULL, IdCard NVARCHAR(20) NOT NULL, Phone NVARCHAR(20) ); -- 订单表预订与入住记录 CREATE TABLE OrderInfo ( OrderId INT IDENTITY(1,1) PRIMARY KEY, RoomId INT NOT NULL, CustId INT NOT NULL, CheckInTime DATETIME NOT NULL, CheckOutTime DATETIME NULL, OrderState INT NOT NULL DEFAULT 0, -- 0预订 1入住 2已退 FOREIGN KEY (RoomId) REFERENCES Room(RoomId), FOREIGN KEY (CustId) REFERENCES Customer(CustId) ); -- 账单表 CREATE TABLE Bill ( BillId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, Amount DECIMAL(10,2) NOT NULL, PayState INT NOT NULL DEFAULT 0, -- 0未结 1已结 CreateTime DATETIME NOT NULL DEFAULT GETDATE(), FOREIGN KEY (OrderId) REFERENCES OrderInfo(OrderId) );字段设计上有两个点值得说。一是RoomState用 0/1/2/3 而不是中文前端显示时再映射成空闲/已住/预订/待清扫这样数据库层面判断逻辑干净。二是OrderInfo里CheckOutTime允许为 NULL因为预订阶段还没退房时间这个 NULL 本身就是业务信号不要给它设默认值。2.3 状态流转的约束怎么保证光建表不够还得保证状态流转合法。比如一间房不能同时有两条入住中的订单。这个约束在应用层做校验最灵活但数据库层加个唯一索引能兜底-- 同一房间同一时刻只能有一条未退订单 CREATE UNIQUE INDEX UX_Room_ActiveOrder ON OrderInfo(RoomId) WHERE OrderState IN (0, 1);这是 SQLServer 支持的筛选索引只对预订和入住状态的订单生效已退订单不受影响。加上它之后哪怕应用层代码有 bug 重复插入数据库也会直接拒绝省掉一堆对账的麻烦。3. 用 WinForm 把业务串起来连接封装与核心窗体3.1 数据库连接别散落在每个窗体里新手最容易犯的错是在每个窗体的按钮事件里现写SqlConnection连接字符串复制十几遍。改一次服务器地址要翻遍所有文件。正确做法是抽一个SqlHelper静态类把连接字符串放配置文件增删改查统一走它。// SqlHelper.cs —— 统一数据访问入口 public static class SqlHelper { // 连接字符串从 App.config 读取不硬编码 private static readonly string connStr ConfigurationManager.ConnectionStrings[HotelDB].ConnectionString; // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } // 执行查询返回 DataTable public static DataTable ExecuteQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter adapter new SqlDataAdapter(sql, conn)) { if (ps ! null) adapter.SelectCommand.Parameters.AddRange(ps); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } // 执行标量查询用于取单个值 public static object ExecuteScalar(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteScalar(); } } }using语句保证连接用完即关params SqlParameter[]让调用方传参方便同时天然防注入——所有值都走参数而不是拼字符串。连接字符串放在App.config的connectionStrings节点里部署时改配置就行不用重新编译。3.2 开房操作一次事务里改两张表开房是整套系统里最典型的跨表操作往订单表插一条记录同时把房间状态改成已住。这两步必须在一个事务里否则插了订单没改房态房间就假空闲了。// 开房插入订单 更新房态同一事务 public static bool CheckIn(int roomId, int custId) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 第一步插入订单 string sqlOrder INSERT INTO OrderInfo(RoomId, CustId, CheckInTime, OrderState) VALUES(roomId, custId, GETDATE(), 1); SqlCommand cmd1 new SqlCommand(sqlOrder, conn, tran); cmd1.Parameters.AddWithValue(roomId, roomId); cmd1.Parameters.AddWithValue(custId, custId); cmd1.ExecuteNonQuery(); // 第二步更新房间状态为已住 string sqlRoom UPDATE Room SET RoomState 1 WHERE RoomId roomId AND RoomState 0; SqlCommand cmd2 new SqlCommand(sqlRoom, conn, tran); cmd2.Parameters.AddWithValue(roomId, roomId); int rows cmd2.ExecuteNonQuery(); // 如果房间不是空闲状态说明被人抢先了回滚 if (rows 0) { tran.Rollback(); return false; } tran.Commit(); return true; } catch { tran.Rollback(); throw; } } } }这里有个关键细节UPDATE Room的 WHERE 条件带了RoomState 0。如果两个前台同时给同一间房开房第二个人的 UPDATE 会返回 0 行事务回滚订单也不会插入。这比先查再改可靠得多避免了并发下的超卖。参数roomId和custId由界面传入GETDATE()用数据库时间而不是客户端时间防止机器时间不准导致入住时间错乱。3.3 房态图窗体用 FlowLayoutPanel 动态生成房间按钮前台最常用的界面是房态图——一屏看到所有房间的状态点一下就能操作。用FlowLayoutPanel加动态生成的Button实现最省事房间多了自动换行。// 加载房态图 private void LoadRoomMap() { flowRooms.Controls.Clear(); DataTable dt SqlHelper.ExecuteQuery( SELECT RoomId, RoomNo, RoomState FROM Room ORDER BY RoomNo); foreach (DataRow row in dt.Rows) { Button btn new Button(); btn.Text row[RoomNo].ToString(); btn.Width 80; btn.Height 60; btn.Tag row[RoomId]; // 把房间ID挂在Tag上 btn.BackColor GetStateColor((int)row[RoomState]); btn.Click RoomBtn_Click; // 统一事件处理 flowRooms.Controls.Add(btn); } } // 根据状态返回颜色 private Color GetStateColor(int state) { switch (state) { case 0: return Color.LightGreen; // 空闲 case 1: return Color.LightCoral; // 已住 case 2: return Color.LightBlue; // 预订 case 3: return Color.LightGray; // 待清扫 default: return Color.White; } }把RoomId塞进按钮的Tag属性点击事件里取出来就知道操作的是哪间房不用去解析按钮文字。颜色映射单独抽成方法以后想改配色只动一处。RoomBtn_Click里根据当前房态弹出不同菜单——空闲房弹开房/预订已住房弹退房/换房逻辑清晰。4. 避坑与排查五个让我返工的血泪经验4.1 现象退房后房间还是红色前台以为没退成功原因退房代码只更新了订单状态忘了同步改房间状态。这是最典型的改一半问题订单表OrderState变成 2但Room.RoomState还停在 1。解决退房和开房一样必须包在事务里两条 UPDATE 一起提交。我后来养成的习惯是凡是涉及房态变更的操作先想清楚哪几张表要动列出来再写代码别边写边想。4.2 现象程序在自己电脑跑得好好的拷到酒店前台机器就报连接失败原因连接字符串里写的是Data Sourcelocalhost或者用了 Windows 身份验证换台机器 SQLServer 实例名和登录方式都变了。解决连接字符串统一用Data Source.;Initial CatalogHotelDB;User Idsa;Passwordxxx这种显式账号密码方式部署前确认目标机器 SQLServer 开了混合验证模式。更稳妥的做法是把连接字符串放App.config现场改配置不用动代码。4.3 现象查询订单时界面卡死几秒原因在 UI 线程里直接执行了耗时查询或者DataGridView绑定了上万行数据一次性渲染。解决数据量大的查询用分页DataGridView只绑当前页。如果确实要跑慢查询用async/await把数据库操作放到后台线程别阻塞 UI。酒店系统单表数据量通常不大但订单表跑一年下来也有几万行分页是必须的。4.4 现象金额计算出现 0.01 的误差对账对不上原因金额字段用了float或double浮点数在累加时会有精度丢失。解决所有金额字段一律用DECIMAL(10,2)C# 里对应decimal类型不要用double。这个坑在账单累加时特别明显住十天每天 199.9用 double 加出来可能是 1998.9999999。4.5 现象删除房型时报外键冲突但界面上明明没房间在用原因房间表里有历史房间引用了这个房型只是那些房间状态是停用没显示在主界面。解决删除房型前先查Room表有没有引用有引用就改成逻辑删除——给房型表加个IsActive字段标记停用而不是物理删除。酒店系统里房型、房间这类基础数据一律逻辑删除保留历史订单的可追溯性。5. 进阶技巧把账单结算做成可验证的闭环前面四章把主流程跑通了最后说一个我踩过最深的坑——账单结算。很多教程讲到退房时算钱就停了但真实场景里账单必须能验证、能追溯否则前台和客人对不上账就是一笔糊涂账。我的做法是给账单表加一个结算快照机制。退房时不是简单算个总数而是把房费、消费、押金逐项列出来每一项都带来源。这样客人质疑时能指着明细说清楚每一笔。// 退房结算生成带明细的账单 public static decimal CheckOut(int orderId) { // 1. 查出订单和房间信息 string sqlInfo SELECT o.CheckInTime, r.RoomNo, rt.Price FROM OrderInfo o JOIN Room r ON o.RoomId r.RoomId JOIN RoomType rt ON r.TypeId rt.TypeId WHERE o.OrderId orderId; DataTable dt SqlHelper.ExecuteQuery(sqlInfo, new SqlParameter(orderId, orderId)); DateTime checkIn (DateTime)dt.Rows[0][CheckInTime]; decimal price (decimal)dt.Rows[0][Price]; // 2. 计算住店天数不足一天按一天算 int days (int)Math.Ceiling((DateTime.Now - checkIn).TotalDays); if (days 0) days 1; // 3. 房费 单价 × 天数 decimal roomFee price * days; // 4. 写入账单 string sqlBill INSERT INTO Bill(OrderId, Amount, PayState) VALUES(orderId, amount, 0); SqlHelper.ExecuteNonQuery(sqlBill, new SqlParameter(orderId, orderId), new SqlParameter(amount, roomFee)); // 5. 更新订单和房态 SqlHelper.ExecuteNonQuery( UPDATE OrderInfo SET OrderState 2, CheckOutTime GETDATE() WHERE OrderId orderId, new SqlParameter(orderId, orderId)); SqlHelper.ExecuteNonQuery( UPDATE Room SET RoomState 3 WHERE RoomId (SELECT RoomId FROM OrderInfo WHERE OrderId orderId), new SqlParameter(orderId, orderId)); return roomFee; }这段代码里有三个我反复调过的点。第一天数用Math.Ceiling向上取整因为酒店行业惯例是过夜算一天下午入住第二天中午退也算一天不足一天按一天。第二退房后房态设成 3待清扫而不是 0空闲这样保洁能看到哪些房需要打扫打扫完再手动改成空闲形成闭环。第三账单先以未结状态写入前台收款后再改成已结中间这一步给了对账的缓冲。验证这套逻辑是否可靠我一般用三个测试用例当天开房当天退应算 1 天、跨天住 3 晚应算 3 天、凌晨 2 点入住次日 12 点退按行业惯例算 1 天。跑通这三个基本覆盖了 90% 的结算场景。最后说个习惯。做这类管理系统我每次改完房态相关的代码都会手动在数据库里跑一遍SELECT RoomNo, RoomState FROM Room看看有没有订单已退但房态没变的孤儿数据。这个动作花不了十秒但能省掉前台打电话来问这间房到底能不能住的半小时扯皮。系统不怕功能少就怕状态不一致把状态流转盯死了这套 WinFormSQLServer 的酒店管理系统就立住了。希望帮到你。本文还有配套的精品资源点击获取