简介:这是一套面向中小型医疗机构信息化建设与WinForm开发学习者的医疗设备管理系统源码,基于C#与WinForm技术栈实现,适合需要参考完整业务系统架构的开发者、毕业设计学生及医院信息科人员。系统覆盖设备信息管理、合同管理、配件管理、维修管理、信息统计与打印六大核心模块,并内置管理员与普通用户两级权限登录,账号密码可直接在配置中修改,便于二次开发与教学演示。压缩包共155个文件,约12.32MB,包含30个cs源码文件、20个dll依赖库、9个resx与17个resources资源文件,以及png、jpg、ico等界面素材和config、xml配置文件,另附需求文档与操作手册,方便理解业务逻辑与部署流程。目前已有777人学习下载,读者可借此掌握设备台账维护、维修流程跟踪、统计报表输出等典型功能的实现思路,并参考其分层结构与数据库设计快速搭建同类管理系统。
1. 从一台挂号机说起:这套 WinForm 医疗管理系统到底能干什么
前阵子帮一个做医院信息化的朋友看项目,他扔过来一个压缩包,说“你帮我看看这套东西还能不能改”。解压之后是一个典型的 C# WinForm 解决方案,双击 sln 打开,编译,跑起来——登录界面、门诊挂号、医生站、收费、药房、住院管理,一整套流程居然都能点通。说实话,我一开始以为就是个练手 Demo,结果翻了下数据库脚本,表结构、存储过程、触发器都有,业务闭环也基本完整。这套医疗管理系统 CS(WinForm)版本,本质上是一个面向中小型医院或诊所的桌面端信息管理系统,覆盖门诊挂号、医生接诊、收费结算、药品库存、住院登记这几条主线。它适合两类人:一类是刚学完 C# 和 WinForm、想找一个有真实业务复杂度的项目练手的开发者;另一类是需要一套可离线部署、能快速二次开发的基层医疗信息化系统的团队。整套系统不依赖浏览器,局域网内 C/S 架构,数据库用 SQL Server,客户端用 .NET Framework WinForm,部署起来比 B/S 省心不少。
2. 技术选型与整体架构:为什么是 WinForm + SQL Server 这套组合
2.1 三层架构在桌面端的落地方式
这套系统没有用 MVC 或 MVVM 那种偏 Web 的套路,而是老老实实按 WinForm 项目最常见的三层架构来组织:表示层(UI)、业务逻辑层(BLL)、数据访问层(DAL),再加一个实体模型层(Model)。表示层就是一堆 Form 和 UserControl,业务逻辑层负责校验和流程编排,数据访问层直接拼 SQL 或者调存储过程。这种结构在 WinForm 项目里非常典型,好处是职责清晰,改一个模块不容易牵连其他模块。
我一般会把解决方案拆成这几个项目:
MedicalSystem.sln ├── MedicalSystem.UI // WinForm 界面层 ├── MedicalSystem.BLL // 业务逻辑层 ├── MedicalSystem.DAL // 数据访问层 ├── MedicalSystem.Model // 实体类 └── MedicalSystem.Common // 公共工具类UI 层引用 BLL,BLL 引用 DAL 和 Model,DAL 引用 Model 和 Common。这种单向依赖保证了底层不会反向依赖界面,后面换 UI 框架或者加 Web API 都不会太痛苦。
2.2 数据库设计与连接管理
数据库用的是 SQL Server,常见做法是把连接字符串放在 App.config 里,通过 ConfigurationManager 读取。DAL 层一般会封装一个 SqlHelper 类,统一处理连接打开、命令执行、参数化查询和异常捕获。下面这段是我从项目里抽出来的 SqlHelper 核心方法,基本能代表这套系统的数据访问风格:
public static class SqlHelper { // 从配置文件读取连接字符串,避免硬编码 private static readonly string connStr = ConfigurationManager.ConnectionStrings["MedicalDB"].ConnectionString; // 执行增删改,返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) { using (SqlCommand cmd = new SqlCommand(sql, conn)) { // 参数化查询,防止 SQL 注入 if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } } // 执行查询,返回 DataTable 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 adapter = new SqlDataAdapter(cmd); DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } } }这段代码里有两个关键点值得注意。第一,连接字符串从配置文件读取,部署到不同医院时只需要改 App.config,不用重新编译。第二,所有参数都走 SqlParameter,这是 WinForm 项目里防 SQL 注入最基本的手段,我见过太多练手项目直接拼字符串,一上生产就翻车。参数说明方面,ExecuteNonQuery 适合 INSERT、UPDATE、DELETE,ExecuteQuery 适合 SELECT,返回值分别是 int 和 DataTable,调用方拿到 DataTable 后可以直接绑定到 DataGridView。
2.3 主界面框架与模块导航
主界面通常是一个 MDI 容器窗体,左侧或顶部放菜单树,点击菜单后动态创建对应的子窗体并嵌入。这种设计在 WinForm 里很常见,核心是控制子窗体不要重复打开。常见做法是用一个 Dictionary 缓存已打开的窗体实例,关闭时移除。下面是一个简化的导航逻辑:
private Dictionary<string, Form> openForms = new Dictionary<string, Form>(); private void OpenForm(string key, Func<Form> formFactory) { // 如果已经打开,直接激活,避免重复创建 if (openForms.ContainsKey(key)) { openForms[key].Activate(); return; } Form form = formFactory(); form.MdiParent = this; form.FormClosed += (s, e) => openForms.Remove(key); openForms[key] = form; form.Show(); }这段逻辑解决的是 WinForm MDI 应用里一个很烦人的问题:用户反复点同一个菜单,结果开了一堆重复窗口。用字典缓存之后,同一个模块只会有一个实例,关闭时自动清理。参数 key 是模块唯一标识,formFactory 是一个委托,延迟创建窗体,避免启动时一次性加载所有模块拖慢速度。
3. 核心业务模块拆解:挂号、接诊、收费、药房怎么串起来
3.1 门诊挂号模块的实现要点
挂号是整个门诊流程的入口,核心动作是:选择科室、选择医生、选择号别(普通/专家/急诊)、生成挂号单、写入挂号表、同时更新医生当日号源。挂号表一般包含挂号 ID、患者 ID、科室 ID、医生 ID、号别、挂号费、挂号时间、状态等字段。下面是一个挂号业务逻辑层的典型写法:
public class RegisterBLL { private RegisterDAL dal = new RegisterDAL(); public bool AddRegister(RegisterModel model) { // 先检查该医生该时段号源是否已满 int used = dal.GetUsedCount(model.DoctorId, model.VisitDate, model.TimeSlot); int total = dal.GetTotalCount(model.DoctorId, model.VisitDate, model.TimeSlot); if (used >= total) { throw new Exception("该医生当前时段号源已满"); } // 写入挂号记录 return dal.Insert(model) > 0; } }这里的关键是号源校验必须在写入之前完成,而且要考虑并发。常见做法是在 DAL 层用事务包住“查询已用号源”和“插入挂号记录”两步,或者直接在存储过程里用 UPDATE 加条件判断。参数方面,DoctorId 是医生主键,VisitDate 是就诊日期,TimeSlot 是时段(上午/下午),这三个字段组合起来决定号源池。
3.2 医生接诊与处方开具
医生接诊模块是业务最重的一块,通常包含患者列表、病历录入、处方开具、检查申请几个子功能。医生登录后看到的是自己当日挂号的患者队列,点击某个患者进入接诊界面。处方开具的核心是把处方主表和处方明细表关联起来,主表存患者、医生、诊断、开方时间,明细表存药品、规格、数量、用法用量。
public bool SavePrescription(PrescriptionModel main, List<PrescriptionDetail> details) { using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); using (SqlTransaction trans = conn.BeginTransaction()) { try { // 插入处方主表,返回处方ID int presId = dal.InsertMain(main, conn, trans); // 批量插入处方明细 foreach (var d in details) { d.PrescriptionId = presId; dal.InsertDetail(d, conn, trans); } trans.Commit(); return true; } catch { trans.Rollback(); throw; } } } }这段代码用事务保证处方主表和明细表要么一起成功,要么一起回滚。这是医疗系统里必须做的,处方开了一半没明细,药房那边就乱了。参数说明:main 是处方主表实体,details 是明细列表,InsertMain 返回自增主键,然后回填到每条明细的 PrescriptionId 上。
3.3 收费结算与退费处理
收费模块要处理的是挂号费、药品费、检查费、治疗费等多种费用类型的合并结算。常见做法是有一个费用明细表,收费时按患者 ID 汇总未结算的费用,生成收费单,更新费用明细状态为已结算。退费则是反向操作,但需要记录退费原因和操作人。
public decimal GetUnpaidTotal(int patientId) { string sql = @"SELECT ISNULL(SUM(Amount),0) FROM FeeDetail WHERE PatientId=@PatientId AND Status=0"; DataTable dt = SqlHelper.ExecuteQuery(sql, new SqlParameter("@PatientId", patientId)); return Convert.ToDecimal(dt.Rows[0][0]); }这里用 ISNULL 兜底是为了防止没有未结算费用时返回 NULL 导致转换异常。Status=0 表示未结算,1 表示已结算,2 表示已退费。收费完成后要把对应记录 Status 更新为 1,退费则更新为 2 并插入一条退费流水。
3.4 药房库存与发药管理
药房模块和处方模块是联动的。医生开完处方,药房那边能看到待发药列表,发药时扣减库存。库存管理要处理入库、出库、盘点、预警几个动作。库存预警的常见做法是设置一个最低库存阈值,低于阈值时在首页或者药房界面高亮提示。
public bool DispenseDrug(int prescriptionId, int pharmacistId) { // 查询处方明细 var details = detailDal.GetByPrescriptionId(prescriptionId); foreach (var d in details) { // 检查库存是否充足 int stock = stockDal.GetStock(d.DrugId); if (stock < d.Quantity) { throw new Exception($"药品 {d.DrugName} 库存不足,当前库存 {stock}"); } // 扣减库存 stockDal.ReduceStock(d.DrugId, d.Quantity); } // 更新处方状态为已发药 return mainDal.UpdateStatus(prescriptionId, 2) > 0; }发药逻辑里最关键的是库存校验和扣减要在同一个事务里,否则并发发药时会出现超卖。参数说明:prescriptionId 是处方主键,pharmacistId 是发药药师 ID,用于记录操作人。
4. 避坑与常见问题:跑这套系统时我踩过的那些雷
4.1 数据库连接失败但报错信息很模糊
现象是程序启动后点登录没反应,或者直接弹一个“未将对象引用设置到对象的实例”。原因通常是 App.config 里的连接字符串名称和代码里读的名称不一致,或者 SQL Server 实例名写错。解决方法是先确认配置文件里的 name 属性,再确认 Data Source 是 . 还是 .\SQLEXPRESS 还是具体实例名,最后用 SSMS 手动连一次验证账号密码。
4.2 WinForm 界面在高分屏上模糊
现象是在 2K 或 4K 显示器上,窗体控件字体发虚、布局错位。原因是 .NET Framework WinForm 默认没有开启 DPI 感知。解决方法是在 app.manifest 里启用 dpiAware,或者在程序入口处调用 SetProcessDPIAware。更彻底的做法是把 AutoScaleMode 设为 Dpi,并重新调整控件布局。
4.3 并发挂号导致号源超卖
现象是两个窗口同时给同一个医生挂号,结果都成功了,号源超出上限。原因是查询和插入之间没有加锁。解决方法是在 DAL 层用事务加更新锁,或者直接在存储过程里用 UPDATE 带条件判断,把校验和写入合并成原子操作。
4.4 处方保存后药房看不到
现象是医生开完处方,药房待发药列表里没有。原因通常是处方状态字段没有正确更新,或者药房查询条件过滤掉了新处方。解决方法是检查处方主表的 Status 字段是否从 0(待缴费)正确流转到 1(已缴费)再到 2(待发药),以及药房查询 SQL 的 WHERE 条件是否包含了正确的状态值。
4.5 编译报错找不到 System.Configuration
现象是 SqlHelper 里引用 ConfigurationManager 时报错。原因是项目没有引用 System.Configuration 程序集。解决方法是在解决方案资源管理器里右键引用,添加 System.Configuration,或者在 NuGet 里安装 System.Configuration.ConfigurationManager 包。
5. 二次开发与改造思路:从能跑到好用还差哪几步
这套系统跑起来之后,真正的工作量在改造上。我一般会从三个方向入手:数据访问层替换、界面现代化、业务规则外置。
第一个方向是把 SqlHelper 换成 ORM。现在这套 DAL 层全是手写 SQL,改一个字段要动好几处。如果换成 Dapper 或者 Entity Framework,增删改查的代码量能少一半以上。Dapper 的好处是轻量,学习成本低,对现有代码侵入小。下面是一个用 Dapper 改写挂号查询的例子:
public List<RegisterModel> GetTodayRegisters(int doctorId, DateTime date) { string sql = @"SELECT * FROM Register WHERE DoctorId=@DoctorId AND VisitDate=@VisitDate"; using (var conn = new SqlConnection(connStr)) { return conn.Query<RegisterModel>(sql, new { DoctorId = doctorId, VisitDate = date }).ToList(); } }对比原来的 DataTable 方式,Dapper 直接返回强类型列表,省掉了手动转换的代码,而且参数用匿名对象,写起来更顺手。
第二个方向是界面现代化。WinForm 原生控件确实有点年代感,但没必要整个推翻。常见做法是引入第三方 UI 库,比如 SunnyUI、HZHControls 或者 DevExpress(商业版),把按钮、表格、导航栏替换掉,整体观感能提升一个档次。替换的时候注意保持事件处理逻辑不变,只换控件皮肤。
第三个方向是把业务规则从代码里抽出来。比如挂号费、诊疗费的计算规则,不同科室、不同号别可能不一样,硬编码在 BLL 里以后改起来很痛苦。我一般会建一张规则配置表,把费用计算参数化,BLL 只负责读取规则并执行计算。
改造过程中有一个验证方法很实用:每改完一个模块,就用原来的测试数据跑一遍完整流程,从挂号到收费到发药,确认数据流转和金额计算没有偏差。我自己的习惯是每次动 DAL 层之前,先把关键查询的返回结果导出成基准数据,改完之后再跑一次对比,差异超过预期就回滚。从那以后我每次改数据访问层都强制走一遍这个对比流程,省了不少后悔药。希望帮到你。
本文还有配套的精品资源,点击获取