ARTICLE DETAIL

资讯详情

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

C# WinForms医院挂号管理系统源码拆解:架构、核心业务与避坑指南

C# WinForms医院挂号管理系统源码拆解:架构、核心业务与避坑指南 简介这是一套基于C#与WinForm开发的医院挂号管理系统采用C/S架构与MVC分层设计适合正在学习WinForm开发、课程设计或毕业设计的学生参考借鉴。系统覆盖用户管理、科室管理、医生管理、门急诊挂号、挂号查询、挂号单打印、修改口令及帮助文档等完整模块并通过存储过程操作数据库、MD5加密用户密码同时以报表图文形式直观展示统计数据还包含医生照片上传功能。资源包共185个文件以64个C#源码文件为主体辅以SQL脚本、报表文件、可执行程序、帮助文档、界面图片及项目工程配置整体约10.03MB结构清晰便于按模块阅读与运行。目前已有1263人学习下载适合希望从实际案例中理解分层架构、数据库设计与桌面端业务流程的开发者。1. 一套能直接改着用的 C# WinForms 医院挂号管理系统朋友接了个街道卫生中心的挂号改造原系统挂号、交费分两个窗口护士每天对账时总会多出几笔重复挂号。他熬了两周开发上线才一天就被护士长叫了过去——同一个患者在同一号源上挂了两次。我让他别再自己硬扛直接拿这套用 C# WinForms 写的医院挂号管理系统做底子。拆完源码我确认它覆盖科室、医生、排班、挂号、退号、查询统计的完整闭环数据库脚本和窗体代码齐全适合课程设计、毕业设计也适合刚开始上手 WinForms 的开发者搭自己的第一套管理信息系统。单机或局域网部署几小时就能跑起来。和网上那些只有登录界面和 CRUD 的半成品不同这套东西真正需要处理的是号源并发、退号状态、日期查询这些业务逻辑。这篇笔记我会按拆项目的顺序讲清楚架构、核心业务、界面交互、常见坑最后给你一个交付前的验证习惯。读完你心里有数改的时候就知道哪些地方不能动。2. 项目结构与数据库设计先看懂这套源码的地图再改代码拆一个 WinForms 项目最忌讳上来就双击窗体。界面文件三五分钟能看完但数据流理不清后面每改一个功能都要翻半天。我拿到手第一件事是列表视图看目录第二件事找 App.config先把地图画出来。2.1 拿到源码先看这几处分层目录、启动入口、连接字符串这类课程设计项目通常是一个解决方案里放四个项目UI 层负责窗体BLL 层放业务规则DAL 层管数据库操作Model 层放实体类。也有的压缩包里把所有 .cs 文件堆在一个项目下靠文件夹区分逻辑是一样的。你在Hospital.sln里双击打开后先看项目引用关系UI 项目是否引用了 BLLBLL 是否引用了 DAL这个链条决定你能不能在窗体里直接 new 一个业务类。启动入口在 Program.cs正常情况下里面只有一行Application.Run(new MainForm())。MainForm 是登录后的主窗体壳子左侧菜单、右侧内容区都从这里出来。如果登录失败问题多半出在数据库连接而不是登录逻辑本身。接下来打开 App.config找到 connectionStrings 节点这是整个系统的命脉。常见写法是Data Source.;Initial CatalogHospitalDB;Integrated SecurityTrue.代表本机默认 SQL Server 实例。如果你拿到的是 Access 版本连接串会是ProviderMicrosoft.ACE.OLEDB.12.0;Data Source|DataDirectory|Hospital.accdb。先确认这台机器装了对应数据库再谈改代码。2.2 四张核心表科室、医生、排班与挂号记录我拆过很多挂号和预约类系统表结构大同小异核心就是这四张表。源码包里通常附带hospital.sql或db.sql里面建表语句大致是这个形态CREATE TABLE Department ( DeptId INT IDENTITY(1,1) PRIMARY KEY, DeptName NVARCHAR(50) NOT NULL ); CREATE TABLE Doctor ( DoctorId INT IDENTITY(1,1) PRIMARY KEY, DeptId INT NOT NULL REFERENCES Department(DeptId), DoctorName NVARCHAR(20) NOT NULL, Title NVARCHAR(20) NULL ); CREATE TABLE Schedule ( ScheduleId INT IDENTITY(1,1) PRIMARY KEY, DoctorId INT NOT NULL REFERENCES Doctor(DoctorId), WorkDate DATE NOT NULL, ShiftTime VARCHAR(10) NOT NULL, TotalSlots INT NOT NULL DEFAULT 20, UsedSlots INT NOT NULL DEFAULT 0 ); CREATE TABLE Register ( RegisterId INT IDENTITY(1,1) PRIMARY KEY, ScheduleId INT NOT NULL REFERENCES Schedule(ScheduleId), PatientName NVARCHAR(20) NOT NULL, PatientPhone VARCHAR(11) NULL, Amount DECIMAL(8,2) NOT NULL, Status TINYINT NOT NULL DEFAULT 0 );先说关系Department 是科室Doctor 挂在科室下Schedule 是某医生某天的排班TotalSlots 表示号源总数UsedSlots 表示已经挂出去的数量Register 是挂号记录通过 ScheduleId 关联到具体排班。Status 字段是 TINYINT0 已挂号、1 已就诊、2 已退号这就是一个隐性的状态机。这里有个设计思路值得注意号源用 TotalSlots 和 UsedSlots 两个计数器来表示而不是每挂一个号就删一条排班记录。好处是退号时可以恢复号源也能随时统计某个排班的使用率。改代码时不要为了省事把这两个字段合二为一否则退号逻辑会直接翻车。2.3 数据访问层 SqlHelper参数化查询与连接串边界整个项目里所有窗体都在调 SqlHelper 这个静态类。它封装了最常见的两种操作查 DataTable 和执行增删改。源码里的实现基本长这样public static class SqlHelper { private static readonly string _connStr ConfigurationManager.ConnectionStrings[hospital].ConnectionString; public static DataTable ExecuteDataTable(string sql, params SqlParameter[] ps) { using (var conn new SqlConnection(_connStr)) using (var cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); var da new SqlDataAdapter(cmd); var dt new DataTable(); da.Fill(dt); return dt; } } public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (var conn new SqlConnection(_connStr)) using (var cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } }逻辑说明ExecuteDataTable 返回的是内存中的 DataTable适合绑定 DataGridView 和下拉框SqlDataAdapter 会自动打开和关闭连接ExecuteNonQuery 返回受影响行数适合 INSERT、UPDATE、DELETE。所有 SQL 都通过 SqlParameter 传值不要用字符串拼接不然患者姓名里带个单引号就能让整个查询崩掉。参数说明ConnectionStrings[hospital] 对应 App.config 里的连接串名称如果你把自己的库改名了这里也要一起改。ps 参数支持可变参数调用时直接new SqlParameter(id, 5)就行。这里有一个常见隐患AddWithValue 会用字符串推断类型遇到日期字段容易出问题所以我一般建议显式cmd.Parameters.Add(date, SqlDbType.Date)后面排班查询那节会展开讲。3. 挂号与退号核心业务事务、锁与状态流转的三个硬骨头挂号系统的业务难点不在窗体而在“一个号不能被两个人抢走”和“退号不能越退越多”。这两件事处理不好上线第一天就会被现场人员骂回来。3.1 挂号流程先锁行再扣号源先看一个错误做法有些半成品源码是先SELECT UsedSlots在 C# 代码里判断小于 TotalSlots然后执行 UPDATE。这在单机测试时没问题但两台收费电脑同时操作时两个进程都读到 UsedSlots19都以为还有号结果多挂出去一笔。解决思路是把“检查号源”和“扣减号源”放进同一个数据库事务并用行锁挡住并发。核心方法大致是public bool CreateRegister(int scheduleId, string patientName, string patientPhone, decimal amount, out string msg) { msg ; using (var conn new SqlConnection(_connStr)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { var checkSql SELECT TotalSlots, UsedSlots FROM Schedule WITH (UPDLOCK, ROWLOCK) WHERE ScheduleId sid; using (var cmd new SqlCommand(checkSql, conn, tx)) { cmd.Parameters.AddWithValue(sid, scheduleId); using (var reader cmd.ExecuteReader()) { if (!reader.Read()) { msg 排班不存在; return false; } int total reader.GetInt32(0); int used reader.GetInt32(1); if (used total) { msg 号源已挂满; return false; } } } var updSql UPDATE Schedule SET UsedSlots UsedSlots 1 WHERE ScheduleId sid; using (var cmd new SqlCommand(updSql, conn, tx)) { cmd.Parameters.AddWithValue(sid, scheduleId); cmd.ExecuteNonQuery(); } var insSql INSERT INTO Register (ScheduleId, PatientName, PatientPhone, Amount, Status) VALUES (sid, name, phone, amount, 0); using (var cmd new SqlCommand(insSql, conn, tx)) { cmd.Parameters.AddWithValue(sid, scheduleId); cmd.Parameters.AddWithValue(name, patientName); cmd.Parameters.AddWithValue(phone, patientPhone ?? ); cmd.Parameters.AddWithValue(amount, amount); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch (Exception ex) { tx.Rollback(); msg ex.Message; return false; } } } }逻辑说明WITH (UPDLOCK, ROWLOCK)表示查询时就锁定这一行直到事务结束第二个客户端只能等第一个提交后才读到新值。先查后改再插入三步在同一个事务里任何一步失败都整体回滚。我用out msg把错误信息带回窗体界面可以直接弹 MessageBox不用在窗体里再解析返回值。参数说明sid是排班 IDamount是挂号费phone里的空字符串是避免 NULL 值导致后续报表统计报错。这个方法已经被我改造成一个通用模板很多管理系统的库存扣减、预约占位都能套用。提示如果运行时报“The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION”检查事务里的每个 SqlCommand 是不是都传了conn, tx两个参数漏传 tx 就会脱离事务。3.2 退号要拦住重复退条件更新的妙用退号比挂号更容易写错。很多人的第一版退号代码是这样的先SELECT Status判断是 0 就执行两个 UPDATE。但用户双击退号按钮或两个窗口同时操作同一笔记录就会连续执行两次恢复号源导致 UsedSlots 越退越多。正确做法是把条件和更新合并成一个原子操作。public bool RefundRegister(int registerId, out string msg) { using (var conn new SqlConnection(_connStr)) { conn.Open(); using (var tx conn.BeginTransaction()) { int affected 0; using (var cmd new SqlCommand( UPDATE Register SET Status 2 WHERE RegisterId rid AND Status 0, conn, tx)) { cmd.Parameters.AddWithValue(rid, registerId); affected cmd.ExecuteNonQuery(); } if (affected 0) { tx.Rollback(); msg 该挂号记录不存在或已经退过号; return false; } using (var cmd new SqlCommand( UPDATE Schedule SET UsedSlots UsedSlots - 1 WHERE ScheduleId (SELECT ScheduleId FROM Register WHERE RegisterId rid), conn, tx)) { cmd.Parameters.AddWithValue(rid, registerId); cmd.ExecuteNonQuery(); } tx.Commit(); msg ; return true; } } }核心在WHERE RegisterId rid AND Status 0这一句。受影响行数为 0 表示这单已经退过或者记录根本不存在直接回滚不执行后面的恢复号源操作。这就是一个最简单的状态机0 已挂号、1 已就诊、2 已退号只有状态为 0 时才能走到 2。如果你拿到源码里退号直接UPDATE Register SET Status2没有 WHERE 条件赶紧补上这是整套系统里最容易越改越错的地方。3.3 排班查询的日期时间陷阱排班查询写起来简单翻车率却在 C# 和 SQL Server 之间的日期类型转换上。C# 的DateTimePicker.Value是一个带时间的 DateTime比如 2025-06-20 00:00:00。SQL Server 里 WorkDate 如果是 DATE 类型直接等值查询没问题如果是 DATETIME 类型直接等值就会查不到数据因为数据库里的值是 2025-06-20 08:30:00。我一般采用两套写法-- 表结构是 DATE 类型时直接等值 SELECT * FROM Schedule WHERE WorkDate date; -- 表结构是 DATETIME 类型时用范围查询 SELECT * FROM Schedule WHERE WorkDate date AND WorkDate DATEADD(day, 1, date);对应的 C# 传参也有讲究。cmd.Parameters.AddWithValue(date, datePicker.Value.Date)会按字符串推断大多数情况下能跑通但遇到本地化日期格式就容易出幺蛾子。稳妥做法是指定参数类型cmd.Parameters.Add(date, SqlDbType.Date).Value datePicker.Value.Date;参数说明SqlDbType.Date强制按日期类型传给 SQL Server不会走字符串转换。用DATEADD(day, 1, date)取第二天作为上界比WorkDate date.AddDays(1)更清晰因为所有日期计算都留在数据库这侧。改排班查询时先看表结构再选写法省去很多现场排错。4. WinForms 界面交互实战菜单折叠、DataGridView 与界面美化界面部分没有太多玄学但控件属性设置对了观感和操作流畅度会提升一大截。这一章讲三个高频场景左侧导航折叠、DataGridView 列绑定、状态栏进度条都是热词里被频繁搜到的点。4.1 左侧导航栏折叠与主窗体布局医院挂号系统的主窗体一般是左侧一排菜单按钮右侧放各个功能页。菜单折叠不需要重绘什么箭头图标用 Timer 改变 Panel 宽度就能实现平滑动画。private bool _collapsed false; private void TimerSlide_Tick(object sender, EventArgs e) { if (_collapsed) { sideBar.Width 5; if (sideBar.Width 200) { _collapsed false; timerSlide.Stop(); } } else { sideBar.Width - 5; if (sideBar.Width 48) { _collapsed true; timerSlide.Stop(); } } }逻辑说明每 10 毫秒把侧边栏宽度增减 5 像素直到到达目标宽度。_collapsed布尔值记录当前是否处于收起状态防止 Timer 重复叠加。宽度上限 200 是展开态下限 48 只显示图标这个数值可以根据按钮文字长度调整。切换功能页的常见做法是清空内容区 Panel 的 Controls再把子窗体的TopLevelfalse后加进去。子窗体要设置FormBorderStyleNone、DockFill否则会带着标题栏和边框嵌进主窗体看起来像两个窗口叠在一起。4.2 DataGridView 手动绑定列列顺序不再乱跑DataGridView 的默认行为是自动生成列SQL 查出什么字段就显示什么字段列顺序跟着 SELECT 顺序走。这在写临时查询时方便做正式界面就是灾难——你明明设计好了患者姓名、科室、金额的排列数据源一换列就乱了。解决办法是把自动生成列关掉手动声明每一列dgvRegister.AutoGenerateColumns false; DataGridViewTextBoxColumn colName new DataGridViewTextBoxColumn(); colName.HeaderText 患者姓名; colName.DataPropertyName PatientName; colName.Width 120; DataGridViewTextBoxColumn colAmount new DataGridViewTextBoxColumn(); colAmount.HeaderText 挂号金额; colAmount.DataPropertyName Amount; colAmount.DefaultCellStyle.Format F2; colAmount.Width 90; dgvRegister.Columns.Add(colName); dgvRegister.Columns.Add(colAmount); var dt SqlHelper.ExecuteDataTable( SELECT RegisterId, PatientName, Amount, Status FROM Register); dgvRegister.DataSource dt;逻辑说明HeaderText是界面显示的列名DataPropertyName是数据源里的字段名两者可以不一样。DefaultCellStyle.Format F2让金额显示两位小数这个格式化对 DECIMAL 类型尤其重要否则可能出现 15.5 而不是 15.50。参数说明AutoGenerateColumns 必须在 DataSource 之前设置绑定后再改无效。列宽按业务需求设姓名 120、金额 90 比较合适电话列建议 110。如果查询结果里有 Status 这类枚举数字可以在 CellFormatting 事件里把它映射为文字比直接显示 0/1/2 对使用者友好得多。4.3 界面美化的三板斧背景色、字体与状态栏进度条WinForms 默认控件外观确实朴素但不用引入第三方皮肤。我一般只做三件事统一窗体背景色、统一 DataGridView 行样式、状态栏加进度条反馈。// 整体风格浅蓝白背景深色文字 this.BackColor Color.FromArgb(244, 247, 252); dataGridView1.BackgroundColor Color.White; dataGridView1.BorderStyle BorderStyle.None; dataGridView1.AlternatingRowsDefaultCellStyle.BackColor Color.FromArgb(238, 242, 249); dataGridView1.RowTemplate.Height 32; dataGridView1.SelectionMode DataGridViewSelectionMode.FullRowSelect; dataGridView1.ColumnHeadersDefaultCellStyle.Font new Font(Microsoft YaHei UI, 9F, FontStyle.Bold); // 保存按钮 btnSave.FlatStyle FlatStyle.Flat; btnSave.BackColor Color.FromArgb(52, 152, 219); btnSave.ForeColor Color.White;逻辑说明AlternatingRowsDefaultCellStyle让偶数行和奇数行颜色交替可读性会好很多。RowTemplate.Height 32避免默认行高显得拥挤。FlatStyle.Flat加自绘背景色是 WinForms 按钮美化的最常用手段不需要额外画图。状态栏进度条配合 BackgroundWorker 使用。在导出 Excel、生成统计报表这类耗时操作里把实际循环放到 DoWork 里用ReportProgress更新进度条界面不会卡死backgroundWorker1.WorkerReportsProgress true; backgroundWorker1.RunWorkerAsync(); private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { for (int i 0; i 100; i) { Thread.Sleep(30); backgroundWorker1.ReportProgress(i 1); } } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { toolStripProgressBar1.Value e.ProgressPercentage; }参数说明WorkerReportsProgresstrue是前提不设这个属性 ProgressChanged 不触发。Thread.Sleep(30)只是模拟耗时真实场景替换为逐行写 Excel 的代码。ToolStrip 的进度条和状态栏是一体的放在窗体的 StatusStrip 控件里比单独放一个 ProgressBar 更像专业软件的布局。5. 常见问题与避坑记录环境、编码与业务逻辑的五条血泪经验这一章是拆了几十个类似项目后攒下来的真实翻车点。每一条都是先看到现象再追原因最后给解法改这套源码时大概率会撞上。5.1 环境与配置类坑连接串、中文编码、32 位驱动第一条换电脑连不上数据库。现象源码在一台机器上跑得好好的复制到另一台电脑双击 exe 或 F5 直接报“Cannot open database”“Login failed for user”连接串问题是这类项目最常见的黑匣子。原因App.config 里写死了Data SourcePC01\SQLEXPRESS或 sa 的明文密码目标机器没有同名实例。解决把连接串改成集成验证加本地默认实例connectionStrings add namehospital connectionStringData Source.;Initial CatalogHospitalDB;Integrated SecurityTrue / /connectionStrings说明.表示本机默认 SQL Server 实例不需要知道机器名。Integrated SecurityTrue使用 Windows 账号登录省去配 SQL Server 账号的麻烦。确认数据库文件已经附加到实例里Initial Catalog要和库名完全一致大小写不敏感但要拼写正确。第二条界面查询中文变成问号或乱码。现象医生姓名、患者姓名在 DataGridView 里显示成???英文和数字正常。原因建表脚本里用了varchar(20)而不是nvarchar(20)SQL Server 的非 Unicode 类型遇到中文按当前代码页转换会丢字。解决把相关字段全部改成 nvarcharALTER TABLE Doctor ALTER COLUMN DoctorName NVARCHAR(20) NOT NULL; ALTER TABLE Register ALTER COLUMN PatientName NVARCHAR(20) NOT NULL;顺便把所有剩余文本字段排查一遍姓名、科室名、诊断描述都用 nvarchar这个习惯能省掉后续一整套报表乱码问题。第三条打包发给客户对方机器报“未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0”或 32/64 位驱动问题。现象用 Access 作为数据库的系统最常见开发机正常部署机报错。原因Access 数据库引擎分 32 位与 64 位两个版本VS 默认“Any CPU”在 32 位和 64 位进程间摇摆不定客户机器没装对应驱动。解决项目属性里把平台目标固定为 x86并在部署机安装 AccessDatabaseEngine.exe 32 位版本如果是 SQL Server 版这套源码基本不会触发这条但很多发行版后续会改成 Access先记着这个坑的位置。5.2 业务逻辑类坑DataGridView 列错乱、重复退号第四条DataGridView 列顺序和表头对不上多出一堆没见过的列。现象绑定数据源后设计器里排好的患者姓名列不见了往前挤了一堆 ScheduleId、DeptId 这样的 ID 列。原因AutoGenerateColumns默认为 true查询结果有几个字段就自动生成几列手动加的设计列排到最前面或直接被新列挤到后面。解决设计器里把 AutoGenerateColumns 设为 false再手动添加需要的列并设置 DataPropertyName。排查时打开设计器的 Columns 集合确认列的集合数量和你意图一致DataGridView 一多转储就很容易找到问题。第五条退号按钮点两次号源越退越多。现象患者退了一次号过会儿窗口又退一次排班表里 UsedSlots 不降反升或变负数。原因退号方法没有判断挂号记录当前状态先 UPDATE Register 再恢复号源第二次操作照样执行。解决把状态判断写进 UPDATE 的条件里受影响行数为 0 直接返回并配合事务确保恢复号源失败时整个退号不生效。这是我接手时第一个动手改的位置。3.2 里给过完整写法这里再强调没有AND Status 0的退号逻辑在一个三班倒的挂号窗口前撑不过一天。6. 验证与发布技巧SQL 回归单和单文件部署源码改完不代表能交付我每次发布前都会做两件事用一段事务 SQL 验证业务链再用工具把依赖打进单文件。这两步做完现场出问题的概率会压到很低。6.1 用一段事务 SQL 验证整个业务链改完核心逻辑后我会在 SSMS 里跑一段手工回归脚本把建科室、建医生、建排班、挂号、退号全链路走一遍。脚本用事务包裹最后 ROLLBACK不污染正式库BEGIN TRAN; INSERT INTO Department (DeptName) VALUES (N测试内科); DECLARE deptId INT SCOPE_IDENTITY(); INSERT INTO Doctor (DeptId, DoctorName, Title) VALUES (deptId, N张三, N主治医师); DECLARE docId INT SCOPE_IDENTITY(); INSERT INTO Schedule (DoctorId, WorkDate, ShiftTime, TotalSlots, UsedSlots) VALUES (docId, 2025-06-20, 上午, 20, 0); DECLARE scheduleId INT SCOPE_IDENTITY(); INSERT INTO Register (ScheduleId, PatientName, PatientPhone, Amount, Status) VALUES (scheduleId, N李四, 13800000000, 15.00, 0); UPDATE Schedule SET UsedSlots UsedSlots 1 WHERE ScheduleId scheduleId; -- 退号 UPDATE Register SET Status 2 WHERE ScheduleId scheduleId AND Status 0; UPDATE Schedule SET UsedSlots UsedSlots - 1 WHERE ScheduleId scheduleId; SELECT * FROM Schedule WHERE ScheduleId scheduleId; ROLLBACK;手工执行后看结果集排班 WorkDate 是否按预期写入UsedSlots 是否回到 0Register 的 Status 是否从 0 走到 2。任何一步数字对不上说明对应方法写错先查这一层再查界面调用。脚本里的日期和金额是测试值跑完 ROLLBACK 不会留下测试数据。我一般把这段存成一个regression.sql文件放在源码包里每次迭代都重跑一遍。6.2 Release 发布后用 Costura.Fody 合并 dllWinForms 项目发布后 bin 目录里一堆 dll拷给客户时少一个就起不来。常见做法是用 Costura.Fody 把所有托管程序集合并进主 exe交付时只有一个文件干净利落。在 NuGet 包管理器里安装两个包然后重新编译Install-Package Costura.Fody Install-Package Fody装完不需要改代码直接 Rebuildbin\Release 下会生成一个体积变大的主 exe其余托管 dll 被打进这个文件里。注意 System.Data 这类框架自带的组件不会被合并也不影响单机运行。如果项目引用了非托管驱动比如 Access 的 ACE 引擎、读卡器的原生 dllCostura.Fody 管不了它们需要单独布置运行库。这也是为什么我给客户装 Access 版时永远先装数据库引擎再拷 exe。我第一次上线这个挂号系统时因为漏带了一个 dll在客户收费电脑前蹲了两个小时才补完环境。从那以后我每次发布强制走一遍四步验证清空 bin 目录Release 重新编译把 exe 拷到一台干净的虚拟机从头跑一遍挂号、退号、报表导出主流程。确认这三步都通过才敢约客户现场。这套习惯留给你改完这个项目之后希望你交付时也能一次通过。本文还有配套的精品资源点击获取
返回列表