☰
基于C#的酒店管理系统毕业设计:数据库设计与三层架构实战
2026/10/9 7:11:27 网站建设 项目流程

简介:这是一套面向毕业设计场景的酒店管理系统完整源码与数据库备份,基于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 换到 WPF,DAL 和 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把结果集直接填充到内存里的 DataTable,WinForms 的 DataGridView 可以直接绑定它作为数据源。

参数说明:连接字符串放在 App.config 的<connectionStrings>节点下,格式大致是Server=.;Database=HotelDB;Integrated Security=True。用Integrated Security=True走 Windows 身份认证,在自己开发机上最省事,但换到别的机器如果没配好 SQL Server 登录名就会连不上,这个问题后面避坑章节单独展开。

3.3 业务层与界面层:事务、校验和事件处理的边界

业务层是三层里最容易写成空壳的一层。很多人把 BLL 写成了 DAL 的透传——界面调 BLL,BLL 里直接 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 '1'='1的输入时,查询条件被改写成恒真,整个客户表全部返回。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=.;Database=HotelDB;User Id=xxx;Password=xxx。要注意老版本 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 拿到手之后,先别急着跑界面,从数据库脚本开始,一条一条把表结构和业务逻辑对上,再动手改代码,比自己从头写一遍还要快出成果。希望帮到你。

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

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

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

立即咨询