简介这是一套基于WinForm与C#语言、搭配SQL Server数据库开发的住院管理系统完整源码包面向桌面管理系统的学习者、课程设计学生以及需要快速搭建医疗信息管理原型的开发人员。系统采用C/S模式集成权限、科室、用户、病人、住院、治疗、费用及金额设置等核心模块覆盖医院住院业务的主要数据管理流程内置管理员账号admin便于直接部署体验。压缩包共112个文件大小约2.59MB以.cs源代码、.Designer.cs界面设计文件、.resx与.resources资源文件为主另含可执行的.exe程序、.dll类库、.config配置、数据库文件winyiyuan.mdf及配套日志文件等结构完整可直接用VS2010与SQL Server 2008打开运行。目前已有606人学习下载适合希望在真实桌面项目中熟悉WinForm分层界面、SQL Server数据库交互及增删改查实现的开发者参考借鉴对理解中小型管理系统从设计到落地的完整流程较有帮助。1. 为什么住院管理系统还在用 WinForms先认清它适合什么场景提到 WinForms很多开发者的第一反应是“老古董”。但如果你去过三甲医院的信息科或者接触过医疗软件公司的交付现场就会发现一个反直觉的事实病区护士站里跑得最稳、护士最愿意用的往往还是 C/S 架构的 WinForms 客户端。住院管理系统这个标题里的 WinForms 不是历史包袱而是当前做院内桌面端最省成本的落地方案。这套系统要解决的核心问题是住院全流程的信息化入院登记、床位分配、医嘱录入与执行、药品消耗、费用核算、出院结算。它服务的人群很明确——护士站护士、病区医生、出入院结算员这些人一天八小时面对同一个屏幕要求的是响应快、键盘操作顺手、断电重连不丢数据。WinForms 在这个场景下的价值不是界面好看而是开发效率高、部署简单、离线容错容易做。这篇文章我会顺着一个可落地的完整方案讲清楚技术选型、数据库设计、关键业务代码、连续踩过的坑以及最后交付时值得注意的细节。2. 技术选型与项目结构WinForms、Dapper 和三层架构怎么配合2.1 为什么是 WinForms 而不是 WPF 或者 Web住院管理系统是典型的“单机为主、网络为辅”的业务。护士执行医嘱、录入体征时操作路径非常固定鼠标点击次数远少于键盘操作。WPF 的绑定和动画确实漂亮但它的学习曲线和 UI 线程陷阱会让项目周期明显拉长Web 端虽然部署方便但在病区网络抖动、无线信号不稳定的情况下刷新页面丢失已录入数据的体验是灾难性的。WinForms 最实在的优势是三点。第一控件模型简单DataGridView、ListView、ComboBox 都是现成的业务控件不需要额外封装。第二UI 线程模型直观BeginInvoke 就能处理异步刷新不会像 WPF 那样动不动就抛线程间操作异常。第三ClickOnce 或 Inno Setup 打包后在院内部署几乎零成本一台护士站电脑装一个客户端数据库放在服务器上断网时本地操作照常进行网络恢复后再同步。2.2 推荐的项目分层与 Dapper 依赖注入我一般会把这个系统拆成五个项目Model 实体层、DAL 数据访问层、BLL 业务逻辑层、UI 表现层、Common 公共工具层。DAL 统一用 Dapper 而不是 EF Core原因很直接住院管理系统的 SQL 大多是固定业务语句Dapper 的参数化查询和轻量映射刚好够用性能开销几乎为零而且遇到复杂统计 SQL 时可以直接写原生语句不用被 ORM 的表达式树限制住。依赖注入我用微软的 Microsoft.Extensions.DependencyInjection在 Program.cs 里统一注册。这里有个容易忽略的点WinForms 不是 Web 项目没有天然的请求作用域所以数据库连接对象应该注册成 Transient每次操作独立创建连接业务服务注册成 Scoped 或 Singleton 要看内部是否有状态。我一般把业务服务注册为 Singleton但要求服务内部不保存跨操作的可变状态避免多窗口并发访问时互相污染。// Program.cs 入口注册 private static ServiceProvider ConfigureServices() { var services new ServiceCollection(); // 注册数据库连接工厂便于切换数据库实例 services.AddSingletonIDbConnectionFactory, SqlConnectionFactory(); // 注册业务服务医嘱服务、床位服务、结算服务 services.AddScopedIOrderService, OrderService(); services.AddScopedIBedService, BedService(); services.AddScopedISettlementService, SettlementService(); // 注册主窗口和其他子窗口 services.AddTransientMainForm(); services.AddTransientPatientAdmissionForm(); services.AddTransientOrderExecuteForm(); return services.BuildServiceProvider(); } // 启动入口 [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var provider ConfigureServices(); var mainForm provider.GetRequiredServiceMainForm(); Application.Run(mainForm); }这里的关键逻辑是把“对象创建”和“业务使用”分离开。比如订单服务内部依赖床位服务构造注入后由容器统一管理生命周期不会出现窗口关闭后服务还持有旧连接的问题。IDbConnectionFactory 是自定义接口内部实现里读取配置文件里的连接串每次调用返回一个新的 SqlConnection这样 Dapper 的 QueryAsync 就能天然做到短连接。2.3 界面结构导航树、工作区和状态栏住院管理系统的界面布局我建议用左侧折叠菜单、右侧 Tab 工作区、底部状态栏的经典结构。左侧菜单用 TreeView 或自定义折叠面板菜单项包括“入院登记”“床位管理”“医嘱处理”“药品管理”“费用结算”“报表查询”。菜单折叠的箭头绘制是 WinForms 界面美化的常见细节——默认的 TreeView 箭头丑且难改很多人用自绘箭头替代我后面专门讲。状态栏一定要实时显示三样东西当前登录用户、当前选择的病区、数据库连接状态。热词里有“c# winform如何更新状态栏与进度条”这个在住院管理系统里非常实用护士批量执行医嘱时用 BackgroundWorker 跑循环每处理一条就更新进度条百分比避免界面假死。这里有个血泪经验更新进度条时要用 progressBar.BeginInvoke 而不是直接赋值否则跨线程操作会随机翻车。3. 数据库设计与核心业务代码从入院到结算的完整链路3.1 住院管理系统的关键表结构先看数据库设计。住院管理系统的主线是“一次住院记录”从入院到出院的所有数据都围绕住院号inhosp_no串联。我常用的核心表有六张病区表、床位表、住院患者表、医嘱表、医嘱执行表、费用明细表。病区分内科外科儿科等床位表挂在病区下住院患者表保存当前在院状态医嘱表和执行表是典型的“主从表”。字段设计上有几个容易踩坑的地方。第一金额字段一律用 decimal(18,2) 而不是 float后者在累计费用时会出现 0.1 0.2 不等于 0.3 的玄学问题。第二时间字段建议用 datetime2精度足够且不受本地时区影响。第三所有业务表都加一个 del_flag 字段做软删除住院系统不允许物理删除任何记录万一误删你连后悔药都没有。表名核心字段说明bed_infobed_id, ward_id, bed_no, bed_statusbed_status 0 空闲 1 占用 2 消毒中patient_inhospinhosp_no, patient_name, bed_id, admission_time, discharge_time一次住院一条记录doctor_orderorder_id, inhosp_no, order_type, drug_id, dosage, frequency, create_time长期医嘱和临时医嘱共用order_executeexecute_id, order_id, execute_time, operator, status每执行一次生成一条我特别强调医嘱表的设计。医嘱类型要区分长期医嘱和临时医嘱长期医嘱按频率每天多次执行临时医嘱只执行一次。业务上一条长期医嘱会对应多条执行记录所以执行表的外键指向医嘱表主键。给医嘱表加一个 order_status 字段从“开立”到“审核”到“执行中”到“停止”护士只能在“审核通过”状态下执行这个状态机就是工作流设计器在 WinForms 里的简化版——核心不是流程引擎而是状态转移的约束。3.2 入院登记与床位分配事务是底线入院登记不仅仅是插入一条患者记录那么简单。它同时要做三件事写住院患者表、把床位状态从“空闲”改成“占用”、生成一条入院费用记录。这三步必须在一个事务里完成否则会出现患者住进了病区但床位还是空闲的数据不一致轻则统计出错重则两个患者被分到同一张床。Dapper 里做事务有个细节Execute 方法要传入同一个 SqlConnection 和 SqlTransaction 对象并且所有操作都用这个事务实例。很多新手翻车在事务内又开了一个新连接导致前两条 SQL 成功、后一条报错回滚却只回滚了后半部分。public async Task AdmitPatientAsync(PatientAdmissionDto dto) { const string sql BEGIN TRY BEGIN TRAN INSERT INTO patient_inhosp(inhosp_no, patient_name, bed_id, admission_time) VALUES(InhospNo, PatientName, BedId, GETDATE()); UPDATE bed_info SET bed_status 1 WHERE bed_id BedId AND bed_status 0; IF ROWCOUNT 0 BEGIN RAISERROR(N床位已被占用, 16, 1); END COMMIT TRAN END TRY BEGIN CATCH ROLLBACK TRAN THROW; END CATCH; using var conn _connectionFactory.CreateConnection(); await conn.ExecuteAsync(sql, new { InhospNo dto.InhospNo, PatientName dto.PatientName, BedId dto.BedId }); }这段 SQL 的关键点有两个。一是 UPDATE 语句带条件bed_status 0并检查影响行数这是防止并发条件下两张床分给同一个患者的兜底二是事务里用 RAISERROR 主动抛错让上层捕获后提示“床位已被占用”。你看这里没有用 SELECT 先查再判断因为那个做法在高并发护理班次交接时完全可能两个护士同时查到空闲、同时插入成功。这种用 UPDATE 影响行数做乐观锁的思路是医疗系统里比较标准的做法也是我在这个问题上踩过坑后换成的方案。3.3 医嘱执行与费用关联用状态机约束操作顺序医嘱执行是护士站使用频率最高的功能。护士选中一条长期医嘱点击“执行”系统要完成三件事写入 order_execute 执行记录、判断是否需要扣减药品库存、把药品费用写入费用明细表。医嘱状态机我定义为1 开立 → 2 审核 → 3 执行中 → 4 停止。护士只能对状态为 2 的记录执行执行时把状态改成 3。如果医生下达停止指令状态变为 4此后系统不再允许执行。这里有个业务细节要提前想清楚药品扣库存发生在医嘱执行时而不是开立时。这样做的好处是库存账和消耗账是同步的月底盘点对账时药品库存加上执行记录消耗量等于期初库存不会出现开立后患者出院但药已扣的麻烦。费用明细也是同理执行一次记一次费结算时直接汇总费用明细表就能得到总费用。费用明细表的金额记录我建议做“单向不可变”也就是说已经写入的费用记录不允许 UPDATE只能通过新增负数记录冲销。这个设计来自一次非常痛的经历护士执行了医嘱、费用已经记账但因为操作失误删除了执行记录费用明细也跟着没了月底医保对账怎么都对不上。改成冲销模式后每一笔账都有痕迹谁在什么时间冲销了什么记录一查就知道再也不用来回扯皮。public class OrderExecuteService : IOrderService { public async Task ExecuteOrderAsync(string orderId, string operatorName) { using var conn _connectionFactory.CreateConnection(); using var tran conn.BeginTransaction(); try { // 1. 检查医嘱状态是否为已审核 var order await conn.QueryFirstOrDefaultAsyncDoctorOrder( SELECT * FROM doctor_order WHERE order_id OrderId AND order_status 2, new { OrderId orderId }, tran); if (order null) throw new InvalidOperationException(医嘱状态不可执行); // 2. 插入执行记录 await conn.ExecuteAsync( INSERT INTO order_execute(order_id, execute_time, operator, status) VALUES(OrderId, GETDATE(), Operator, 1), new { OrderId orderId, Operator operatorName }, tran); // 3. 更新医嘱状态为执行中 await conn.ExecuteAsync( UPDATE doctor_order SET order_status 3 WHERE order_id OrderId, new { OrderId orderId }, tran); // 4. 扣减库存并写费用明细 await conn.ExecuteAsync( UPDATE drug_stock SET stock_qty stock_qty - 1 WHERE drug_id DrugId AND stock_qty 0, new { DrugId order.DrugId }, tran); await conn.ExecuteAsync( INSERT INTO cost_detail(inhosp_no, item_name, amount, create_time) VALUES(InhospNo, ItemName, Amount, GETDATE()), new { order.InhospNo, ItemName order.DrugName, Amount order.Price }, tran); tran.Commit(); } catch { tran.Rollback(); throw; } } }代码里要特别注意的是状态判断条件的写法SELECT ... WHERE order_status 2查不到就抛异常。为什么不在内存里先 SELECT 一次判断后再 UPDATE因为两次操作之间存在时间窗口另一个护士可能已经执行了这条医嘱。虽然住院系统的并发量远没有电商那么大但交接班的高峰时段两条执行请求同时打到同一医嘱上的概率并不低。单条 UPDATE 语句是原子操作但“检查-更新”是复合操作必须用事务包住或者把状态条件直接写进 UPDATE 的 WHERE 子句里。库存扣减那句stock_qty 0也是同样道理库存为 0 时 UPDATE 影响行数为 0但不会报错所以最好再加个影响行数判断影响行数为 0 时主动抛异常提示“药品库存不足”。这个细节如果不处理会出现护士执行了医嘱、费用也记了但实际根本没药可发的情况药房去核对时一头雾水。4. WinForms 住院管理系统避坑指南5 个最常见的翻车现场4.1 症状界面卡死护士点一下鼠标转圈 10 秒原因把数据库查询直接丢在 UI 线程执行DataGridView 绑定数据时又同步执行了费用计算。病区如果同时显示 200 个患者、每个患者滚动加载医嘱UI 线程忙不过来。解决所有数据库操作统一走 async/await用 SemaphoreSlim 控制并发查询数量。我习惯在 DAL 层封装一个TaskT QueryAsyncWithRetry方法内部做好连接重试UI 层所有按钮的 Click 事件都改成 async void数据库操作用 await 等待这样界面始终保持响应。状态栏的进度条更新要单独用IProgressint接口避免跨线程访问控件。4.2 症状同一张床被两个患者先后入住床位状态显示空闲原因入院登记时先 SELECT 再 UPDATE并发场景下两个请求都读到“空闲”都插入成功。这种问题在演示环境复现不出来只有实际病区高强度使用时才会出现属于最让人头疼的“玄学 bug”。解决用我 3.2 节写的方案UPDATE 语句带状态条件影响行数为 0 时主动抛异常。另外床位表上的 bed_status 字段一定要加索引这样 UPDATE 的定位更快锁的持有时间更短。护士站录完入院信息、点保存到看到结果整个过程尽量控制在 500 毫秒以内锁冲突的概率会小很多。4.3 症状费用明细对不上医保月结时差几块钱原因金额字段用了 float多笔费用累计后出现精度误差。还有一个隐藏原因费用明细表允许 UPDATE某笔费用被改过但没有任何痕迹。解决全部金额字段改成 decimal(18,2)费用明细表做成只追加表不允许 UPDATE 和 DELETE只允许插入正常记录和冲销记录。这个改动需要在一开始就定好规范等系统跑了一年再改数据迁移的工作量会让人崩溃。我在项目启动时的建表脚本里就直接用 CHECK 约束强制 amount 大于 0冲销记录用单独类型字段标识而不是用负数金额。4.4 症状程序在部分电脑上启动报错在另一部分正常原因目标电脑没装对应版本的 .NET Framework 或运行时。WinForms 项目的坑在于开发机装了最新 SDK打包时没有包含运行时部署到医院那些多年不更新的 Windows 7 电脑上就直接崩。解决项目属性里把“目标框架”设为医院实际环境兼容的版本发布方式选“自包含”把运行时一起打进去。如果医院不允许安装额外运行库就改用 .NET Framework 4.7.2 编译这个版本在 Windows 7 SP1 及以上系统基本都有。另外连接字符串里的数据库实例名要支持配置文件修改不要硬编码医院的信息科经常会迁移数据库服务器。4.5 症状日期时间统计出现 8 小时偏差原因C# 的 DateTime.Now 是本地时间SQL Server 的 GETDATE() 是数据库服务器时间如果两台机器时区设置不一致或者数据库服务器在云上而客户端在内网就会出现入院时间显示和结算时间对不上的情况。解决全系统统一用数据库服务器时间所有 INSERT 和 UPDATE 语句的时间字段都用GETDATE()或SYSDATETIME()不要在 C# 端传时间参数。报表查询的时间范围也统一以数据库时间为准前端只负责格式化显示。日切时间点要单独设定住院系统的“一天”不是自然日 0 点而是早上 8 点因为医嘱执行和费用结算的日切节点通常在 8 点这个逻辑要放在业务层做不能简单用日期字符串比较。5. 交付体验优化界面美化、状态栏进度与验证清单住院管理系统跑通业务逻辑只是第一步真正让护士愿意用的是界面细节。WinForms 默认控件风格偏旧但也不需要过度美化——医院场景要的是清晰、耐看、误操作率低。我常用的美化手段有三个。第一给 DataGridView 设置统一的单元格边框样式和行高隔行变色用AlternatingRowsDefaultCellStyle设置浅灰背景护士长时间看屏幕不会眼花。第二把功能按钮统一改成扁平风格用 FlatStyle.Flat 加背景色主操作按钮用深绿色危险操作如停止医嘱用红色这个颜色规范要写进项目约定里。第三左侧菜单的折叠箭头不画系统默认的而是在自定义 UserControl 里用 GDI 绘制三角箭头——展开时向右、折叠时向下配合鼠标点击事件实现平滑展开和收起效果比 TreeView 自带的箭头好太多。状态栏和进度条的配合有个实用技巧。护士批量执行 100 条医嘱时进度条不能只显示百分比还要显示当前执行到第几条、正在执行哪个患者的医嘱。我把BackgroundWorker的 ReportProgress 传两个参数percent 和 userStateuserState 是一个包含“当前序号/总条数/患者姓名”的字符串在 ProgressChanged 事件里同时更新 ProgressBar 和 StatusLabel。这样护士能看到进度不是卡住而是真的在跑减少因焦虑而重复点击放大问题的概率。private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { var orders (ListDoctorOrder)e.Argument; for (int i 0; i orders.Count; i) { _orderService.ExecuteOrder(orders[i].OrderId, _currentUser); int percent (int)((i 1) * 100.0 / orders.Count); string state $正在执行 {i 1}/{orders.Count}{orders[i].PatientName}; backgroundWorker.ReportProgress(percent, state); } } private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar.Value e.ProgressPercentage; statusLabel.Text e.UserState?.ToString(); }这套方案交付前我会按照一个清单走一遍验证流程。先验证入院登记并发分配床位两个窗口同时提交同一张床的入院申请只有一个成功再验证医嘱执行扣库存执行后库存减少数等于执行记录数然后验证费用累计精度手工录入 100 条 0.1 元的费用汇总结果必须是 10.00 而不是 9.999999最后验证跨天医嘱的日切判断——长期医嘱在 7:59 和 8:01 执行时分别计入哪一天的费用统计。这个顺序有讲究业务链路是“入院→医嘱→费用→结算”问题最容易出现在状态转换和并发控制的位置。单个功能的正确性不代表整体流程顺畅只有把每个环节的数据串起来验证才能保证一个月的数据都能对账平。做这类系统最大的教训就是永远不要相信演示环境护士站的实际操作习惯、网络波动、突然断电都会把看似完美的代码打回原形。设计时多扛一层交付时少跑一趟这个原则比任何花哨的技术选型都管用。希望帮到你。本文还有配套的精品资源点击获取