资讯动态

C# WinForm酒店管理系统:入住退房事务与房态闭环解析

发布时间:2026/10/23 8:24:46 来源:尧图企业网站定制
简介这是一套基于C# WinForm与SQL Server的酒店管理系统完整项目包面向计算机专业课程大作业、毕业设计以及WinForm入门进阶学习者。系统按管理员、学生两类角色划分权限管理员可维护账号与学生信息并完成课程开设、查询、成绩录入和统计等操作适合用来理解桌面端管理系统的业务组织方式。压缩包共617个文件、约96.58MB包含39个cs源码、66个db数据库、23个dll依赖、15组resx与resources窗体资源以及gif、jpg等界面素材工程文件、解决方案和数据库文件相对齐全。已有173人学习可直接用Visual Studio打开sln参考窗体布局、数据库访问、角色权限控制和成绩统计等实现也便于替换业务模块进行二次开发与答辩演示。1. 酒店管理系统先搞清楚它到底管了什么答辩前一晚最容易出问题的环节往往不是代码写不出来而是现场演示。老师随手点一次“入住”界面弹窗提示成功房态那一栏还是绿色的“空闲”后台一查入住单根本没有写进数据库。这份基于 C# WinForm 窗体的酒店管理系统把客房、预订、入住登记、退房结账、会员和统计报表全都放进了同一套桌面程序里属于典型的 C# 管理系统大作业、毕业设计规模代码量不大但链路完整数据库十来张表熟悉 WinForm 的人两天就能上手改。它真正值钱的地方不在一堆窗体而在于房态流转、账单计算、权限控制这条闭环跑通了——这正是多数同学生搬硬套做不出来的部分。2. 目录结构与技术栈解压后先翻这五个地方2.1 从解决方案文件到 DB 目录解压之后别急着双击.sln解决方案文件。先看目录结构基本就能判断这份代码是照着教程拼出来的还是真的跑过一遍。这类项目的目录通常长这样HotelManager/ ├─ HotelManager.sln # 双击进 IDE 的入口 ├─ HotelManager/ │ ├─ Program.cs # Main 方法全局异常和登录窗体在这里起 │ ├─ App.config # 连接串换机器第一件事就是改它 │ ├─ Forms/ # 全部窗体一屏一个业务动作 │ │ ├─ FrmLogin.cs │ │ ├─ FrmMain.cs # 主框架左侧菜单 右侧内容面板 │ │ ├─ FrmCheckIn.cs # 入住登记 │ │ ├─ FrmCheckOut.cs # 退房结账 │ │ ├─ FrmRoomManage.cs # 客房与房态 │ │ ├─ FrmBook.cs # 预订 │ │ └─ FrmReport.cs # 统计报表 │ ├─ DAL/ # 数据访问层 │ │ ├─ SqlHelper.cs # 全项目唯一的数据库出入口 │ │ ├─ RoomDAL.cs │ │ └─ OrderDAL.cs │ ├─ Models/ # 实体类字段和表一一对应 │ └─ Utils/ # 枚举、校验、导出辅助 └─ DB/ └─ HotelDB.sql # 建库建表脚本含少量初始房型数据判断标准很简单有DB目录且带.sql脚本的说明作者至少考虑过别人怎么部署只有.mdf文件或者干脆让你“自己还原数据库”的八成是他自己机器上跑通过一次就打包了。Forms目录下的窗体命名也能反映分层——如果FrmCheckIn.cs里直接出现了new SqlConnection(...)那这个项目名义上有 DAL实际上数据访问还是散落在每一个按钮的Click事件里改造时这就是第一个需要处理的问题。提示拿到 zip 先别编译用文本编辑器打开App.config把连接串那几行抄下来。后面所有关于“跑不起来”的问题八成和这几行有关系。2.2 技术栈盘点.NET Framework WinForm ADO.NET打开.csproj文件看TargetFrameworkVersion这一行就知道这份代码吃的是哪一代运行时。这类管理系统绝大多数落在 .NET Framework 4.x 区间因为 WinForm 的拖拽设计器、第三方表格控件、报表设计器在 Framework 下最省事。换成新的 .NET窗体设计器能用但老控件和System.Configuration那套配置读取方式要改对应届生来说不值得。数据访问这一层没有 ORM走的是纯 ADO.NET 加手写 SQL。这一点对毕业设计来说反而是好事——答辩老师问“你的 SQL 注入怎么防的”你能指着SqlParameter说得清楚换成 EF 之后很多人连生成出来的 SQL 长什么样都不知道一旦查询慢就是个难以透析的黑匣子。技术栈大致是这么几个部分部分用的东西在项目里的角色界面WinForm 窗体 DataGridView所有业务操作入口一屏一动作数据访问ADO.NETSqlConnection/SqlCommand/SqlDataAdapter手写 SQLSqlHelper统一封装数据库SQL ServerExpress 或 LocalDB 居多十来张表脚本在 DB 目录配置App.config ConfigurationManager连接串、默认房价参数报表PrintDocument 或表格控件导 Excel交班对账、营收统计其中报表部分是这类项目最常见的半成品。很多大作业的“统计报表”就是弹一个窗体、塞一个 DataGridView、截个图放进论文根本没有导出和打印。真正要交付用这块是必须补的后面第六章会讲怎么补最省事。2.3 十来张表撑起的业务模型表设计决定了这个系统好不好改。房态字段用什么类型存是整个模型的关键。见过太多版本用VARCHAR存“空闲”“已入住”“待清扫”结果某天改文案写成“已入住 ”多一个空格全系统查不到房。合格的写法是用TINYINT配枚举-- 房间表State 用数字是整个系统的地基 CREATE TABLE Room ( RoomId INT IDENTITY(1,1) PRIMARY KEY, RoomNo VARCHAR(10) NOT NULL UNIQUE, -- 301、302 这种房号 TypeId INT NOT NULL, -- 房型标间 / 大床 / 套房 FloorNo INT NULL, State TINYINT NOT NULL DEFAULT 0,-- 0空闲 1已预订 2已入住 3待清扫 4维修 Remark NVARCHAR(200) NULL ); -- 入住单一个客人一次入住一行退房时回填 OutTime 和 TotalFee CREATE TABLE CheckIn ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(20) NOT NULL, RoomId INT NOT NULL, GuestName NVARCHAR(30) NOT NULL, IdCard VARCHAR(18) NULL, Phone VARCHAR(20) NULL, InTime DATETIME NOT NULL, PlanOutTime DATETIME NOT NULL, OutTime DATETIME NULL, Deposit DECIMAL(10,2) NOT NULL DEFAULT 0, TotalFee DECIMAL(10,2) NULL, Status TINYINT NOT NULL DEFAULT 0 -- 0在住 1已退房 2已取消 ); -- 消费单住店期间的餐费、洗衣、迷你吧退房时一起结 CREATE TABLE Consume ( ConsumeId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL, ItemName NVARCHAR(50) NOT NULL, Amount DECIMAL(10,2) NOT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() );金额一律用DECIMAL(10,2)不要用FLOAT。这一点看起来无关紧要等你算出的房价是299.99999999997前台当着客人面念出来就尴尬了。房态用数字还有个直接好处枚举和数字一一对应DataGridView里做一次转换就能显示中文改文案不用动数据库。3. 入住登记链路从窗体验证到事务写入数据库3.1 SqlHelper全项目唯一的数据库出入口不管这份代码现在有多少地方在直接new SqlConnection改造的第一步都是把所有数据库操作收到一个静态类里。这既是规范也是给自己留一条处理问题的线索——出问题只查一个文件而不是在二十个窗体里翻。四个方法够用public static class SqlHelper { // 连接串只在 App.config 里存一份换机器改一处 private static readonly string ConnStr ConfigurationManager.ConnectionStrings[HotelConn].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 ps.Length 0) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } // 查单个值比如“这个房号存在吗”“当前房态是什么” 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 ps.Length 0) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteScalar(); } } // 查表直接喂给 DataGridView public static DataTable ExecuteDataTable(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(ConnStr)) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { if (ps ! null ps.Length 0) da.SelectCommand.Parameters.AddRange(ps); DataTable dt new DataTable(); da.Fill(dt); return dt; } } }using不是为了好看。SqlConnection没释放连接池被占满之后程序表现就是“卡在登录界面十几秒然后报超时”而且换了电脑还能复现很容易被当成玄学问题。params SqlParameter[]这个写法让调用方可以传零个参数也让拼接 SQL 这件事从代码习惯上被堵死——只要全项目统一走这个方法注入问题就不会出现。3.2 房态枚举与房间查询窗体上那一排五颜六色的房态卡片本质上就是把数据库的State字段翻译成人能看懂的东西。枚举定义和显示名分开写改文案的时候只动一处public enum RoomState { Free 0, // 空闲 Booked 1, // 已预订 Occupied 2, // 已入住 Dirty 3, // 待清扫 Repair 4 // 维修中 } // 界面上显示的中文和枚举值一一对应 public static string StateText(int state) { switch (state) { case 0: return 空闲; case 1: return 已预订; case 2: return 已入住; case 3: return 待清扫; case 4: return 维修中; default: return 未知; } }房间列表查询按房号和楼层过滤参数化写法如下。注意这里的kw 判断是为了让“不输入关键字”和“输入关键字”共用一条 SQL免得在代码里用if拼字符串public static DataTable QueryRooms(string keyword) { string sql SELECT r.RoomId, r.RoomNo, t.TypeName, r.FloorNo, r.State FROM Room r LEFT JOIN RoomType t ON r.TypeId t.TypeId WHERE (kw OR r.RoomNo LIKE % kw %) ORDER BY r.FloorNo, r.RoomNo; return SqlHelper.ExecuteDataTable(sql, new SqlParameter(kw, keyword ?? string.Empty)); }LEFT JOIN而不是INNER JOIN是因为房型表里如果漏了某个TypeId用内连接会让整间房从列表里消失——查不出原因的那种消失。这种细节不影响跑通但影响你排错时的判断。3.3 入住登记一个必须走事务的动作这是整个系统里唯一不能省事务的地方。一次入住要同时做三件事写入住单、改房态、扣预订记录如果是预订转入住。任何一步失败另外两步都必须撤回来否则就会出现“房态显示已入住但入住单表里没有这一行”的经典脏数据。public static bool CheckIn(int roomId, string guestName, string idCard, DateTime inTime, DateTime planOut, decimal deposit, out string msg) { msg string.Empty; using (SqlConnection conn new SqlConnection(ConnStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 1) 加更新锁读房态防止两个前台同时给同一间房办入住 string sqlState SELECT State FROM Room WITH (UPDLOCK, ROWLOCK) WHERE RoomIdrid; SqlCommand cmdState new SqlCommand(sqlState, conn, tran); cmdState.Parameters.Add(new SqlParameter(rid, roomId)); object state cmdState.ExecuteScalar(); if (state null) { msg 房间不存在; tran.Rollback(); return false; } if (Convert.ToInt32(state) (int)RoomState.Occupied) { msg 该房间已有客人在住; tran.Rollback(); return false; } // 2) 写入住单 string sqlOrder INSERT INTO CheckIn(OrderNo, RoomId, GuestName, IdCard, InTime, PlanOutTime, Deposit, Status) VALUES(no, rid, name, id, in, plan, dep, 0); SqlCommand cmdOrder new SqlCommand(sqlOrder, conn, tran); cmdOrder.Parameters.Add(new SqlParameter(no, CI DateTime.Now.ToString(yyMMddHHmmss))); cmdOrder.Parameters.Add(new SqlParameter(rid, roomId)); cmdOrder.Parameters.Add(new SqlParameter(name, guestName)); cmdOrder.Parameters.Add(new SqlParameter(id, (object)idCard ?? DBNull.Value)); cmdOrder.Parameters.Add(new SqlParameter(in, inTime)); cmdOrder.Parameters.Add(new SqlParameter(plan, planOut)); cmdOrder.Parameters.Add(new SqlParameter(dep, deposit)); cmdOrder.ExecuteNonQuery(); // 3) 改房态 string sqlRoom UPDATE Room SET States WHERE RoomIdrid; SqlCommand cmdRoom new SqlCommand(sqlRoom, conn, tran); cmdRoom.Parameters.Add(new SqlParameter(s, (int)RoomState.Occupied)); cmdRoom.Parameters.Add(new SqlParameter(rid, roomId)); cmdRoom.ExecuteNonQuery(); tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); msg 入住失败 ex.Message; return false; } } } }三个参数值得单独说。WITH (UPDLOCK, ROWLOCK)是给房间那一行加更新锁作用范围就在这个事务内别人想读会等想同时改就会被挡住——这是防并发重复入住最省事的做法比在应用层加锁靠谱。(object)idCard ?? DBNull.Value处理的是空身份证号直接传null给SqlParameter在某些情况下会抛异常转成DBNull才算干净。OrderNo用时间戳拼够用但不够严谨如果一天内同一秒有两单就会撞号改成时间戳 房号或者干脆建个自增序列更稳妥。4. 退房结账夜数、消费和房态回收4.1 房费怎么算才不被前台追着问退房那一屏最容易被追问。客人 14:00 入住、次日 12:00 退房按小时算是 22 小时按天算是不到 1 天——直接拿TimeSpan.TotalDays取整会算出 0 元。酒店行业的通行规则是按“夜”计跨一个零点算一夜当天进当天出至少算一夜public static decimal CalcRoomFee(DateTime inTime, DateTime outTime, decimal dayPrice) { // 按自然日差算夜数14:00 进、次日 12:00 出 1 夜 int nights (outTime.Date - inTime.Date).Days; // 当天进当天出按一夜收不能算出 0 if (nights 0) nights 1; return nights * dayPrice; }dayPrice要从房型表实时读不能缓存在窗体字段里。原因很实际管理员在另一个窗口改了房价退房窗口还拿着登录时加载的旧价格两边对不上账客人一算就发现多收了。查询走一次数据库几十毫秒的事。消费部分单独一句聚合就能拿到总额public static decimal GetConsumeTotal(int orderId) { string sql SELECT ISNULL(SUM(Amount), 0) FROM Consume WHERE OrderIdoid; object v SqlHelper.ExecuteScalar(sql, new SqlParameter(oid, orderId)); return v null ? 0m : Convert.ToDecimal(v); }ISNULL(SUM(...), 0)这个写法是必要的。没有消费记录时SUM返回NULLConvert.ToDecimal(null)会直接抛出异常而这个异常发生在退房按钮里前台看到的就是“程序未响应”。在没有消费记录的绝大多数场景下这条路径每天都会被走到。4.2 退房事务与状态回收退房的写操作比入住还多一步回填入住单的OutTime和TotalFee、改房态为“待清扫”、写一条交班流水。这三个动作同样绑在一个事务里public static bool CheckOut(int orderId, int roomId, DateTime outTime, decimal totalFee, out string msg) { msg string.Empty; using (SqlConnection conn new SqlConnection(ConnStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 回填账单Status 置为已退房 string sql1 UPDATE CheckIn SET OutTimeout, TotalFeefee, Status1 WHERE OrderIdoid AND Status0; SqlCommand c1 new SqlCommand(sql1, conn, tran); c1.Parameters.Add(new SqlParameter(out, outTime)); c1.Parameters.Add(new SqlParameter(fee, totalFee)); c1.Parameters.Add(new SqlParameter(oid, orderId)); // 影响行数为 0 说明这单已经被退过了直接拦掉 if (c1.ExecuteNonQuery() 0) { msg 该订单已退房或不存在; tran.Rollback(); return false; } // 房态改成待清扫不是直接改空闲 string sql2 UPDATE Room SET States WHERE RoomIdrid; SqlCommand c2 new SqlCommand(sql2, conn, tran); c2.Parameters.Add(new SqlParameter(s, (int)RoomState.Dirty)); c2.Parameters.Add(new SqlParameter(rid, roomId)); c2.ExecuteNonQuery(); tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); msg 退房失败 ex.Message; return false; } } } }WHERE OrderIdoid AND Status0这个条件加上之后重复点击退房按钮只会第二次返回“已退房”不会重复扣费。退房后房态置为“待清扫”而不是“空闲”是业务上的一处关键设计——保洁没打扫完之前前台不应该能把这间房卖出去。很多大作业图省事直接置空闲演示时看不出来真用一天就会遇到客人拿到没收拾的房间。4.3 DataGridView 的绑定、着色与刷新房间列表是前台盯得最久的一屏可读性直接影响操作失误率。绑定的核心是“数据源只读、显示靠转换”不要在CellFormatting里再去查数据库private void LoadRoomGrid(string keyword) { DataTable dt RoomDAL.QueryRooms(keyword); dgvRoom.AutoGenerateColumns false; // 关掉自动列用设计器里定好的列 dgvRoom.DataSource dt; // 房态列显示中文顺便按状态给整行上色 foreach (DataGridViewRow row in dgvRoom.Rows) { int state Convert.ToInt32(row.Cells[colState].Value); row.Cells[colState].Value RoomStateHelper.StateText(state); switch ((RoomState)state) { case RoomState.Free: row.DefaultCellStyle.BackColor Color.Honeydew; break; case RoomState.Occupied: row.DefaultCellStyle.BackColor Color.MistyRose; break; case RoomState.Dirty: row.DefaultCellStyle.BackColor Color.LightYellow; break; case RoomState.Repair: row.DefaultCellStyle.BackColor Color.LightGray; break; } } }AutoGenerateColumns false必须显式设上。开着自动列绑定之后你写的中文表头会被数据库字段名覆盖掉改一次列顺序下次绑定又乱。用设计器里的固定列还有个好处列名colState能直接作为索引访问比用数字下标Cells[4]稳定得多以后表结构加字段也不会让代码错位。刷新时机同样要注意。退房成功后不能只在CheckOut窗体里更新那一行主界面的房间列表也得刷——否则前台看着一间“已入住”的房间客人已经走了十分钟。最简单的做法是每次操作完直接LoadRoomGrid(txtSearch.Text)整表重载房间数量在几百以内完全没有性能问题不要为了省这点开销去搞增量更新那才是后续维护的麻烦来源。5. 避坑与排查五条把大作业拖到凌晨的记录5.1 启动阶段连接串与数据库附加坑一换台电脑打开就报“未找到或无法连接服务器”现象在自己机器上双击就能跑拷给同学或者拷到答辩教室的电脑上登录窗体刚出来就弹异常框提示连不上数据库实例。原因App.config里的连接串写的是本机的实例名比如Data Source.\SQLEXPRESS而对方机器上装的是LocalDB实例名是(localdb)\MSSQLLocalDB两边对不上。还有一种是用了AttachDbFilename附加.mdf的写法路径写成了绝对路径D:\项目\DB\HotelDB.mdf换机器直接找不到文件。解决连接串改成相对路径加|DataDirectory|占位符程序启动时把数据目录指到可执行文件旁边// Program.cs 的 Main 里第一件事就是定数据目录 string dbDir Path.Combine(Application.StartupPath, DB); AppDomain.CurrentDomain.SetData(DataDirectory, dbDir);连接串相应写成AttachDbFilename|DataDirectory|\HotelDB.mdf。另外在FrmLogin的Load里先做一次试探连接连不上就弹一个明确的中文提示而不是让异常直接抛到用户脸上。坑二数据库是.mdf文件但本机装的是另一个版本的 SQL Server现象附加文件时报“数据库版本不兼容”或“该文件由较新版本创建”。原因.mdf文件的内部版本号和创建它的数据库引擎版本绑定高版本创建的库在低版本实例上根本打不开这是向下不兼容的。解决优先用DB目录里的.sql脚本重建库脚本是纯文本跨版本没问题。如果只有.mdf就用对方机器上同版本的实例去附加或者在自己机器上导出成脚本再带过去。这也是为什么我前面强调判断一个包靠不靠谱先看有没有.sql。5.2 运行阶段房态与账目对不上坑三提示“入住成功”但房态没变 / 入住单没记录现象点入住按钮弹窗显示成功关掉再打开房态还是“空闲”或者房态变了但查不到订单。原因写入住单和改房态是两条独立执行的 SQL中间没有事务。第一条成功第二条失败时界面上的提示已经弹出来了用户以为都成了。解决按第 3.3 节的写法把两条 SQL 塞进同一个SqlTransaction任何一条抛异常就整体回滚返回值由ExecuteNonQuery的行数判断不靠try有没有抛异常来判断业务是否成功。坑四退房后房间一直是“已入住”第二天卖不出去现象客人明明走了账单也结了房间列表里还是红的。原因退房只更新了CheckIn表忘了同步Room.State或者同步了但主界面没刷新DataGridView抱着一份旧DataTable不放。解决事务里同时改两张表操作完成后统一调一次列表重载。判断标准是“点完按钮列表立刻变”做不到就说明刷新链路断了一环。5.3 数据阶段日期区间、金额精度与编码坑五按日期查营收当天的记录永远查不出来现象查“10 月 1 日到 10 月 1 日”的营业额是 0但明明有单。原因DateTimePicker拿到的值带时分秒2024-10-01 00:00:00到2024-10-01 00:00:00这个区间长度为 0一条都匹配不上。如果数据库里的时间还带毫秒边界更加不可控。解决查询前把结束时间取到当天最后一刻或者干脆用“小于次日零点”的半开区间写法DateTime start dtpStart.Value.Date; DateTime end dtpEnd.Value.Date.AddDays(1); // 半开区间天然覆盖全天 string sql SELECT COUNT(*) AS 单数, ISNULL(SUM(TotalFee),0) AS 营收 FROM CheckIn WHERE Status 1 AND OutTime s AND OutTime e; DataTable dt SqlHelper.ExecuteDataTable(sql, new SqlParameter(s, start), new SqlParameter(e, end)); s AND e这个写法比BETWEEN稳因为BETWEEN两端都是闭区间遇到带毫秒的时间就会漏掉最后一条。金额聚合那句的ISNULL也别省否则查询到空区间时整个报表窗体直接抛出异常。6. 二次开发把它改成能拿去答辩也能真跑的系统6.1 三处性价比最高的改造第一处是给所有写操作补事务。原项目里只要出现“一次操作改两张表”就必须包SqlTransaction这是投入产出比最高的改动改完之后同类型的脏数据问题基本绝迹。第二处是把SqlHelper里再加一个带SqlTransaction参数的重载让事务内的命令不用重复写new SqlCommand(sql, conn, tran)三件套public static int ExecuteNonQuery(SqlConnection conn, SqlTransaction tran, string sql, params SqlParameter[] ps) { using (SqlCommand cmd new SqlCommand(sql, conn, tran)) { if (ps ! null ps.Length 0) cmd.Parameters.AddRange(ps); return cmd.ExecuteNonQuery(); } }第三处是给登录加角色维度。员工表加一个RoleIdFrmMain加载时按角色隐藏菜单项比在每个按钮里写if判断干净得多答辩时讲权限控制也有东西可讲// 登录成功后把角色带到主窗体 if (LoginUser.RoleId ! 1) // 1 管理员 { menuStaff.Visible false; // 员工管理 menuReport.Visible false; // 财务报表 }6.2 改完怎么验证没改坏别手动点手动点一定会漏。最快的验证方式是直接在数据库里跑几条对抗性 SQL先插一条入住单但不改房态再跑一次退房流程看会不会出现“已退房但仍然已入住”的房间把Room.State手动改成非法的9看列表会不会整屏报错。这两条能过说明事务和状态转换的边界基本守住了。验证项操作期望结果并发入住两个窗口同时给同一间房办入住一个成功一个提示“已有客人在住”重复退房对同一订单连点两次退房第二次提示“已退房”账单不重复空消费退房无消费记录的订单直接退房正常结算不弹异常非法房态手动把 State 改成 9列表正常加载显示“未知”而不是崩溃从那以后我每次接这类 WinForm 管理系统第一件事都是先把SqlHelper里的方法数一遍看看有几个入口在绕开它直接连库——绕开的那几处就是后面所有棘手问题的源头。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑