
简介这是一套面向毕业设计场景的酒店管理系统完整源码与数据库备份基于C#和SQL Server实现适合计算机相关专业学生作为课程设计或毕业设计参考也可供初学者学习三层架构与WinForms桌面应用开发。压缩包共55个文件其中19个C#源文件对应界面与业务逻辑SQL脚本用于创建数据库表结构与基础数据另含配置文件、界面图片、可执行程序等整体仅1.15MB结构精简便于直接打开运行和二次修改。系统涵盖客户管理、房间管理、在线预订、入住退房、财务管理及报表分析等核心模块代码中体现了表示层、业务逻辑层、数据访问层的清晰分层SQL脚本则展示了酒店业务的数据表关系与示例数据。目前已吸引232人学习对需要快速理解C#与SQL Server交互、掌握实际项目代码组织方式的读者很有帮助。通过阅读源码可掌握常用控件数据绑定、SQL查询操作、模块化设计等技巧为独立开发类似管理系统打下扎实基础。1. 基于 C# 的酒店管理系统为什么这个毕业设计题目年年有人做年年有人翻车酒店管理系统大概是 C# 毕业设计里最经典的题目没有之一。它的边界很清晰管房间、管订单、管客户、管账务听起来就是几千行代码的事可每年答辩季都有一批人卡在同样几个地方——数据库表设计不合理、订房时房间状态被并发覆盖、退房算钱跟前台对不上账。这篇笔记就把这套基于 C# 的酒店管理系统从数据表到核心功能完整拆一遍SQL 脚本和关键代码都给到可直接改的程度。适合两类人一是拿这个题目做毕业设计的学生想知道源码该怎么组织、SQL 该怎么写才不会被答辩老师追问到哑口无言二是刚接触 C# 想练手 WinForms 加 SQL Server 的开发者想看看一个正经的业务系统是怎么把界面、业务和数据访问分开的。2. 从业务到建库先把酒店管家的四张核心表设计清楚2.1 客房、房型、房态三张表决定系统的地基很多初学者拿到需求就先写界面这是最容易返工的开局。正确顺序是先想清楚数据模型因为后面所有窗体和业务逻辑都在围着表转。酒店管理系统的核心数据就三类房型标准间、大床房、套房、房间具体到房号 801、802、房态这个房间现在是空闲、已预订、已入住还是脏房。房型和房间必须拆成两张表理由是房价挂在房型上而不是房间上。如果直接把价格字段写进房间表同一栋楼同一房型的二十个房间就要重复维护二十次价格改价时漏改一间账就算不对。房态我一般用 TINYINT 存数字0 表示空闲、1 表示预订、2 表示入住、3 表示脏房。有人喜欢用字符串存中文或者建一张独立的房态字典表去关联看起来更规范但对 C# 毕业设计来说反而增加麻烦——每次查询都要 JOIN 一次字典表界面显示还要再做一层翻译纯属给代码量注水。用数字维护状态在 DataGridView 里做颜色标记也最顺手一个 switch 就解决了。房间表的房号要加唯一约束这是硬性规则。你不想看到数据库里出现两间 801那样订房逻辑再严谨也会被脏数据击穿。楼层字段可以留着做房态总览时按楼层分组显示会比较直观。2.2 订单与客户订房业务的状态流转怎么落库客户和订单是另外两张表。客户表存姓名、身份证号、手机号身份证号建议加唯一索引因为回头查客史、查黑名单都以它为凭证。订单表是整个系统的核心字段要多想一步订单号、客户 ID、房间 ID、实际入住时间、计划退房时间、实际退房时间、押金、应收总额、订单状态。订单状态用数字维护四态0 预订、1 入住、2 已退房、3 已取消。这里有个常见误区是把预订和入住混在一个状态里结果系统分不清「订了还没来」和「已经住进去了」退房时一团乱。我的习惯是订房时只产生预订订单客户到店办理入住后才把状态从 0 改成 1同时写入实际入住时间。如果你想简化流程也可以把预订和入住合并成一步——直接生成状态为 1 的订单很多简化版毕业设计这么干但答辩时如果被问到「客户电话预订怎么处理」就答不上来。宁可把状态机画清楚哪怕代码里只实现其中几条路径面试时也能讲出设计意图。还要拆一张消费记录表挂在订单 ID 下。住店期间产生的加床、迷你吧、洗衣费用都写进这张子表退房时把房间费加消费明细汇总成应收总额。如果只做一个总金额字段在订单表里退房时就只能手改金额账目说不清。2.3 建库脚本一份能直接执行的 SQL 让评委看懂你的设计这部分直接给建库脚本。我一般用 SQL Server 2012 以上版本开发脚本向下兼容 2008 R2因为不少实验室和老电脑还在跑老版本。把下面这段保存成HotelDB.sql在 SSMS 里选中整段执行即可。-- 创建酒店管理系统数据库 IF DB_ID(HotelDB) IS NULL 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, -- 床位数 Area DECIMAL(6,2) NULL, -- 面积单位平方米 Remark NVARCHAR(200) NULL ); GO -- 房间表 CREATE TABLE Room ( RoomId INT IDENTITY(1,1) PRIMARY KEY, RoomNo VARCHAR(10) NOT NULL UNIQUE, -- 房号如 801 TypeId INT NOT NULL REFERENCES RoomType(TypeId), FloorNum INT NULL, -- 所在楼层 RoomStatus TINYINT NOT NULL DEFAULT 0, -- 0空闲 1预订 2入住 3脏房 Remark NVARCHAR(200) NULL ); GO -- 客户表 CREATE TABLE Customer ( CustomerId INT IDENTITY(1,1) PRIMARY KEY, CustomerName NVARCHAR(50) NOT NULL, IdCard VARCHAR(18) NOT NULL UNIQUE, -- 身份证号 Phone VARCHAR(20) NULL, Address NVARCHAR(100) NULL, RegisterTime DATETIME NOT NULL DEFAULT GETDATE() ); GO -- 订单表 CREATE TABLE Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(20) NOT NULL UNIQUE, -- 业务订单号如 HT20250512001 CustomerId INT NOT NULL REFERENCES Customer(CustomerId), RoomId INT NOT NULL REFERENCES Room(RoomId), CheckInTime DATETIME NULL, -- 实际入住时间 PlanCheckOutTime DATETIME NULL, -- 计划退房时间 CheckOutTime DATETIME NULL, -- 实际退房时间 Deposit DECIMAL(10,2) NOT NULL DEFAULT 0, -- 押金 TotalAmount DECIMAL(10,2) NOT NULL DEFAULT 0,-- 应收总额 OrderStatus TINYINT NOT NULL DEFAULT 0, -- 0预订 1入住 2已退房 3已取消 CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); GO -- 消费记录表 CREATE TABLE ConsumeRecord ( ConsumeId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL REFERENCES Orders(OrderId), ConsumeName NVARCHAR(50) NOT NULL, -- 消费项目如迷你吧可乐 Quantity INT NOT NULL DEFAULT 1, UnitPrice DECIMAL(10,2) NOT NULL, ConsumeTime DATETIME NOT NULL DEFAULT GETDATE() ); GO -- 初始化房型数据 INSERT INTO RoomType(TypeName, Price, BedCount, Area) VALUES (N标准间, 188.00, 1, 25), (N大床房, 228.00, 1, 28), (N商务套房, 468.00, 2, 45); GO -- 初始化 6 层楼 24 间房 DECLARE floor INT 6; WHILE floor 2 BEGIN DECLARE room INT 1; WHILE room 4 BEGIN INSERT INTO Room(RoomNo, TypeId, FloorNum) VALUES (CAST(floor AS VARCHAR) 0 CAST(room AS VARCHAR), (room - 1) % 3 1, floor); SET room room 1; END SET floor floor - 1; END GO脚本的逻辑说明前四张表是业务主体IDENTITY(1,1)做主键让编号自增外键关系在创建表时直接声明SQL Server 会自动维护引用完整性。房态字段给了默认值 0新插入的房间默认就是空闲。初始化数据部分用WHILE循环生成房间号从 6 楼到 2 楼每层 4 间房型按 (房间号 - 1) % 3 1 循环分配到三种房型上保证每层都有不同房型。参数说明DECIMAL(10,2)是金额字段的标配能存到千万级且保留两位小数NVARCHAR处理中文排序规则不受影响DATETIME存时间订单统计时要按它分组。如果你不想手动跑 SQL也可以建一个空数据库后用CREATE TABLE逐条执行但强烈建议用脚本因为毕业设计要交文档SQL 脚本本身就是设计文档的一部分。不要用附加 .mdf 文件的方式交付那玩意在别人机器上经常附加失败。3. 三层架构与数据访问别把 SQL 写在按钮点击事件里3.1 为什么毕业设计一定要拆 UI、BLL、DAL 三层WinForms 开发的天然诱惑是把所有代码都堆在按钮的Click事件里点一下按钮拼一条 SQL执行弹窗。小 demo 这么写很快但酒店管理系统有十几个窗体、几十个操作全堆在一起之后改一个需求要满文件找代码。三层架构是最稳的妥协方案界面层只负责显示和收集用户输入业务层处理规则数据访问层管 SQL 和数据库连接。我一般会建四个项目文件夹或者四个类库Model放实体类对应每张表的行数据DAL放数据访问代码里面只有 SQL 语句和参数BLL放业务规则比如计算房费、校验订单状态UI是 WinForms 窗体。实体类定义属性像这样public class OrderModel { public int OrderId { get; set; } public string OrderNo { get; set; } public int CustomerId { get; set; } public int RoomId { get; set; } public DateTime? CheckInTime { get; set; } public DateTime? PlanCheckOutTime { get; set; } public DateTime? CheckOutTime { get; set; } public decimal Deposit { get; set; } public decimal TotalAmount { get; set; } public int OrderStatus { get; set; } }这段代码没有业务逻辑纯粹是数据的容器。DateTime?表示可空时间因为预订阶段实际入住时间还没写入。如果是 C# 新手建议先去把基础语法过一遍再动手网上 C# 教程多的是但直接照着一个业务系统写遇到泛型和可空值类型时容易一头雾水。拆了层之后最明显的好处是以后想从 WinForms 换到 WPFDAL 和 BLL 一行不用改只重建 UI 层。3.2 数据访问层写一个 SqlHelper 把连接和参数统一管起来DAL 层最该先写的不是业务代码而是一个公共的数据访问助手类。用SqlConnection和SqlCommand直接散落在各处连接字符串改一处漏一处连接对象忘关闭还会把数据库连接池耗尽。我习惯写一个静态类SqlHelper提供两个高频方法执行非查询语句和返回 DataTable。public static class SqlHelper { // 连接串从 App.config 读不要硬编码在类里 private static readonly string connStr ConfigurationManager.ConnectionStrings[HotelDb].ConnectionString; public static int ExecuteNonQuery(string sql, params SqlParameter[] paras) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (paras ! null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteNonQuery(); } } public static DataTable ExecuteDataTable(string sql, params SqlParameter[] paras) { DataTable dt new DataTable(); using (SqlConnection conn new SqlConnection(connStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { if (paras ! null) da.SelectCommand.Parameters.AddRange(paras); da.Fill(dt); } return dt; } }逻辑说明using块保证连接和命令对象用完后自动释放这是 C# 里处理非托管资源的习惯写法泄漏连接是新手最容易犯的问题。params SqlParameter[]允许调用方传入任意数量的参数没参数时不传即可。ExecuteDataTable用SqlDataAdapter把结果集直接填充到内存里的 DataTableWinForms 的 DataGridView 可以直接绑定它作为数据源。参数说明连接字符串放在 App.config 的connectionStrings节点下格式大致是Server.;DatabaseHotelDB;Integrated SecurityTrue。用Integrated SecurityTrue走 Windows 身份认证在自己开发机上最省事但换到别的机器如果没配好 SQL Server 登录名就会连不上这个问题后面避坑章节单独展开。3.3 业务层与界面层事务、校验和事件处理的边界业务层是三层里最容易写成空壳的一层。很多人把 BLL 写成了 DAL 的透传——界面调 BLLBLL 里直接 return DAL 的方法等于没拆。业务层的价值在于处理单一数据访问搞不定的规则。举个具体例子订房时要做两步操作先检查房间状态再写订单。这两个操作必须放在一个数据库事务里要么都成功要么都失败不能出现订单写入了但房间状态没改的情况。这个事务逻辑放在 BLL 层DAL 层只提供单条 SQL 的执行能力。界面层做的事情应该只有三件收集输入、调用 BLL、展示结果。所有输入校验像身份证号格式、电话号码位数、退房时间不能早于入住时间都放在 BLL 层做这样即使以后做别的界面复用同一套逻辑规则不会丢。界面层还有一个容易被忽视的职责统一把底层异常翻译成用户能看懂的话。数据库连接失败、字段长度超了、外键冲突这些异常信息直接弹给用户看很吓人。我一般会在 BLL 的调用处用try-catch捕获SqlException把错误信息替换成「操作失败请检查数据库连接或输入内容」原始异常记到日志文件里。毕业设计不要求做日志但统一异常提示会让答辩演示时从容很多。4. 核心业务代码订房、退房与房态刷新的最小实现4.1 订房流程用事务加条件更新防住并发重复订房订房是酒店管理系统最核心的业务路径客户到店前台选一间空闲房录入客户信息收押金房间状态变成入住。看起来简单但有一个隐蔽的坑——两个前台同时看到 801 是空闲的同时点了订房。如果代码先查询状态再更新状态两次查询都返回空闲两单都写进去了房间只住得下一个人。解决办法是让更新语句自己带状态条件数据库层面保证只有一个请求能成功。这段代码放在 BLL 层是整套系统里最值得在答辩时讲清楚的一段public bool CheckIn(OrderModel order, out string message) { string connStr ConfigurationManager.ConnectionStrings[HotelDb].ConnectionString; using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { // 第一步条件更新房态。WHERE 里带 RoomStatus 0 // 并发时只有一条 UPDATE 能影响一行天然防重复订房 string sqlRoom UPDATE Room SET RoomStatus 2 WHERE RoomId RoomId AND RoomStatus 0; SqlCommand cmdRoom new SqlCommand(sqlRoom, conn, tran); cmdRoom.Parameters.AddWithValue(RoomId, order.RoomId); int rows cmdRoom.ExecuteNonQuery(); if (rows 0) { tran.Rollback(); message 房间不是空闲状态请刷新房态后重试; return false; } // 第二步写订单OrderStatus 置 1 表示已入住 string sqlOrder INSERT INTO Orders (OrderNo, CustomerId, RoomId, CheckInTime, PlanCheckOutTime, Deposit, OrderStatus) VALUES (OrderNo, CustomerId, RoomId, GETDATE(), PlanCheckOutTime, Deposit, 1); SqlCommand cmdOrder new SqlCommand(sqlOrder, conn, tran); cmdOrder.Parameters.AddWithValue(OrderNo, order.OrderNo); cmdOrder.Parameters.AddWithValue(CustomerId, order.CustomerId); cmdOrder.Parameters.AddWithValue(RoomId, order.RoomId); cmdOrder.Parameters.AddWithValue(PlanCheckOutTime, order.PlanCheckOutTime); cmdOrder.Parameters.AddWithValue(Deposit, order.Deposit); cmdOrder.ExecuteNonQuery(); tran.Commit(); message 入住成功; return true; } catch (Exception ex) { tran.Rollback(); message 入住失败 ex.Message; return false; } } }逻辑说明BeginTransaction开启事务后所有命令都要传入同一个事务对象这样两条 SQL 就在同一个事务里执行。先 UPDATE 房间状态如果影响行数为 0说明房间不是空闲状态回滚事务并直接返回。这一步既完成了状态修改又完成了并发控制比先 SELECT 再 UPDATE 的方案少一次查询还更安全。第二步插入订单全部成功才Commit任何一步抛异常就Rollback数据库回到操作前的状态。参数说明所有用户输入都用参数名传给 SQL Server绝不能用字符串拼接。GETDATE()取数据库服务器时间比用 C# 的DateTime.Now更可靠因为服务器时间和客户端时间可能有偏差。OrderNo建议在 C# 侧生成格式像HT20250512001用日期加三位流水号避免数据库自增编号暴露订单量。4.2 退房结账算清房费、押金和消费的三步流程退房比订房复杂因为要算钱。房费怎么算要提前定规则最常见的做法是按晚计算不足一晚按一晚计划退房时间默认次日 12 点超时到 18 点前加收半天房费18 点后加收全天。酒店行业规则各不相同毕业设计只要把规则写清楚并实现一条简单路径即可一般实现按晚计算就能满足要求。退房代码同样要在事务里完成三件事读订单和房价、算总额、更新订单状态和房间状态。完整实现如下public bool CheckOut(int orderId, decimal extraConsume, out string message) { string connStr ConfigurationManager.ConnectionStrings[HotelDb].ConnectionString; using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); try { // 第一步读取订单、房型价格同时锁定该行防止重复退房 string sqlQuery SELECT o.CheckInTime, o.PlanCheckOutTime, o.Deposit, rt.Price FROM Orders o JOIN Room r ON o.RoomId r.RoomId JOIN RoomType rt ON r.TypeId rt.TypeId WHERE o.OrderId OrderId AND o.OrderStatus 1; SqlCommand cmdQuery new SqlCommand(sqlQuery, conn, tran); cmdQuery.Parameters.AddWithValue(OrderId, orderId); SqlDataReader reader cmdQuery.ExecuteReader(); if (!reader.Read()) { reader.Close(); tran.Rollback(); message 未找到有效入住订单; return false; } DateTime checkIn Convert.ToDateTime(reader[CheckInTime]); DateTime planOut Convert.ToDateTime(reader[PlanCheckOutTime]); decimal deposit Convert.ToDecimal(reader[Deposit]); decimal price Convert.ToDecimal(reader[Price]); reader.Close(); // 房费按晚计算订一晚就是 1 * price int nights (planOut - checkIn).Days; if (nights 0) nights 1; decimal total nights * price extraConsume; // 第二步更新订单为已退房写入实际退房时间和总金额 string sqlUpdateOrder UPDATE Orders SET CheckOutTime GETDATE(), TotalAmount Total, OrderStatus 2 WHERE OrderId OrderId AND OrderStatus 1; SqlCommand cmdUpdateOrder new SqlCommand(sqlUpdateOrder, conn, tran); cmdUpdateOrder.Parameters.AddWithValue(Total, total); cmdUpdateOrder.Parameters.AddWithValue(OrderId, orderId); cmdUpdateOrder.ExecuteNonQuery(); // 第三步房间置为脏房等待保洁后恢复空闲 string sqlRoom UPDATE Room SET RoomStatus 3 WHERE RoomId (SELECT RoomId FROM Orders WHERE OrderId OrderId); SqlCommand cmdRoom new SqlCommand(sqlRoom, conn, tran); cmdRoom.Parameters.AddWithValue(OrderId, orderId); cmdRoom.ExecuteNonQuery(); tran.Commit(); message string.Format(退房成功应收 {0:F2} 元押金 {1:F2} 元应退 {2:F2} 元, total, deposit, deposit - total); return true; } catch (Exception ex) { tran.Rollback(); message 退房失败 ex.Message; return false; } } }逻辑说明先查订单和房价JOIN两张表一次把房型价格带出来避免二次查询。查询条件里带OrderStatus 1如果订单已经被退过reader.Read()返回 false 直接拦截防止重复退房。计算完金额后更新订单同样带OrderStatus 1条件做第二道防线。最后把房间置为脏房状态 3这是退房和取消订单的关键区别——退房后房间不能直接变空闲要等保洁确认。参数说明(planOut - checkIn).Days得到的是两个日期之间的整天数预订时如果计划住两天PlanCheckOutTime应该比CheckInTime晚两天这里算出来的就是 2。extraConsume是退房时手动录入的额外消费总额比如客人说 mini bar 喝了两瓶可乐前台直接录入金额明细已经存在ConsumeRecord表里这里只汇总。4.3 房态总览DataGridView 做可视化房态表与颜色刷新前台最常用的界面是房态总览一个窗口把所有房间按楼层列出来空闲白色、预订黄色、入住绿色、脏房灰色。这个功能的关键不在显示而在刷新时机——订房、退房操作完成后必须立刻刷新否则界面和数据库状态不一致前台就会误操作。房态加载方法可以写成窗体的一个私有方法窗体加载时调用一次每次订房或退房成功后调用一次。核心代码private void LoadRoomStatus() { string sql SELECT Room.RoomNo, RoomType.TypeName, Room.FloorNum, Room.RoomStatus FROM Room JOIN RoomType ON Room.TypeId RoomType.TypeId ORDER BY Room.FloorNum, Room.RoomNo; DataTable dt SqlHelper.ExecuteDataTable(sql); dgvRooms.DataSource dt; // 按房态着色0空闲白 1预订浅黄 2入住浅绿 3脏房灰 foreach (DataGridViewRow row in dgvRooms.Rows) { int status Convert.ToInt32(row.Cells[RoomStatus].Value); switch (status) { case 0: row.DefaultCellStyle.BackColor Color.White; break; case 1: row.DefaultCellStyle.BackColor Color.LightYellow; break; case 2: row.DefaultCellStyle.BackColor Color.LightGreen; break; case 3: row.DefaultCellStyle.BackColor Color.LightGray; break; } } }逻辑说明SQL 让房态表和房型表关联直接在查询结果里带出房型名和楼层界面不用再做二次查询。ORDER BY FloorNum, RoomNo保证房间按楼层和房号排序显示出来的顺序符合前台习惯。着色逻辑遍历每行根据RoomStatus字段设置背景色颜色是我常用的四色方案你也可以换成更明显的配色。参数说明row.Cells[RoomStatus]用的是列名索引而不是数字索引列名来自 SQL 的别名这样查询语句里调整列顺序不会影响这里取值。刷新方法的调用位置很关键在CheckIn和CheckOut返回成功后紧接着调用LoadRoomStatus()不要在异步线程里调用WinForms 控件跨线程访问会抛异常。如果以后做定时自动刷新记得用Timer控件并在Invoke里更新界面。5. 毕业设计最容易翻车的 5 个坑连接串、注入、并发与日期5.1 连接字符串写死在代码里换台机器就崩很多人拿到源码第一件事是改SqlConnection里的连接串结果在开发机上跑得好好的拷贝到笔记本或者答辩用的机器上就报「建立与 SQL Server 的连接时出错」。原因是连接串写死在代码里里面的服务器名是开发机的实例名别人机器上未必有这个实例。解决方法是把连接串挪到 App.config 里。在项目里加一个App.config文件写入connectionStrings节点代码里用ConfigurationManager.ConnectionStrings[HotelDb].ConnectionString读取。换机器时只需要改配置文件不用重新编译。如果你把数据库文件复制到别人机器上并附加到对方的 SQL Server还要注意服务器名和实例名是否符合对方环境比如对方装的是SQLEXPRESS实例连接串里就要写Server.\SQLEXPRESS。这个坑几乎每个拿源码改的同学都会踩一遍提前检查能省一晚上。5.2 SQL 字符串拼接出 SQL 注入漏洞答辩老师一眼看穿界面输入框的值直接拼进 SQL 字符串是新手最容易犯且最伤印象分的错误。比如查询客户信息时写SELECT * FROM Customer WHERE CustomerName txtName.Text 这行代码遇到姓名为 OR 11的输入时查询条件被改写成恒真整个客户表全部返回。SQL 注入是数据库课程的必考点答辩老师只要看到代码里有字符串拼接基本都会追问。解决方法是所有 SQL 一律参数化用参数名占位把值放进SqlParameter里传给命令对象。前面章节的SqlHelper已经支持传入SqlParameter数组调用时这样写string sql SELECT * FROM Customer WHERE CustomerName LIKE Keyword OR Phone LIKE Keyword; DataTable dt SqlHelper.ExecuteDataTable(sql, new SqlParameter(Keyword, % keyword %));参数说明模糊查询时通配符%要拼在参数值里而不是拼在 SQL 语句里否则参数化失去意义。SQL Server 会把传入内容当作纯数据而不是可执行代码这是参数化防注入的根本原理。答辩时可以主动讲一句「所有数据库操作都走参数化查询」这是加分项。5.3 并发订房两个前台同时卖同一间房没有并发控制的订房逻辑是查一下状态再更新但查询和更新之间有时间差。两个窗口同时查到某房间空闲先后执行更新结果订单表里出现两条对同一房间的入住记录。现实中前台两个窗口同时操作完全可能发生这也是我在第 4 章强调条件更新的原因。现象是系统里出现同房号的重复订单房态显示却是入住中。原因是代码先 SELECT 再 INSERT/UPDATE缺少数据库层面的约束。解决就是第 4 章的写法UPDATE 语句本身带RoomStatus 0条件ExecuteNonQuery返回 0 就说明被别人抢先了提示用户刷新。如果订单表里已经有重复数据就要写清理脚本把后写入的重复单取消并把对应房间释放。防重于治编码阶段就用条件更新后面能省大量麻烦。5.4 日期时间处理不规范退房算钱总是差一天退房算费最怕日期出问题。两个表现一是直接用DateTime.Now存储和计算如果应用服务器和数据库服务器时区不一致入住和退房时间可能差出几小时二是计算入住天数时用了(CheckOutTime - CheckInTime).TotalDays直接取小数部分舍入规则没定义28 小时有时算一晚有时算两晚。统一做法是时间写入用GETDATE()由 SQL Server 提供标准时间跨机器也不怕。天数计算用DateTime相减后取Days属性它取的是整天数不足一天舍去然后业务规则兜底if (nights 0) nights 1保证至少收一晚。超时退房的加收规则建议用查询语句里的CASE WHEN判断或者直接用 C# 算好再传入。无论哪种方式规则要写在 BLL 层并加注释答辩时被问到「你们超时怎么收费」能直接说清楚。5.5 附加数据库失败和登录认证问题打开就报错交付时最常见的问题是对方拿到 .mdf 文件尝试附加报「无法附加数据库」或者权限错误。原因是 .mdf 文件在开发机上是附加过的文件路径和数据库名称记录在系统目录里拷贝到新机器后附加路径对不上。另外如果开发时用的是 Windows 身份认证对方机器的 Windows 用户不在 SQL Server 登录列表里连接也会失败。解决方法是交付 SQL 脚本而不是 .mdf 文件。让对方用脚本重建数据库然后用脚本里的初始化数据跑一遍这是最省心的方式。如果必须交付数据库文件把 SQL Server 的身份认证模式改成混合模式创建一个带密码的sa或专用登录账号连接串用Server.;DatabaseHotelDB;User Idxxx;Passwordxxx。要注意老版本 SQL Server 2008 R2 的兼容性问题如果对方环境是 2008 R2你在高版本建的库它附加不了脚本方式反而是兼容性最好的。交付前一定要在干净环境里跑一遍建库脚本确认字段类型和语句在对方版本上都能执行。6. 交付前别急着打包完整验证一遍再加三个加分项拿到源码和 SQL 脚本后不要急着生成压缩包交出去。先把整套系统当作一个真实酒店跑一遍完整业务流程按下面的清单逐项验证每项都过一遍再验收。验证模块具体操作预期结果数据库初始化执行 SQL 脚本无报错24 间房初始化成功客户管理新增、编辑、查询客户身份证重复被拦截模糊查询可用订房入住选择空闲房间办理入住房态变绿订单号生成押金记录正确并发订房两个窗口同时订同一间房只有一个成功另一个被提示刷新退房结账办理退房并录入额外消费金额计算正确房态变脏房灰色订单查询按客户名和日期范围查询数据与操作记录一致数据库备份用脚本备份数据库备份文件可还原还原后数据完整验证时重点关注钱相关的计算押金、房费、额外消费、找零。酒店管理系统账目错了比功能缺失还严重答辩老师一定会翻订单明细对金额。三个加分技巧我每次都会做。第一个是订单号自动生成用DateTime.Now.ToString(yyyyMMddHHmmss)加三位随机数组合避免手动输入订单号同时保证唯一。第二个是统一的异常提示在 BLL 调用处捕获SqlException后弹自定义消息界面层不暴露技术细节演示时不容易尴尬。第三个是报表导出把 DataGridView 的数据导出成 CSV 文件用 Excel 能直接打开代码量很少但视觉效果好private void ExportToCsv(DataTable dt, string filePath) { StringBuilder sb new StringBuilder(); for (int i 0; i dt.Columns.Count; i) { sb.Append(dt.Columns[i].ColumnName); if (i dt.Columns.Count - 1) sb.Append(,); } sb.AppendLine(); foreach (DataRow row in dt.Rows) { for (int i 0; i dt.Columns.Count; i) { string value row[i].ToString(); if (value.Contains(,)) value \ value \; sb.Append(value); if (i dt.Columns.Count - 1) sb.Append(,); } sb.AppendLine(); } File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); }逻辑说明先把列名写成表头再逐行写数据。字段内容里如果包含逗号就用双引号包起来否则 Excel 打开时列会错位。用Encoding.UTF8写文件是防止中文乱码的老问题很多没经验的同学导出 CSV 后用 Excel 打开发现中文全是乱码多半是编码没指定。做了这么多次类似的系统交付我最深的体会是毕业设计项目到最后拼的不是功能多少而是边界处理够不够稳。一个能正确处理并发订房、日期计算和数据库连接异常的系统比堆了十个功能模块但逻辑漏洞百出的系统要值钱得多。你把这个标题下的源码和 SQL 拿到手之后先别急着跑界面从数据库脚本开始一条一条把表结构和业务逻辑对上再动手改代码比自己从头写一遍还要快出成果。希望帮到你。本文还有配套的精品资源点击获取