资讯动态

C#医院HIS系统源码解析:从挂号到发药的完整业务闭环与二次开发

发布时间:2026/10/20 21:33:11 来源:尧图企业网站定制
简介这是一套基于C#与.NET框架开发的医院HIS系统源码面向医疗信息化开发者、软件工程专业学生及需要二次开发的技术人员可用于学习大型企业级信息系统的架构设计与业务实现。系统覆盖患者管理、医疗业务、财务管理、物资管理、报表统计、权限控制、系统集成与数据库设计等核心模块采用SQL Server存储结构化医疗数据并借助ADO.NET、Windows Forms或WPF完成数据交互与界面呈现。压缩包共186个文件约2.8MB以cs源码、exe可执行程序、resx与resources资源文件、bmp与png图标素材、dll类库、sql脚本及mdf数据库文件为主另含sln与csproj工程文件便于直接编译运行。已有642人学习下载。读者可从中获取完整项目结构、数据库脚本与业务模块实现思路对照源码理解挂号、处方、收费、库存等流程的编码方式适合作为课程设计、毕业设计或HIS二次开发的参考范例。1. 从挂号窗口到药房发药一套 C# 医院 HIS 系统到底在管什么如果你在医疗信息化行业待过大概率听过这句话“HIS 是医院信息系统的地基地基没打好上面盖什么楼都晃。”我最近拆了一套用 C# 写的医院 HIS 系统源码从门诊挂号、医生站开方、收费结算到药房发药整条链路都跑通了。它不是那种只放几个界面的演示壳子而是把患者主索引、医嘱、费用、库存这几条核心数据流真正串了起来。适合谁看一是想理解医疗业务闭环的后端开发者二是需要做院内系统二次开发的技术团队三是计算机相关专业拿它当课程设计或毕业设计的同学。这套代码用的是 WinForms ADO.NET 直连 SQL Server 的经典组合没有过度分层反而让业务逻辑一眼能看穿改起来不绕。2. 技术选型与整体架构为什么用 WinForms 而不是 Web2.1 客户端形态决定了技术栈医院门诊大厅的自助机、医生诊室的桌面终端、药房窗口的扫码枪工作站这些场景对网络波动的容忍度极低。Web 方案一旦内网抖动医生开方到一半页面白屏那是要出医疗事故的。这套源码选 WinForms 做客户端本质上是把业务逻辑放在本地进程里跑数据库只负责持久化交互响应是毫秒级的。C# 的 WinForms 虽然被很多人嫌弃“老”但在院内这种固定分辨率、固定外设读卡器、打印机、扫码枪的环境里它的稳定性和外设兼容性反而是优势。技术栈清单如下层次技术选型说明客户端WinForms (.NET Framework 4.7.2)桌面窗体支持 MDI 多文档数据访问ADO.NET SqlClient直连数据库无 ORM数据库SQL Server 2016存储过程承担部分业务计算报表本地 RDLC 报表发票、处方笺打印通信本地直连 部分 WCF药房库存同步走 WCF2.2 三层结构在代码里的真实样子很多教程讲三层架构画一堆箭头落到代码里却是一锅粥。这套源码的分层很朴素UI层只做控件事件响应和数据显示BLL层写业务规则比如挂号费按年龄段减免DAL层只负责拼 SQL 和参数化查询。我截一段挂号业务的 BLL 代码// BLL/RegistrationService.cs public class RegistrationService { private readonly RegistrationDAL _dal new RegistrationDAL(); // 计算挂号费普通号5元专家号20元70岁以上免普通号费 public decimal CalculateFee(int patientAge, string doctorLevel) { decimal baseFee doctorLevel 专家 ? 20m : 5m; if (patientAge 70 doctorLevel ! 专家) return 0m; // 老年优待 return baseFee; } // 挂号主流程先查重再插挂号记录最后扣减号源 public RegistrationResult Register(Patient patient, string deptCode, string doctorId) { if (_dal.HasActiveRegistration(patient.PatientNo, DateTime.Today)) return RegistrationResult.Fail(该患者今日已挂号请勿重复操作); var fee CalculateFee(patient.Age, _dal.GetDoctorLevel(doctorId)); var regNo _dal.InsertRegistration(patient, deptCode, doctorId, fee); _dal.DecreaseQuota(doctorId); // 号源减一 return RegistrationResult.Ok(regNo, fee); } }这段代码里CalculateFee是纯业务规则不碰数据库单元测试好写Register方法把“查重→算费→插记录→扣号源”四步串起来每一步失败都有明确返回。参数说明patientAge来自患者主索引doctorLevel从医生排班表读取deptCode是科室编码。常见做法是把这些规则写在存储过程里但源码选择放在 C# 层好处是调试时能直接断点看变量坏处是数据库压力大时不好横向扩展。我一般会建议把号源扣减放到存储过程里加锁避免并发超挂。2.3 数据库表设计的几个关键点HIS 系统的表设计有个特点主表字段少而精从表字段多而杂。比如Patient表只存患者基本信息和主索引号而Visit表存每次就诊的科室、医生、时间、诊断。这样设计的好处是患者换科室就诊时主索引不变只新增就诊记录。源码里Patient表的主键是自增PatientId但对外业务全部用PatientNo比如P20240001这是为了避免自增 ID 暴露给用户后被人猜到业务量。3. 核心业务模块拆解从挂号到发药的完整链路3.1 门诊挂号与排班联动挂号不是简单插一条记录。源码里挂号界面加载时会先调ScheduleDAL.GetTodaySchedule(deptCode)拉取当天排班每个医生显示“剩余号源/总号源”。号源扣减用的是UPDATE DoctorSchedule SET RemainQuota RemainQuota - 1 WHERE DoctorId id AND RemainQuota 0返回受影响行数判断是否成功。这个AND RemainQuota 0是防超挂的关键少了它并发时就会挂出负数。// DAL/ScheduleDAL.cs public bool DecreaseQuota(string doctorId) { string sql UPDATE DoctorSchedule SET RemainQuota RemainQuota - 1 WHERE DoctorId DoctorId AND RemainQuota 0; using (var conn new SqlConnection(ConnectionString)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(DoctorId, doctorId); conn.Open(); return cmd.ExecuteNonQuery() 0; // 返回false说明号源已满 } }参数DoctorId是医生工号ExecuteNonQuery返回受影响行数。如果返回 0说明号源已被抢完BLL 层要回滚之前插入的挂号记录。这里有个血泪经验早期版本没加事务号源扣失败但挂号记录已插入导致患者拿着挂号单却看不了病。后来在 BLL 层用TransactionScope把两步包起来才解决。3.2 医生站开方与医嘱校验医生站是 HIS 里逻辑最重的模块。源码里开方界面左侧是患者列表右侧是医嘱录入表格。每条医嘱要校验三件事药品是否在院内目录、剂量是否超限、是否与过敏史冲突。校验逻辑写在OrderService.ValidateOrder里// BLL/OrderService.cs public ValidationResult ValidateOrder(Order order, Patient patient) { // 1. 药品目录校验 if (!_drugDal.ExistsInCatalog(order.DrugCode)) return ValidationResult.Fail($药品 {order.DrugCode} 不在院内目录); // 2. 单次剂量上限校验从药品字典读MaxDose var drug _drugDal.GetDrug(order.DrugCode); if (order.Dose drug.MaxDose) return ValidationResult.Fail($剂量 {order.Dose} 超过上限 {drug.MaxDose}); // 3. 过敏史冲突校验 if (patient.AllergyList.Contains(drug.AllergyCode)) return ValidationResult.Fail($患者对 {drug.DrugName} 过敏禁止使用); return ValidationResult.Ok(); }这段代码的patient.AllergyList是从患者主索引的过敏史字段反序列化来的存的时候用逗号分隔的过敏原编码。常见做法是把校验做成规则引擎但源码用硬编码 if 判断好处是执行快、调试直观坏处是加新规则要改代码。二次开发时如果医院要加“医保适应症校验”就在第三步后面追加一个if即可。3.3 收费结算与发票打印收费窗口的流程是扫处方条码 → 拉取未收费医嘱 → 计算总金额 → 收款 → 更新医嘱状态 → 打印发票。源码里金额计算有个细节药品费、诊疗费、材料费分开累计因为医保报销比例不同。ChargeService.Settle方法返回一个SettleResult里面包含自费金额、医保金额、找零。// BLL/ChargeService.cs public SettleResult Settle(string visitNo, decimal paidAmount) { var orders _orderDal.GetUnchargedOrders(visitNo); decimal drugFee orders.Where(o o.ItemType 药品).Sum(o o.Amount); decimal treatFee orders.Where(o o.ItemType 诊疗).Sum(o o.Amount); decimal total drugFee treatFee; if (paidAmount total) return SettleResult.Fail(收款金额不足); _orderDal.MarkAsCharged(visitNo); // 批量更新收费状态 _invoiceDal.InsertInvoice(visitNo, total, paidAmount); return SettleResult.Ok(total, paidAmount - total); }ItemType字段区分费用类型MarkAsCharged用UPDATE ... WHERE VisitNo visitNo AND ChargeStatus 0批量更新避免逐条更新带来的性能问题。发票打印走 RDLC 报表模板文件放在Reports/Invoice.rdlc数据源就是SettleResult对象。3.4 药房发药与库存扣减药房窗口扫处方条码后系统显示待发药品列表药师核对后点“发药”触发库存扣减。源码里库存扣减和挂号号源扣减思路一致但多了一步“批次选择”同一药品可能有多个批次优先发效期近的。// DAL/InventoryDAL.cs public bool DispenseDrug(string drugCode, int quantity) { // 按效期升序取批次逐批扣减直到扣完 string sql UPDATE Inventory SET Quantity Quantity - Qty WHERE BatchId IN ( SELECT TOP 1 BatchId FROM Inventory WHERE DrugCode DrugCode AND Quantity Qty ORDER BY ExpireDate ASC ); // 实际源码用事务循环处理跨批次扣减此处简化展示 using (var conn new SqlConnection(ConnectionString)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(DrugCode, drugCode); cmd.Parameters.AddWithValue(Qty, quantity); conn.Open(); return cmd.ExecuteNonQuery() 0; } }参数Qty是发药数量ORDER BY ExpireDate ASC保证先发近效期。如果单批次不够源码用事务循环扣多个批次。这里踩过的坑是并发发药时两个窗口同时扣同一批次导致库存扣成负数。后来在UPDATE里加了AND Quantity Qty条件才堵住。4. 环境搭建与运行从零把系统跑起来4.1 开发环境准备这套源码基于 .NET Framework 4.7.2所以开发机需要装 Visual Studio 2019 或 2022社区版够用勾选“.NET 桌面开发”工作负载。数据库用 SQL Server 2016 以上Express 版也行但要注意 Express 版单库 10GB 限制正式数据量大的话得换标准版。另外需要装一个 RDLC 报表设计器VS 里自带但如果是 VS 2022 要单独装 Microsoft RDLC Report Designer 扩展。4.2 数据库还原与连接字符串配置源码包里有个Database/HIS_DB.bak备份文件还原步骤-- 在 SSMS 里执行 RESTORE DATABASE HIS_DB FROM DISK D:\HIS\Database\HIS_DB.bak WITH MOVE HIS_DB TO D:\HIS\Data\HIS_DB.mdf, MOVE HIS_DB_Log TO D:\HIS\Data\HIS_DB_Log.ldf, REPLACE;还原后要改连接字符串。源码里连接字符串写在App.config的connectionStrings节点connectionStrings add nameHISConn connectionStringData Source.;Initial CatalogHIS_DB;User IDsa;Password你的密码 providerNameSystem.Data.SqlClient/ /connectionStrings注意Data Source.表示本机默认实例如果是命名实例要写成.\SQLEXPRESS。密码别用弱口令院内系统被扫到 sa 弱密码是常见翻车点。4.3 首次运行与初始账号编译前先确认HIS.UI是启动项目。F5 运行后弹出登录窗体初始账号在SysUser表里默认管理员是admin密码字段存的是 MD5 值。源码里 MD5 加密写在Common/SecurityHelper.cs首次登录后建议立刻改密码。登录成功后进入主窗体左侧是功能导航树右侧是 MDI 子窗体区域。先点“系统管理→科室维护”建几个科室再点“医生排班”给医生排班否则挂号界面拉不到号源。4.4 常见启动报错排查第一次跑最容易遇到三个错一是“未找到 RDLC 报表”原因是Reports文件夹没复制到输出目录在项目属性里把Reports设为“始终复制”二是“SQL Server 连接失败”检查 SQL Server 的 TCP/IP 协议是否启用端口 1433 是否放行三是“System.Data.SqlClient 版本冲突”在App.config里加bindingRedirect重定向到统一版本。5. 避坑与常见问题那些年我们踩过的雷5.1 并发挂号导致号源超卖现象两个窗口同时给最后一个号挂号系统都提示成功但号源变成 -1。原因DecreaseQuota的UPDATE没加AND RemainQuota 0或者加了但 BLL 层没判断返回值。解决在UPDATE语句里强制加条件并且 BLL 层根据ExecuteNonQuery返回值决定是否回滚挂号记录。我一般还会在号源表加一个RowVersion字段做乐观锁。5.2 医嘱保存后药品名称显示为空现象医生开方保存成功但重新打开医嘱列表药品名称列是空白。原因Order表只存了DrugCode查询时没 JOIN 药品字典表。解决在OrderDAL.GetOrdersByVisit的 SQL 里加LEFT JOIN DrugDict ON Order.DrugCode DrugDict.DrugCode查询结果里带出DrugName。这个坑的教训是别为了省一次 JOIN 把冗余字段省掉该冗余时就得冗余。5.3 发票打印偏移或乱码现象RDLC 报表打印出来金额跑到纸张外面或者中文变问号。原因RDLC 报表的纸张尺寸没设对或者字体用了系统没有的。解决在 RDLC 设计器里把PageSize设成和实际发票纸一致常见是 241mm×140mm字体统一用“宋体”或“微软雅黑”。如果还乱码检查App.config里有没有加globalization requestEncodingutf-8 responseEncodingutf-8/。5.4 药房发药后库存没扣减现象药师点了发药界面提示成功但库存查询里数量没变。原因发药和扣库存不在同一个事务里扣库存失败被吞了异常。解决用TransactionScope把DispenseDrug和UpdateOrderStatus包起来任何一步失败整体回滚。源码里早期版本就是分开写的后来改成事务才稳定。5.5 患者主索引重复导致病历串号现象同一个患者两次挂号系统生成了两个PatientNo病历信息对不上。原因挂号时查重只按姓名身份证但身份证字段有空格或大小写差异。解决查重前先TRIM和UPPER标准化并且给Patient表的身份证字段加唯一索引。这个坑最要命病历串号是医疗质量红线二次开发时务必检查查重逻辑。6. 二次开发改造从能跑到好用6.1 把硬编码的业务规则抽成配置源码里挂号费、剂量上限这些规则是写死在 C# 里的。如果医院要调价得改代码重新编译这在院内环境里很麻烦。我一般会把这些规则抽到数据库的SysConfig表里用键值对存储BLL 层启动时加载到内存缓存。比如RegistrationFee_Normal5、RegistrationFee_Expert20改价时只改数据库重启客户端即可生效。改造时注意加一个配置刷新按钮不然每次改完都要重启所有终端。6.2 给关键操作加审计日志HIS 系统过等保要求操作留痕。源码里只有登录日志挂号、收费、发药这些关键操作没记录。改造思路是写一个AuditHelper.Log(userId, action, targetId, detail)在 BLL 层每个关键方法入口调一次日志写SysAuditLog表。表结构建议包含LogId、UserId、Action、TargetId、Detail、LogTime、ClientIP。注意日志表要定期归档不然半年就上千万行查询会变慢。6.3 用存储过程替换部分 C# 计算当挂号量上来后C# 层算费再写库的模式会让数据库连接数飙升。我一般会把“算费插记录扣号源”整体下沉到存储过程sp_RegisterC# 只负责传参和接收返回码。存储过程里用BEGIN TRAN ... COMMIT保证原子性并且用UPDLOCK提示锁住号源行。改造后单台服务器支撑的门诊量能翻一倍。但要注意存储过程调试不如 C# 方便改之前先在测试库跑通。6.4 验证改造是否成功的三个指标改完别急着上线先跑三个验证一是并发测试用 JMeter 模拟 50 个窗口同时挂最后一个号看是否只有一个成功二是数据一致性挂号后查Visit表和DoctorSchedule表号源扣减数和挂号数必须相等三是回滚测试故意在扣号源时断网看挂号记录是否回滚。这三个指标过了基本就稳了。6.5 一个具体技巧用 RowVersion 做乐观锁源码里号源扣减用的是UPDATE ... WHERE RemainQuota 0这是悲观锁思路。更优雅的做法是给DoctorSchedule表加RowVersion列C# 读取时带出RowVersion更新时WHERE RowVersion oldVersion如果返回 0 行说明被人改过提示用户重试。这样锁粒度更细并发性能更好。改造代码// DAL/ScheduleDAL.cs 乐观锁版本 public bool DecreaseQuotaWithVersion(string doctorId, byte[] oldVersion) { string sql UPDATE DoctorSchedule SET RemainQuota RemainQuota - 1 WHERE DoctorId DoctorId AND RowVersion OldVersion; using (var conn new SqlConnection(ConnectionString)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(DoctorId, doctorId); cmd.Parameters.AddWithValue(OldVersion, oldVersion); conn.Open(); return cmd.ExecuteNonQuery() 0; // false表示版本冲突需重试 } }参数OldVersion是查询时读到的RowVersion字节数组。BLL 层拿到 false 后重新查询最新号源再试一次最多重试三次。这个技巧在库存扣减、收费结算里同样适用。从那以后我每次改 HIS 的并发逻辑都强制走一遍“并发测试→数据一致性校验→回滚测试”三步少一步都不敢上线。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑