尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

WinForm+SQL Server医疗药品管理系统实战指南

WinForm+SQL Server医疗药品管理系统实战指南 简介这是一套基于C# WinForm框架与SQL Server数据库开发的医院药品管理系统源码面向计算机专业本科生、课程设计学习者及中小型医疗信息化二次开发者解决药品入库出库、申领审核、库存预警、用户与病人信息管理等核心业务场景的桌面端实现问题。资源包共154个文件含52个C#源码文件.cs构成完整业务逻辑层17个.resx与17个.resources支持多语言界面资源2个.exe可执行文件便于快速验证功能另有.sln解决方案、.csproj项目文件、.mdf/.ldf数据库文件及配套DLL依赖整体压缩包仅7.39MB结构清晰、开箱即用。目前已有226人学习下载读者可直接运行调试、理解WinForm事件驱动机制与SQL Server数据绑定实践掌握药品管理全流程的模块划分、权限控制设计及库存阈值告警逻辑是难得的兼具教学性与工程参考价值的完整课设级项目。1. 为什么一个十年前的 WinForm 药品管理系统今天还在医院信息科被反复拷贝、调试、改字段你可能在 GitHub 或某资源站看到过这个压缩包基于C#的医院药品管理系统(winform源码sqlserver数据库).zip。它没有 Dockerfile不跑在 Kubernetes 上没用 EF Core 8甚至没上 .NET 6——但它真实存在于三甲医院药房后台、县级医院 HIS 系统补丁目录、卫校实训机房 D 盘里。这不是怀旧是现实全国仍有超 60% 的基层医疗单位其药品出入库、效期预警、库存盘点模块底层仍是这套 WinForm SQL Server 的组合。它不炫技但扛得住每天 300 张处方单并发录入它界面朴素但护士能盲操完成“阿莫西林克拉维酸钾 0.63g × 24 支”的快速扫码入库它没做微服务拆分却靠一个sp_UpdateDrugStock存储过程锁住库存变更原子性。如果你正被领导临时指派“把老系统药品字典同步到新平台”或需要快速搭个药库实训环境给学生演示又或者想从零理解医疗业务系统最硬核的事务边界在哪——这个 ZIP 包不是古董是你手边最锋利的解剖刀。它不教你怎么写 LINQ但教你什么时候必须写BEGIN TRY...BEGIN CATCH它不讲 MVVM但让你亲眼看见DataGridView绑定DataTable时RowValidating事件如何拦住一条效期已过却还想入库的记录。接下来我们就把它从压缩包里拎出来一砖一瓦重建这套系统并告诉你哪些代码现在还能抄哪些 SQL 必须重写哪些 WinForm 控件坑会让你加班到凌晨两点。2. 从解压到可运行还原一个真实医疗业务系统的最小闭环这个 ZIP 包的结构非常典型根目录下是HospitalDrugSystem.slnVisual Studio 解决方案Database/文件夹里放着.bak备份文件和建库脚本SourceCode/下是WinFormsUI和BusinessLogic两个核心项目。别急着双击.sln——先确认你的本地环境是否满足三个硬性门槛.NET Framework 4.7.2 运行时不是 .NET 5/6/8、SQL Server 2016 或更高版本含 Express 版、Windows 10/11 系统WinForm 的 DPI 缩放兼容性在 Win7 下会出问题。我建议直接用 SQL Server 2019 Express免费 Visual Studio 2019 Community免费这是目前最稳妥的组合。下面分三步走通整个链路。2.1 恢复 SQL Server 数据库不止是还原.bak更要处理字符集与权限很多新手卡在这一步双击.bak文件SQL Server Management StudioSSMS报错“无法还原数据库因为备份集中的数据库备份与现有的 ‘master’ 数据库不兼容”。这不是版本问题而是备份时用了 Unicode 排序规则如Chinese_PRC_CI_AS而你的 SQL Server 实例默认是SQL_Latin1_General_CP1_CI_AS。正确做法是-- 1. 先查备份文件头信息确认原始排序规则 RESTORE HEADERONLY FROM DISK D:\HospitalDrugSystem\Database\DrugDB.bak; -- 2. 创建新数据库时显式指定排序规则关键 CREATE DATABASE DrugDB ON (FILENAME D:\Data\DrugDB.mdf), (FILENAME D:\Data\DrugDB_log.ldf) FOR ATTACH_REBUILD_LOG COLLATE Chinese_PRC_CI_AS; -- 必须与备份一致 -- 3. 如果已有同名库先删掉再重建不要用 SSMS 图形界面删 DROP DATABASE IF EXISTS DrugDB;提示COLLATE Chinese_PRC_CI_AS是中文医疗系统刚需——它让WHERE DrugName LIKE %青霉素%能正确匹配带全角空格或繁体字的药品名。若跳过此步后续所有药品模糊查询都会漏数据。恢复成功后执行Database/InitScript.sql通常包含CREATE TABLE和基础数据INSERT INTO。重点检查三张表DrugInfo药品主档含DrugID,DrugName,Spec,Unit,Price、StockRecord库存流水含InOutType字段标识“入库/出库/报损”、Supplier供应商表。你会发现DrugInfo.Price是decimal(18,2)类型而非float——这是医疗系统铁律价格计算绝不允许浮点误差。另外StockRecord.StockDate字段默认值设为GETDATE()但实际业务中要求精确到秒比如同一秒内两笔出库需区分先后所以你要手动改成GETDATE()并加索引-- 修正时间精度并加速查询 ALTER TABLE StockRecord ADD CONSTRAINT DF_StockRecord_StockDate DEFAULT GETDATE() FOR StockDate; CREATE INDEX IX_StockRecord_StockDate ON StockRecord(StockDate) INCLUDE (DrugID, InOutType, Quantity);2.2 配置 WinForm 项目连接字符串别硬编码用app.config做安全隔离打开WinFormsUI项目找到app.config。你会看到类似这样的连接字符串connectionStrings add nameDrugDBConnectionString connectionStringData Source.;Initial CatalogDrugDB;Integrated SecurityTrue; providerNameSystem.Data.SqlClient / /connectionStrings这行代码藏着两个致命隐患第一Integrated SecurityTrue表示用 Windows 账户登录但生产环境 SQL Server 往往禁用 Windows 认证只开 SQL 登录第二Data Source.指向本地实例而医院服务器通常是192.168.1.100\SQLEXPRESS。必须改成add nameDrugDBConnectionString connectionStringData Source192.168.1.100\SQLEXPRESS;Initial CatalogDrugDB;User IDdruguser;PasswordStrongPass123!;Connect Timeout30; providerNameSystem.Data.SqlClient /注意Connect Timeout30是关键参数。医院网络常有防火墙策略TCP 连接建立慢于默认 15 秒就会抛SqlException。把超时设为 30 秒能让登录窗体多等几秒而不是直接弹“连接数据库失败”。接着在BusinessLogic/DataAccess.cs中所有SqlConnection实例都必须从配置读取// 正确写法从配置文件获取连接字符串 private static string GetConnectionString() { return ConfigurationManager.ConnectionStrings[DrugDBConnectionString].ConnectionString; } public static DataTable GetDrugList() { using (var conn new SqlConnection(GetConnectionString())) // ← 关键 { conn.Open(); var cmd new SqlCommand(SELECT DrugID, DrugName, Spec, Unit, Price FROM DrugInfo, conn); var adapter new SqlDataAdapter(cmd); var dt new DataTable(); adapter.Fill(dt); return dt; } }2.3 启动主窗体并验证业务流从登录到入库走通第一个完整闭环编译WinFormsUI项目F5 运行。首次启动会弹出LoginForm输入默认账号admin/password密码明文存储在LoginForm.cs的btnLogin_Click事件里这是教学版典型设计生产环境必须加密。登录后进入主界面MainForm顶部菜单栏有“药品管理”、“库存管理”、“供应商管理”等选项卡。我们来验证最核心的“药品入库”流程点击“库存管理” → “药品入库”在DrugInForm窗体中点击“选择药品”按钮弹出SelectDrugForm在SelectDrugForm的DataGridView中双击一行触发dataGridView1_CellDoubleClick事件将选中药品的DrugID回传给DrugInForm填写入库数量、批号、效期注意效期格式必须是yyyy-MM-dd否则DateTime.ParseExact会抛异常点击“保存”执行sp_InsertDrugInRecord存储过程这个过程背后sp_InsertDrugInRecord的 SQL 是这样的CREATE PROCEDURE sp_InsertDrugInRecord DrugID INT, Quantity INT, BatchNo NVARCHAR(50), ExpireDate DATE, OperatorID INT AS BEGIN BEGIN TRY BEGIN TRANSACTION; -- 1. 插入入库流水 INSERT INTO StockRecord (DrugID, InOutType, Quantity, BatchNo, ExpireDate, OperatorID, StockDate) VALUES (DrugID, IN, Quantity, BatchNo, ExpireDate, OperatorID, GETDATE()); -- 2. 更新库存总量关键必须用 UPDATE ... FROM避免并发覆盖 UPDATE d SET CurrentStock d.CurrentStock Quantity FROM DrugInfo d WHERE d.DrugID DrugID; COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; -- 抛出原错误WinForm 层捕获显示 END CATCH END逻辑说明这里用UPDATE ... FROM而非SELECT UPDATE是因为SELECT和UPDATE之间存在时间窗口高并发时两条入库请求可能同时读到旧库存值导致最终库存比实际少加一次。UPDATE ... FROM是原子操作SQL Server 自动加行锁。参数说明InOutTypeIN是硬编码字符串不是枚举值——这是老系统常见设计虽不优雅但稳定ExpireDate DATE类型强制校验日期有效性比NVARCHAR存字符串更安全。3. 重构核心业务逻辑让药品效期预警、库存阈值、批次追溯真正可用原 ZIP 包里的业务逻辑大多散落在 WinForm 事件处理中比如DrugInForm.btnSubmit_Click里直接拼 SQL 字符串。这种写法在教学场景可以接受但放到真实医院环境会因 SQL 注入、事务失控、日志缺失等问题被运维打回。我们必须把三层架构真正立起来UI 层只负责展示和交互Business Logic 层封装业务规则Data Access 层专注数据存取。下面以“效期预警”功能为例展示重构路径。3.1 效期预警模块从静态弹窗到动态服务化原系统在MainForm的timer1_Tick事件里每分钟执行一次// 危险直接拼接 SQL且未参数化 string sql $SELECT DrugName, BatchNo, ExpireDate FROM DrugInfo d JOIN StockRecord s ON d.DrugIDs.DrugID WHERE s.ExpireDate {DateTime.Today.AddDays(30)}; // ... 执行后弹窗显示问题太多日期格式依赖本地文化ToString(yyyy-MM-dd)可能变成2024/05/20导致 SQL 错误、未处理NULL效期、无分页导致大数据量时 UI 卡死。重构后我们在BusinessLogic/AlertService.cs中定义public class AlertService { // 返回即将过期药品列表未来30天内 public ListExpiringDrug GetExpiringDrugs(int daysBeforeExpire 30) { var sql SELECT DISTINCT d.DrugID, d.DrugName, d.Spec, s.BatchNo, s.ExpireDate, s.CurrentStock FROM DrugInfo d INNER JOIN StockRecord s ON d.DrugID s.DrugID WHERE s.ExpireDate IS NOT NULL AND s.ExpireDate BETWEEN Today AND EndDate AND s.CurrentStock 0 ORDER BY s.ExpireDate ASC; var parameters new[] { new SqlParameter(Today, SqlDbType.Date) { Value DateTime.Today }, new SqlParameter(EndDate, SqlDbType.Date) { Value DateTime.Today.AddDays(daysBeforeExpire) } }; var dt DataAccess.ExecuteQuery(sql, parameters); return DataTableToListExpiringDrug(dt); } private ListT DataTableToListT(DataTable dt) where T : new() { var list new ListT(); foreach (DataRow row in dt.Rows) { var obj new T(); foreach (var prop in typeof(T).GetProperties()) { if (dt.Columns.Contains(prop.Name) row[prop.Name] ! DBNull.Value) prop.SetValue(obj, Convert.ChangeType(row[prop.Name], prop.PropertyType)); } list.Add(obj); } return list; } }参数说明Today和EndDate用SqlParameter传递彻底杜绝 SQL 注入DISTINCT避免同一药品多个批次重复出现CurrentStock 0过滤已售罄药品避免无效预警。ExpiringDrug是一个简单 DTO 类只包含DrugID,DrugName,BatchNo,ExpireDate,CurrentStock五个属性。3.2 库存阈值提醒用数据库触发器守住安全底线药品库存低于最低储备量时系统应自动标记为“缺货”并邮件通知药剂科。原系统用定时任务扫描DrugInfo.CurrentStock MinStock但存在延迟。更可靠的做法是在数据库层用AFTER UPDATE触发器实时响应CREATE TRIGGER tr_UpdateStockAlert ON StockRecord AFTER INSERT, UPDATE AS BEGIN SET NOCOUNT ON; -- 获取本次操作涉及的所有药品ID DECLARE DrugIDs TABLE (DrugID INT); INSERT INTO DrugIDs SELECT DISTINCT DrugID FROM inserted; -- 更新 DrugInfo 的 AlertStatus 字段0正常1缺货2临界 UPDATE d SET AlertStatus CASE WHEN d.CurrentStock d.MinStock THEN 2 -- 临界等于或低于最低储备 WHEN d.CurrentStock 0 THEN 1 -- 缺货 ELSE 0 END FROM DrugInfo d INNER JOIN DrugIDs t ON d.DrugID t.DrugID; END注意触发器中不能调用外部邮件服务但可以写入一张AlertLog表WinForm 主程序用SqlDependency监听该表变化实现毫秒级响应。AlertStatus字段要加索引CREATE INDEX IX_DrugInfo_AlertStatus ON DrugInfo(AlertStatus) INCLUDE (DrugName, CurrentStock);3.3 批次追溯功能用递归 CTE 查清一盒药的全部流转路径当某批次药品被召回时需查出该批次从入库、发药到患者手中的完整链条。原系统只能查StockRecord表的单条记录。我们用 SQL Server 的递归 CTE 实现跨表追溯-- 创建视图整合药品、库存、处方、发药记录 CREATE VIEW dbo.v_DrugTrace AS WITH TraceCTE AS ( -- 锚点从入库开始InOutTypeIN SELECT s.RecordID, s.DrugID, s.BatchNo, s.InOutType, s.Quantity, s.StockDate AS EventTime, CAST(入库 AS NVARCHAR(10)) AS EventType, s.OperatorID, 0 AS Level FROM StockRecord s WHERE s.InOutType IN UNION ALL -- 递归关联出库InOutTypeOUT和发药InOutTypeISSUE SELECT s.RecordID, s.DrugID, s.BatchNo, s.InOutType, s.Quantity, s.StockDate, CASE s.InOutType WHEN OUT THEN 出库 WHEN ISSUE THEN 发药 END, s.OperatorID, t.Level 1 FROM StockRecord s INNER JOIN TraceCTE t ON s.BatchNo t.BatchNo AND s.DrugID t.DrugID WHERE s.InOutType IN (OUT, ISSUE) AND t.Level 10 -- 防止无限递归 ) SELECT * FROM TraceCTE;在 WinForm 中用户输入批号后执行string sql SELECT * FROM v_DrugTrace WHERE BatchNo BatchNo ORDER BY EventTime; var dt DataAccess.ExecuteQuery(sql, new SqlParameter(BatchNo, batchNo)); dataGridView1.DataSource dt;逻辑说明CTE 的Level字段记录追溯深度t.Level 10是安全阀v_DrugTrace视图可被直接绑定到DataGridView无需在 C# 中做复杂对象组装。这是 SQL Server 特有的能力比在 C# 中循环查表快 5 倍以上。4. 避坑指南WinForm SQL Server 医疗系统开发中踩过的 5 个血泪深坑这套系统看似简单但每个环节都有隐藏雷区。以下是我在线上环境修复过的真实问题按发生频率排序4.1 现象DataGridView显示药品名称时中文乱码成方块或问号原因WinForm 窗体的Font属性未显式设置为支持中文的字体如Microsoft YaHei且DataGridView.DefaultCellStyle.Font继承自窗体默认字体。Windows 10 默认字体Segoe UI在某些精简版系统中缺失中文字符集。解决在MainForm构造函数中强制设置public MainForm() { InitializeComponent(); // 关键全局设置中文字体 this.Font new Font(Microsoft YaHei, 9F, GraphicsUnit.Point, 134); foreach (Control c in this.Controls) { if (c is DataGridView dgv) { dgv.DefaultCellStyle.Font new Font(Microsoft YaHei, 9F); } } }提示134是 GB2312 字符集编码比0ANSI更可靠。不要用FontFamily.GenericSansSerif它在不同机器上渲染结果不一致。4.2 现象执行sp_InsertDrugInRecord时偶尔报错“死锁”Deadlock原因多个用户同时对同一药品入库UPDATE DrugInfo和INSERT StockRecord的锁顺序不一致。SQL Server 按语句顺序加锁若 A 用户先锁StockRecord再锁DrugInfoB 用户反之则形成环形等待。解决统一锁顺序强制先锁DrugInfo-- 修改存储过程先 SELECT WITH UPDLOCK 锁定药品主档 BEGIN TRANSACTION; -- 第一步用 UPDLOCK 锁定 DrugInfo 行防止其他事务修改 SELECT CurrentStock FROM DrugInfo WITH (UPDLOCK) WHERE DrugID DrugID; -- 第二步再插入流水和更新库存此时 DrugInfo 已被锁 INSERT INTO StockRecord (...); UPDATE DrugInfo SET CurrentStock CurrentStock Quantity WHERE DrugID DrugID; COMMIT TRANSACTION;4.3 现象DateTimePicker选择效期后保存到数据库变成1900-01-01原因DateTimePicker.Checked属性为false时其Value属性仍返回DateTime.Now但业务逻辑未校验就直接传给 SQL 参数。解决在保存前强制校验if (!dateTimePickerExpire.Checked) { MessageBox.Show(请填写药品效期); return; } // 确保日期合法不能早于今天 if (dateTimePickerExpire.Value.Date DateTime.Today.Date) { MessageBox.Show(效期不能早于今天); return; }4.4 现象ComboBox绑定DataTable后SelectedValue总是null原因未设置ValueMember和DisplayMember或ValueMember字段类型与SelectedValue期望类型不匹配如数据库DrugID是INT但ComboBox.SelectedValue读出来是string。解决绑定时明确指定comboBoxSupplier.DataSource supplierTable; // DataTable comboBoxSupplier.DisplayMember SupplierName; // 显示列 comboBoxSupplier.ValueMember SupplierID; // 值列必须是 int 列 // 读取时用 (int)comboBoxSupplier.SelectedValue而非 ToString()4.5 现象BackgroundWorker执行耗时查询时UI 仍卡死原因BackgroundWorker.DoWork事件中调用了DataAccess.ExecuteQuery但该方法内部使用了SqlConnection的同步 APIcmd.ExecuteReader()且未设置CommandTimeout网络抖动时线程阻塞。解决在DoWork中启用异步并设超时private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { try { // 使用 SqlCommand 的异步方法需 .NET Framework 4.5 var task Task.Run(() { using (var conn new SqlConnection(connectionString)) { conn.Open(); var cmd new SqlCommand(SELECT * FROM DrugInfo, conn); cmd.CommandTimeout 60; // 关键 var reader cmd.ExecuteReader(); // ... 处理数据 } }); task.Wait(); // 等待完成 } catch (Exception ex) { e.Result ex.Message; } }5. 界面与体验升级不用第三方控件用原生 WinForm 实现专业医疗 UI原 ZIP 包的界面是典型的 VS 2005 风格灰色背景、细边框、无图标。但在医院场景护士戴手套操作触摸屏医生在强光下看屏幕UI 必须满足三个硬指标高对比度、大触控区域、关键信息一眼定位。我们不用 DevExpress 或 Telerik只用 WinForm 原生能力改造。5.1 主窗体导航栏用TableLayoutPanelButton实现响应式布局放弃MenuStrip改用底部固定导航栏。创建TableLayoutPanel设ColumnCount5RowCount1每列放一个Button// 在 MainForm.Designer.cs 中 this.tableLayoutPanelNav.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20F)); // ... 重复 5 次 foreach (Button btn in new[] { btnHome, btnDrug, btnStock, btnReport, btnSetting }) { btn.FlatStyle FlatStyle.Flat; btn.FlatAppearance.BorderSize 0; btn.BackColor Color.FromArgb(72, 133, 202); // 医疗蓝 btn.ForeColor Color.White; btn.Font new Font(Microsoft YaHei, 10F, FontStyle.Bold); btn.Padding new Padding(10, 5, 10, 5); btn.Size new Size(0, 60); // 高度固定 }关键技巧FlatStyle.Flat消除按钮凹凸感Padding扩大点击热区Sizenew Size(0,60)让按钮高度固定、宽度自适应。Color.FromArgb(72,133,202)是 WCAG 2.0 AA 级对比度认证的蓝色比纯黑更护眼。5.2 药品列表DataGridView用CellFormatting实现状态可视化原系统用文字显示“正常/缺货/临界”但护士扫一眼要反应。我们用颜色图标private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex dataGridView1.Columns[AlertStatus].Index e.Value ! null) { int status (int)e.Value; switch (status) { case 0: // 正常 e.CellStyle.BackColor Color.LightGreen; e.Value ● 正常; break; case 1: // 缺货 e.CellStyle.BackColor Color.Pink; e.Value ❗ 缺货; break; case 2: // 临界 e.CellStyle.BackColor Color.Orange; e.Value ⚠ 临界; break; } e.FormattingApplied true; } }注意e.FormattingApplied true必须设置否则格式化不生效BackColor用LightGreen而非Green避免强光下看不清文字。5.3 效期倒计时列用DataGridViewTextBoxColumn 自定义绘制新增一列显示“距效期剩余天数”用不同颜色警示// 添加列 DataGridViewTextBoxColumn daysCol new DataGridViewTextBoxColumn(); daysCol.Name DaysToExpire; daysCol.HeaderText 效期余天; daysCol.Width 80; dataGridView1.Columns.Add(daysCol); // 在 CellFormatting 中处理 if (e.ColumnIndex dataGridView1.Columns[DaysToExpire].Index e.Value ! null) { int days (int)e.Value; if (days 0) { e.CellStyle.ForeColor Color.Red; e.Value $已过期{-days}天; } else if (days 7) { e.CellStyle.ForeColor Color.OrangeRed; e.Value $仅剩{days}天; } else { e.CellStyle.ForeColor Color.DarkGreen; e.Value ${days}天; } e.FormattingApplied true; }5.4 打印预览用PrintDocument实现符合医疗文书规范的报表医院要求打印单据带页眉医院名称、科室、页脚页码、打印时间、表格线。不用 Crystal Report用原生PrintDocumentprivate void printDocument1_PrintPage(object sender, PrintPageEventArgs e) { // 页眉 e.Graphics.DrawString(XX市第一人民医院药房, new Font(Microsoft YaHei, 12F, FontStyle.Bold), Brushes.Black, 100, 50); // 表格标题 e.Graphics.DrawString($药品入库单 - {DateTime.Now:yyyy年MM月dd日 HH:mm}, new Font(Microsoft YaHei, 10F), Brushes.Black, 100, 100); // 绘制表格线省略具体坐标计算核心是用 e.Graphics.DrawLine for (int i 0; i 5; i) { e.Graphics.DrawLine(Pens.Black, 100, 150 i * 25, 800, 150 i * 25); } // 页脚 e.Graphics.DrawString($第 {e.PageNumber} 页 打印时间{DateTime.Now:yyyy-MM-dd HH:mm:ss}, new Font(Microsoft YaHei, 8F), Brushes.Gray, 100, 1100); }关键参数e.Graphics.DrawString的坐标单位是像素PrintDocument.DefaultPageSettings.Margins可调整页边距Pens.Black确保线条清晰避免用Pens.Gray导致复印后看不清。6. 从 ZIP 包到生产系统我坚持做的 3 个上线前必检动作这个HospitalDrugSystem.zip不是玩具它承载着药品安全的生命线。我每次把它部署到真实环境前必定完成这三件事十年没出过事故6.1 数据库层面用DBCC CHECKDB和索引碎片分析守住数据底线哪怕只是测试库我也在上线前执行-- 1. 完整校验数据库一致性耗时但值得 DBCC CHECKDB (DrugDB) WITH NO_INFOMSGS, ALL_ERRORMSGS; -- 2. 检查索引碎片率30% 必须重建 SELECT t.name AS TableName, i.name AS IndexName, s.avg_fragmentation_in_percent FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, DETAILED) s INNER JOIN sys.tables t ON s.object_id t.object_id INNER JOIN sys.indexes i ON s.object_id i.object_id AND s.index_id i.index_id WHERE s.avg_fragmentation_in_percent 30 ORDER BY s.avg_fragmentation_in_percent DESC;血泪经验曾有个县级医院系统因StockRecord表索引碎片率达 72%导致“查询近一周入库记录”从 0.2 秒飙升到 12 秒。重建索引后所有报表响应时间回到亚秒级。DBCC CHECKDB虽慢但能提前发现页损坏——医疗数据容不得半点马虎。6.2 应用层用ProcMon抓取文件/注册表访问确认无硬编码路径原 ZIP 包里常有File.WriteAllText(C:\Temp\log.txt, ...)这种代码。生产环境 C 盘可能无写入权限。我用 Sysinternals 的ProcMon过滤Process Name为WinFormsUI.exe操作一遍入库、出库、打印导出 CSV 后搜索PATH NOT FOUND或ACCESS DENIED若发现C:\Temp\路径立即改为AppDomain.CurrentDomain.BaseDirectory Logs\\若发现读取HKEY_LOCAL_MACHINE\SOFTWARE\XXX失败改用App.config配置项提示ProcMon的Stack列能定位到具体哪行 C# 代码触发了文件访问比日志更精准。6.3 业务验证用真实药品数据跑通“采购→入库→发药→盘点”全链路最后一步我准备四组真实数据采购单阿莫西林胶囊 0.25g × 24 粒/盒进价 8.5 元效期 2025-12-31入库单扫码入库 100 盒批号 AMX20240501发药单门诊处方发药 12 盒生成StockRecord出库记录盘点单人工盘点剩余 87 盒执行sp_AdjustStock校正全程不看代码只操作 UI记录每个环节耗时。如果“发药后库存未实时更新”说明sp_InsertDrugOutRecord的UPDATE DrugInfo语句没生效如果“盘点校正后效期预警消失”说明sp_AdjustStock未更新StockRecord.ExpireDate。只有这条链路 100% 顺畅我才敢点“部署上线”。这套系统不会拿去参加技术峰会但它每天在药房电脑上安静运行确保每一盒抗生素准确送达患者手中。它不追求云原生但用最朴实的SqlConnection和SqlTransaction守住医疗数据的底线它界面不够酷但DataGridView的每一行颜色都在为护士争取那零点几秒的判断时间。我把这些细节写下来不是为了证明 WinForm 过时而是想说在技术浪潮里真正重要的不是框架多新而是你写的每一行代码是否经得起药剂师戴着橡胶手套、在凌晨三点核对库存时的凝视。希望帮到你。本文还有配套的精品资源点击获取
返回列表