ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#与SQL Server宾馆管理系统课程设计:数据库设计与WinForms实现

C#与SQL Server宾馆管理系统课程设计:数据库设计与WinForms实现 简介面向计算机专业学生与C#初学者的课程设计级项目涵盖宾馆管理场景中的客房预订、入住登记、退房处理等业务展示了Windows Forms界面设计、ADO.NET数据库交互及业务逻辑封装方法。资源内含121个文件以C#源代码.cs、界面资源.resx/.resources、程序集.dll和可执行文件.exe为主附带SQL数据库脚本与Word版实验报告压缩包大小9.84MB目录结构清晰便于对照源码、资源与文档逐项学习。已有210人学习下载适合作为软件工程课程设计、毕业设计或自学实践的重要参考资料。通过这套项目可掌握从需求分析、界面搭建到数据访问与测试文档的完整流程实验报告中的开发过程与排错记录能帮助读者理解常见问题的解决思路快速复现并扩展自己的宾馆管理系统。1. 这是一个课程设计项目也是你第一次认真面对C#和数据库之间的协作拿到这个压缩包的人要么在赶数据库课程设计要么刚学完C#和SQL Server想找一个完整项目看看真实系统长什么样。标题里的“含数据库文件和实验报告”说明了它的交付形态一个能运行的小型WinForms程序、一套可以附加的数据库文件、一份撑得起答辩的实验报告。它解决的问题非常具体——前台登记入住、退房结算、房间状态切换、客户信息查询这些最日常的宾馆业务用C#界面操作SQL Server数据库来完成。适合谁呢刚入门C#、需要把一个增删改查项目从零拼起来的人或者正在头疼课程设计题目的学生。这篇文章不假装见过源码只按这类系统最可靠、最稳妥的方案把设计思路、表结构、关键代码和翻车点一次讲透。2. 先把数据底座立住小型宾馆管理系统的基础表结构与关系2.1 房型、房间、客户三张基础表怎么设计才不冗余宾馆管理系统的核心数据只有三类房间有什么、客户是谁、谁住过哪间房。围绕这三个问题最合理的设计是用 6 张表撑起整个业务分别是RoomType房型表、Room房间表、Customer客户表、Reservation预订表、CheckIn入住记录表、Consume消费记录表。先看房型和房间这两张表。房型表保存“标间”“大床房”“套房”这类公共属性房间表保存具体的房号、楼层和当前状态。拆开成两张表是为了避免同一个房型下 20 个房间重复写 20 遍价格和面积。常见字段设计如下表名字段类型说明RoomTypeTypeIdINT 自增主键房型编号RoomTypeTypeNameNVARCHAR(50)房型名称RoomTypePriceDECIMAL(10,2)门市价单位元/晚RoomTypeBedTypeNVARCHAR(20)床型如大床/双床RoomTypeAreaDECIMAL(5,2)面积平方米RoomRoomIdINT 自增主键房间主键RoomRoomNoNVARCHAR(10) 唯一房间号如 501RoomTypeIdINT 外键关联 RoomType.TypeIdRoomFloorINT楼层RoomStatusTINYINT0空闲/1已入住/2已预订/3维修Status这个字段是整个系统的状态中心后面所有业务逻辑都在围绕它做转换。用TINYINT存状态数字UI 层再映射成文字是为了让数据库更小、判断更快。RoomNo加唯一约束防止同一楼层出现两个 501。客户表要特别注意身份证字段。IdCard建议用NVARCHAR(18)并在上面建唯一索引同一个人重复登记时就合并成一条客户记录。姓名、性别、电话、入住次数这些字段属于高频查询列索引选择上优先IdCard和Name即可不需要对所有列做索引否则数据量大了以后写入会变慢。2.2 预订、入住、消费三张业务表如何把业务串成闭环有了基础表接下来是业务表。Reservation记录客户提前订房的信息CheckIn记录实际入住和退房Consume记录住店期间产生的额外消费小卖部、餐费、洗衣费。CheckIn是整个系统最重要的表它包含押金、入住时间、应退时间、实际离店时间、操作员等字段。押金用DECIMAL(10,2)入住时间和退房时间用DATETIME因为后面算房费需要精确到时间差。Status字段在这里表示入住单的状态0 在住、1 已退、2 已结账。有人会问“房间表已经存了状态为什么入住单还要状态”因为房间状态是给前台看房间用的入住单状态是给财务对账用的两者业务目不同都要保留。Consume表通过CheckInId外键关联入住记录这样退房结算时一句SUM(Amount)就能算出总消费。字段设计如下表名字段类型说明CheckInCheckInIdINT 自增主键入住单号CheckInCustomerIdINT 外键关联 Customer 表CheckInRoomIdINT 外键关联 Room 表CheckInDepositDECIMAL(10,2)押金CheckInArriveTimeDATETIME实际入住时间CheckInLeaveTimeDATETIME实际退房时间CheckInStatusTINYINT0在住/1已退CheckInOperatorNVARCHAR(20)操作员姓名ConsumeConsumeIdINT 自增主键消费记录号ConsumeCheckInIdINT 外键关联 CheckIn 表ConsumeItemNameNVARCHAR(50)消费项目名称ConsumeAmountDECIMAL(10,2)消费金额ConsumeCreateTimeDATETIME消费时间外键关系走向是Room指向RoomTypeCheckIn指向Customer和RoomConsume指向CheckInReservation指向Customer和Room。这个关系链在数据库关系图里画出来是一个很标准的星形结构课程设计答辩时老师让你画 E-R 图照着这个画就对了。2.3 从 SQL 文件到可用的本地库数据库脚本的正确写法标题里说“含数据库文件”多数课程设计项目的交付物是一个.mdf文件或.sql脚本。.mdf文件直接附加到 SQL Server 中方式是在 SSMS 里右键“数据库”节点选“附加”指向.mdf路径即可。但如果你拿到的是.sql脚本就要自己在本地新建数据库再执行脚本。手动执行时容易漏掉脚本顶部的CREATE DATABASE直接在master库里建了表程序连接时又连到另一个库导致“找不到表”的报错。我一般建议用下面的脚本结构建库分清楚CREATE DATABASE和CREATE TABLE的关系USE master; GO IF EXISTS (SELECT * FROM sys.databases WHERE name NHotelDB) BEGIN ALTER DATABASE HotelDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE HotelDB; END GO 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, BedType NVARCHAR(20) NULL, Area DECIMAL(5,2) NULL, Remark NVARCHAR(200) NULL ); CREATE TABLE Room ( RoomId INT IDENTITY(1,1) PRIMARY KEY, RoomNo NVARCHAR(10) NOT NULL UNIQUE, TypeId INT NOT NULL, Floor INT NOT NULL, Status TINYINT NOT NULL DEFAULT 0, CONSTRAINT FK_Room_RoomType FOREIGN KEY (TypeId) REFERENCES RoomType(TypeId) );说明两点。第一脚本开头做一次“删除重建”而不是直接CREATE DATABASE这样重复执行脚本不会报错是课程设计项目里最保险的写法。第二外键约束要成对写Room表的TypeId引用RoomType表的TypeId如果忘了加CONSTRAINT之后程序里写 JOIN 也能工作但数据库层面就失去了完整性保护。执行完脚本后用一条查询验证表是否建立成功SELECT t.name AS 表名 FROM sys.tables t;能看到 6 张表再开始写代码。如果执行到一半报错多半是脚本里混了中文字符集问题把文件另存为 UTF-8 with BOM 再执行或者把脚本在 SSMS 里重新打开一次执行。3. 用三层架构搭代码骨架DBHelper、数据访问层与 UI 绑定3.1 DBHelper把 SqlConnection 的打开、执行、释放统一收口写 C# 连接 SQL Server 的代码最忌每个窗体里各写一套。10 个窗体就有 10 份连接字符串一旦数据库路径变了要改 10 处。常见做法是写一个静态的DBHelper类把连接字符串和基础方法集中管理。连接字符串这里有个关键细节。课程设计项目经常在开发机之间拷贝绝对路径C:\Users\xxx\source\repos\Hotel\HotelDB.mdf一换电脑就失效。用|DataDirectory|代替绝对路径让程序自己去运行目录找数据库文件是这类项目最常见的操作public class DBHelper { private static readonly string connStr Server(LocalDB)\MSSQLLocalDB;AttachDbFilename|DataDirectory|\HotelDB.mdf;Integrated SecurityTrue; public static DataTable ExecuteQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } SqlDataAdapter da new SqlDataAdapter(cmd); DataTable dt new DataTable(); da.Fill(dt); return dt; } } } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn new SqlConnection(connStr)) { using (SqlCommand cmd new SqlCommand(sql, conn)) { if (parameters ! null) { cmd.Parameters.AddRange(parameters); } conn.Open(); return cmd.ExecuteNonQuery(); } } } }这套代码的核心是using块。SqlConnection和SqlCommand都实现了IDisposable用using包裹方法执行完自动释放连接不用写conn.Close()。ExecuteQuery负责查询返回DataTableExecuteNonQuery负责插入、更新、删除返回受影响的行数。两个方法都接收SqlParameter数组强制调用方用参数化查询而不是拼字符串这是后面避免 SQL 注入的第一道防线。3.2 数据访问层RoomDao 里的查询与状态更新数据访问层一般按实体类命名RoomDao、CustomerDao、CheckInDao等。每个 DAO 只操作自己的表不掺业务逻辑。以房间查询为例前台界面需要显示“房间号、房型、价格、楼层、状态”这些字段分布在Room和RoomType两张表里所以 DAO 里要写 JOIN 查询public DataTable GetRoomsByStatus(int status) { string sql SELECT r.RoomId, r.RoomNo, rt.TypeName, rt.Price, r.Floor, r.Status FROM Room r INNER JOIN RoomType rt ON r.TypeId rt.TypeId WHERE r.Status status ORDER BY r.Floor, r.RoomNo; return DBHelper.ExecuteQuery(sql, new SqlParameter(status, status)); }为什么 JOIN 要写在 DAO 而不是在 UI 层拼两个查询因为 UI 层只需要一个“房间列表”的表格数据源两次查询意味着数据要在界面层手动关联代码更难维护。ORDER BY r.Floor, r.RoomNo保证显示顺序稳定否则每次刷新列表顺序都可能变。更新房间状态的 DAO 方法要特别小心最常见的错误是把RoomId和RoomNo搞混。界面传过来的是房间号但UPDATE语句的WHERE条件应该用主键RoomId。如果表里恰好多条记录号码相同用RoomNo会一次更新多行造成状态错乱public bool UpdateRoomStatus(int roomId, int newStatus) { string sql UPDATE Room SET Status newStatus WHERE RoomId roomId; int rows DBHelper.ExecuteNonQuery(sql, new SqlParameter(newStatus, newStatus), new SqlParameter(roomId, roomId)); return rows 0; }返回bool值而不是void是为了让上层业务判断操作是否成功。rows 0表示确实更新到了一条记录如果是 0说明传入的RoomId不存在这本身就是一种异常情况应该在 UI 层提示。3.3 UI 层绑定DataGridView 的列映射与单元格取值WinForms 的DataGridView是最常用的表格控件。直接把DataTable赋给DataSource会按查询结果的顺序自动生成所有列但你会看到RoomId这种用户不该关心的字段。更好的做法是关闭自动生成列手动配置需要显示的列private void LoadRoomList() { DataTable dt roomDao.GetRoomsByStatus(0); dgvRooms.AutoGenerateColumns false; dgvRooms.Columns.Clear(); dgvRooms.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName RoomNo, HeaderText 房间号, Width 80 }); dgvRooms.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName TypeName, HeaderText 房型, Width 100 }); dgvRooms.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName Price, HeaderText 门市价, Width 90 }); dgvRooms.Columns.Add(new DataGridViewTextBoxColumn { DataPropertyName Status, HeaderText 状态, Width 60 }); dgvRooms.DataSource dt; }DataPropertyName是列和数据表字段之间的映射这是容易被新手忽略的参数。设置错误的话表格能显示行数但每个单元格都是空白因为它找不到对应的数据字段。状态列建议用值转换而不是直接展示 0/1/2枚举转字符串的逻辑要放在独立的函数里比如private string GetStatusText(int status) { switch (status) { case 0: return 空闲; case 1: return 已入住; case 2: return 已预订; default: return 维修; } }在CellFormatting事件里调用这个转换函数而不是改数据库里的值。数据库里永远存数字展示层才做格式化这样后续统计 SQL 写起来才顺畅。4. 把入住到退房的业务闭环跑通事务、状态机与账单计算4.1 入住登记一条事务里完成客户写入、入住单创建、房间状态更新入住登记是整个系统最核心的操作它涉及三件事客户信息写入、入住单插入、房间状态从 0 改成 1。这三个操作必须打包成一个整体——中间任何一步失败都不能让前台看到“客户登记了但房间还是空的”这种脏状态。C# 里用SqlTransaction实现事务。流程是打开连接、开启事务、顺序执行三条 SQL、提交任何一步抛异常就回滚。代码结构如下public bool CheckIn(Customer customer, int roomId, decimal deposit, string operatorName) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction trans conn.BeginTransaction(); try { // 1. 插入客户返回自增主键 string sqlCustomer INSERT INTO Customer (Name, IdCard, Phone, Gender, CreateTime) OUTPUT INSERTED.CustomerId VALUES (name, idCard, phone, gender, createTime);; SqlCommand cmdCustomer new SqlCommand(sqlCustomer, conn, trans); cmdCustomer.Parameters.AddWithValue(name, customer.Name); cmdCustomer.Parameters.AddWithValue(idCard, customer.IdCard); cmdCustomer.Parameters.AddWithValue(phone, customer.Phone); cmdCustomer.Parameters.AddWithValue(gender, customer.Gender); cmdCustomer.Parameters.AddWithValue(createTime, DateTime.Now); int customerId (int)cmdCustomer.ExecuteScalar(); // 2. 插入入住单 string sqlCheckIn INSERT INTO CheckIn (CustomerId, RoomId, Deposit, ArriveTime, Status, Operator) VALUES (customerId, roomId, deposit, arriveTime, 0, operatorName); SELECT CAST(SCOPE_IDENTITY() AS INT);; SqlCommand cmdCheckIn new SqlCommand(sqlCheckIn, conn, trans); cmdCheckIn.Parameters.AddWithValue(customerId, customerId); cmdCheckIn.Parameters.AddWithValue(roomId, roomId); cmdCheckIn.Parameters.AddWithValue(deposit, deposit); cmdCheckIn.Parameters.AddWithValue(arriveTime, DateTime.Now); cmdCheckIn.Parameters.AddWithValue(operatorName, operatorName); int checkInId (int)cmdCheckIn.ExecuteScalar(); // 3. 房间状态置为已入住 string sqlRoom UPDATE Room SET Status 1 WHERE RoomId roomId;; SqlCommand cmdRoom new SqlCommand(sqlRoom, conn, trans); cmdRoom.Parameters.AddWithValue(roomId, roomId); cmdRoom.ExecuteNonQuery(); trans.Commit(); return true; } catch (Exception ex) { trans.Rollback(); throw new Exception(入住登记失败 ex.Message); } } }这段代码有两个关键点。第一所有SqlCommand都传入同一个trans对象这是它们参与同一事务的方式如果只传了连接不传事务每条 SQL 会自动提交前面的操作就失去保障。第二用OUTPUT INSERTED.CustomerId和SCOPE_IDENTITY()拿取自增主键有人会先查最大值再 1这在多用户并发时会拿到错误的主键值务必用数据库提供的自增返回值。4.2 退房结算房费计算是逻辑层的事别让 DAO 去算钱退房结算牵涉的数据更多今天退房时房间住了几天、房价每晚多少、押金多少、期间消费多少最终算出“应补交多少钱”。房费计算属于业务逻辑应该放在 BLL 层DAO 只负责取数。计算的核心是日期差public decimal CalcRoomFee(DateTime arriveTime, DateTime leaveTime, decimal pricePerNight) { int days (leaveTime.Date - arriveTime.Date).Days; if (days 0) { days 1; } return days * pricePerNight; }注意这里用的是.Date而不是直接减两个DateTime。如果入住是当晚 23:00离店是次日凌晨 1:00直接相减得到的Hours是 2Days是 0房费会算成 0 元。取.Date之后再减天然把时间归零隔了一晚就是 1 天。同日入住的特殊情况强制按 1 天算这是宾馆行业的通行规则。退房的完整事务是算出房费、把消费记录汇总、更新入住单状态为已退、把房间状态改回 0。消费汇总用聚合查询SELECT ISNULL(SUM(Amount), 0) AS TotalConsume FROM Consume WHERE CheckInId checkInId;ISNULL保证没有消费记录时返回 0 而不是 NULL否则 C# 里decimal收到DBNull会抛转换异常。汇总结果加上房费再减去押金结果大于 0 就是要补的钱小于 0 则是要退给客人的钱。这个小逻辑要在 BLL 层算清楚并写成单独的CalcSettlement方法方便答辩演示时单独调用测试。4.3 预订与换房两个常见的边界场景如何处理状态预订操作的本质是“提前占用房间”。客户打电话来订房前台把房间状态从 0 改成 2已预订客人到店后把 2 改回 1已入住。这个过程没有实际的款项入账只需要在Reservation表插入一条带上预计入住、离店日期的记录。这里最容易忽略的是“预订却没来”的情况。如果只插入预订记录、不更新房间状态那这间房就一直显示空闲别人也能订造成超卖。所以预订事务要同时更新Room.Status 2。如果客人未到店需要取消就把Reservation.Status标记为已取消同时把房间状态改回 0。换房是退房和入住的组合操作。原房间退掉CheckIn 状态置 1Room 状态置 0新房间登记入住新建 CheckInRoom 状态置 1关键是原入住单上的消费记录要跟人走。实现上可以在Consume表增加CheckInId直接更新为新单的主键也可以在换房时做一次“消费转单”的存储过程。用代码实现时把两步放进同一事务避免换到一半失败出现房间两边都有人住的脏数据。public bool ChangeRoom(int oldRoomId, int newRoomId, int checkInId) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction trans conn.BeginTransaction(); try { // 旧房间释放 SqlCommand cmdOld new SqlCommand( UPDATE Room SET Status 0 WHERE RoomId oldRoomId, conn, trans); cmdOld.Parameters.AddWithValue(oldRoomId, oldRoomId); cmdOld.ExecuteNonQuery(); // 入住单关联到新房间 SqlCommand cmdCheckIn new SqlCommand( UPDATE CheckIn SET RoomId newRoomId WHERE CheckInId checkInId, conn, trans); cmdCheckIn.Parameters.AddWithValue(newRoomId, newRoomId); cmdCheckIn.Parameters.AddWithValue(checkInId, checkInId); cmdCheckIn.ExecuteNonQuery(); // 新房间置为已入住 SqlCommand cmdNew new SqlCommand( UPDATE Room SET Status 1 WHERE RoomId newRoomId, conn, trans); cmdNew.Parameters.AddWithValue(newRoomId, newRoomId); cmdNew.ExecuteNonQuery(); trans.Commit(); return true; } catch { trans.Rollback(); return false; } } }这里要强调的是更新顺序。先释放旧房再锁定新房可以避免两个房间同时处于已入住状态的时间窗口。如果先锁定新房再释放旧房中间任何一步失败都会让两间房状态同时为 1前台看到的就是“一间房被占了另一间也显示有人”。5. 避坑指南宾馆管理系统最容易翻车的 5 个细节5.1 数据库附加失败程序启动就报“无法打开物理文件”现象运行程序后连接数据库报错提示无法打开物理文件或访问路径被拒绝。原因.mdf文件放在项目的生成目录下但连接字符串里写的是绝对路径把项目复制到另一台机器后路径失效。另一个常见原因是文件被放在了系统盘的受保护目录当前用户没有写权限。解决连接字符串改用|DataDirectory|相对路径并把.mdf文件放在项目的bin\Debug输出目录或在项目文件里设置“如果较新则复制”选项让每次生成时自动把数据库文件拷贝到运行目录。把原来硬编码的绝对路径改成上文DBHelper里的写法问题就解决了。同时确认.mdf文件本身不是只读属性右键文件取消勾选。5.2 退房成功但房间状态永远停在“已入住”现象结算界面提示退房完成但回到房间列表一看刚才那间房状态还是 1已入住。原因退房的代码只更新了CheckIn表的状态没有同步更新Room表的Status。这是初学 C# 时最常见、最隐蔽的遗漏因为退房查询的 SQL 用的是CheckInId而房间列表查询用的是Room表两个数据源看起来都正常。解决把“更新入住单状态”和“更新房间状态”放进同一个事务两条 SQL 要么都成功要么都失败。排查时先在 SQL Server 里执行SELECT RoomId FROM CheckIn WHERE CheckInId 1拿到房间号再执行SELECT Status FROM Room WHERE RoomId 该房间号对比两条结果就知道是不是同步遗漏。养成每次改状态都检查“有没有第二个地方要改”的习惯这套系统的坑一半都在状态同步上。5.3 房费结算总差一天客人少住一晚却被多收现象客人中午 12 点入住次日 11 点退房按 1 天收费没问题但客人当天凌晨 1 点入住早上 8 点退房系统算出 0 元。原因直接用(leaveTime - arriveTime).Days计算天数凌晨入住与上午退房的时间差不足 24 小时Days属性返回 0。解决把两个时间都取.Date再相减如第 4.2 节里的CalcRoomFee方法。注意DateTime的减法返回的是TimeSpan它的Days属性只取整天数不包含四舍五入。用.Date归零后再计算无论几点入住、几点退房跨过一晚就算一天。测试时至少覆盖三种情况同日入住退房、跨一晚、跨多晚每种都打印出计算结果。5.4 DataGridView 里删了行数据库却一条没少现象在界面上按 Delete 键表格行消失了刷新后那一行又回来了。原因DataGridView默认的删除只是从绑定的DataTable里删除行这只发生在内存里并没有调用任何 SQL。很多人以为把AllowUserToDeleteRows设为true就够了其实界面上的删除操作和数据库的DELETE语句没有任何关系。解决在删除按钮的点击事件里取当前选中行的主键值然后执行数据库删除。取主键的方式是dgvRooms.CurrentRow.Cells[RoomId].Value注意这里RoomId是列名必须在AutoGenerateColumns false的情况下通过DataPropertyName映射过否则取到的是显示列而不是主键列。删除后重新调用加载列表的方法刷新界面让用户看到的是数据库里的真实状态。5.5 前台并发操作同一间房入住登记时状态判断失效现象两个前台同时选中一间空闲房办理入住两个窗口都能进入住界面提交后房间被登记两次状态错乱。原因界面加载时查到房间状态是 0然后走流程中间没人再验证一次状态。两次提交各自成功数据库里就出现两条入住单绑定同一房间。解决在入住登记的 UPDATE 语句里加状态条件这叫乐观锁。把更新房间的 SQL 改成UPDATE Room SET Status 1 WHERE RoomId roomId AND Status 0如果返回的受影响行数为 0说明房间已经被别人占用了这时弹出提示并刷新列表。一个WHERE条件就能堵住并发漏洞比用锁表、事务隔离级别简单得多也非常适合在答辩时说明“我考虑了并发场景”。6. 让课程设计从“能跑”升级到“能答辩”异常捕获、账单打印与数据备份很多人的项目止步于“功能能用”但答辩时老师问几个边界问题就露馅。让这套系统显得成熟有三个投入产出比很高的进阶点。第一个是全局异常捕获。WinForms 默认的报错对话框显示一堆英文堆栈很掉档次。在Program.cs的Main方法里挂两个事件Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); Application.ThreadException (sender, e) { MessageBox.Show(发生未处理异常 e.Exception.Message, 系统提示, MessageBoxButtons.OK, MessageBoxIcon.Warning); };这样任何未捕获异常都会被拦下来弹一个带中文提示的窗口而不是默认的崩溃界面。自己排查问题时把e.Exception.StackTrace同时写入日志文件比在崩溃现场抓瞎强得多。第二个是账单打印。退房结算完成后用一个PrintDocument把消费明细、房费、押金、实收金额画出来输出到打印机或预览窗体。哪怕只是简单的文字排版答辩时也比“我们只能在界面上看一眼”高一个档次。关键数据用String.Format统一格式化金额保留两位小数日期显示到分钟单据号用自增主键补齐前导零这些格式问题提前处理打印效果才像真的。第三个是数据备份按钮。在系统设置界面放一个“备份数据库”按钮本质就是复制.mdf文件到指定目录。用File.Copy前先通过CHECKPOINT让 SQL Server 把脏页写入文件否则备份的库可能缺失最后几笔数据。备份文件名加上日期戳如HotelDB_20240115_0830.bak就不怕覆盖旧备份。我做这套系统打的最惨的一次是改了一下午表结构却忘记导出 SQL 脚本第二天打开项目连的是旧库所有新字段都报“列名无效”。从那以后我养成两个习惯每次改完表结构就导出一份.sql脚本连同项目代码一起存档每次答辩演示前先跑一遍入住到退房的完整流程确认数据不丢。这套系统本身不难难的是把每一步都做得滴水不漏。希望帮到你。本文还有配套的精品资源点击获取
返回列表