news 2026/10/20 21:32:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#医院HIS系统源码解析:从挂号到发药的完整业务闭环与二次开发

简介:这是一套基于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 而不是 Web

2.1 客户端形态决定了技术栈

医院门诊大厅的自助机、医生诊室的桌面终端、药房窗口的扫码枪工作站,这些场景对网络波动的容忍度极低。Web 方案一旦内网抖动,医生开方到一半页面白屏,那是要出医疗事故的。这套源码选 WinForms 做客户端,本质上是把业务逻辑放在本地进程里跑,数据库只负责持久化,交互响应是毫秒级的。C# 的 WinForms 虽然被很多人嫌弃“老”,但在院内这种固定分辨率、固定外设(读卡器、打印机、扫码枪)的环境里,它的稳定性和外设兼容性反而是优势。

技术栈清单如下:

层次技术选型说明
客户端WinForms (.NET Framework 4.7.2)桌面窗体,支持 MDI 多文档
数据访问ADO.NET + SqlClient直连数据库,无 ORM
数据库SQL Server 2016+存储过程承担部分业务计算
报表本地 RDLC 报表发票、处方笺打印
通信本地直连 + 部分 WCF药房库存同步走 WCF

2.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 name="HISConn" connectionString="Data Source=.;Initial Catalog=HIS_DB;User ID=sa;Password=你的密码" providerName="System.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 requestEncoding="utf-8" responseEncoding="utf-8"/>。

5.4 药房发药后库存没扣减

现象:药师点了发药,界面提示成功,但库存查询里数量没变。原因:发药和扣库存不在同一个事务里,扣库存失败被吞了异常。解决:用TransactionScope把DispenseDrug和UpdateOrderStatus包起来,任何一步失败整体回滚。源码里早期版本就是分开写的,后来改成事务才稳定。

5.5 患者主索引重复导致病历串号

现象:同一个患者两次挂号,系统生成了两个PatientNo,病历信息对不上。原因:挂号时查重只按姓名+身份证,但身份证字段有空格或大小写差异。解决:查重前先TRIM和UPPER标准化,并且给Patient表的身份证字段加唯一索引。这个坑最要命,病历串号是医疗质量红线,二次开发时务必检查查重逻辑。

6. 二次开发改造:从能跑到好用

6.1 把硬编码的业务规则抽成配置

源码里挂号费、剂量上限这些规则是写死在 C# 里的。如果医院要调价,得改代码重新编译,这在院内环境里很麻烦。我一般会把这些规则抽到数据库的SysConfig表里,用键值对存储,BLL 层启动时加载到内存缓存。比如RegistrationFee_Normal=5、RegistrationFee_Expert=20,改价时只改数据库,重启客户端即可生效。改造时注意加一个配置刷新按钮,不然每次改完都要重启所有终端。

6.2 给关键操作加审计日志

HIS 系统过等保要求操作留痕。源码里只有登录日志,挂号、收费、发药这些关键操作没记录。改造思路是写一个AuditHelper.Log(userId, action, targetId, detail),在 BLL 层每个关键方法入口调一次,日志写SysAuditLog表。表结构建议包含LogId、UserId、Action、TargetId、Detail、LogTime、ClientIP。注意日志表要定期归档,不然半年就上千万行,查询会变慢。

6.3 用存储过程替换部分 C# 计算

当挂号量上来后,C# 层算费再写库的模式会让数据库连接数飙升。我一般会把“算费+插记录+扣号源”整体下沉到存储过程sp_Register,C# 只负责传参和接收返回码。存储过程里用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 的并发逻辑,都强制走一遍“并发测试→数据一致性校验→回滚测试”三步,少一步都不敢上线。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 21:32:26

每日CV开源速览:多模态分割、视频编辑与轻量化检测实战

干这行久了&#xff0c;每天刷一遍计算机视觉的开源动态差不多成了肌肉记忆。尤其像今天这种“Paper with Code速览日”&#xff0c;我习惯把当天冒出来的新库和可复现代码按技术方向筛一遍&#xff0c;能读进去的读进去&#xff0c;能跑通的就顺手跑通&#xff0c;跑不通的也会…

作者头像 李华
网站建设 2026/10/20 21:32:25

IP冲突排查实录:ARP抓包与定位全过程解析

局域网IP冲突排查实录&#xff1a;一次模拟生产环境的抓包与定位全过程做网络运维和系统部署的人&#xff0c;十有八九都撞上过IP冲突的鬼打墙。现象千奇百怪&#xff1a;时通时断、能ping通网关却上不了外网、某台设备频繁掉线、隔一阵子就断一次然后又自己恢复。以前在学校实…

作者头像 李华
网站建设 2026/10/20 21:28:43

Transformer从零实现:自注意力机制原理与PyTorch实战

1. 从"transformers"这个词说起&#xff1a;它到底是什么&#xff0c;为什么值得聊如果你最近在技术社区里频繁看到"transformers"这个词&#xff0c;第一反应可能是那部科幻电影。但在技术圈&#xff0c;尤其是做自然语言处理、计算机视觉、语音识别的圈子…

作者头像 李华
网站建设 2026/10/20 21:28:23

Eclipse导入项目报错排查全攻略:从环境配置到构建路径

如果有朋友问我&#xff0c;Eclipse 使用过程中最折磨人的瞬间是什么&#xff0c;我大概率会说导入项目源码之后那一片红色叉号。明明在别人电脑上运行得好好的项目&#xff0c;到了你这儿一导入&#xff0c;Problems 视图立刻刷出几十条错误&#xff0c;错误类型五花八门&…

作者头像 李华
网站建设 2026/10/20 21:27:40

WordPress时间轴归档页模板改造:从WP_Query优化到纯CSS实现

WordPress后台的“归档”页面&#xff0c;一直是我最不想点开的地方。默认主题给的归档页就是一排月份链接&#xff0c;点进去才能看到文章标题&#xff0c;想找一篇旧文&#xff0c;得像翻纸箱一样一层层扒拉。后来我想明白一件事&#xff1a;归档页不该是文章的仓库索引&…

作者头像 李华
网站建设 2026/10/20 21:27:38

ADU是什么?一文读懂天文图像处理中的像素值、增益与噪声计算

先给一个一句话结论&#xff1a;ADU 就是相机传感器经过模数转换之后&#xff0c;以“整数计数”形式输出的那个像素值&#xff0c;也叫数字量或 DN 值。APEX 系统里显示、统计、相减、平场化处理的所有底层的那个数&#xff0c;都是它。刚接触这类天文图像处理系统的朋友&…

作者头像 李华