简介面向C#开发者与医疗信息化学习者的医院HIS系统完整源码包覆盖患者管理、医疗业务、财务、物资、报表统计等核心模块并配有数据库文件可快速部署运行。压缩包共186个文件约2.8MB文件以C#源程序cs、可执行程序exe、界面资源resx、图标位图bmp/ico为主另含SQL脚本、MDF数据库备份及XSD数据集定义结构清晰便于按模块查阅其中数据库备份可直接还原省去环境搭建时间。已有641人学习下载。读者可借此熟悉HIS系统从数据库设计到界面交互的完整链路理解C#结合Windows Forms开发企业级信息系统的分层思路学习ADO.NET数据访问、RBAC权限控制、报表导出等实现方式同时源码具备二次开发基础适合作为相关课程设计、毕业设计或中小型医院信息化的改造参考。无论用于教学还是产品原型都足够扎实。1. C#医院HIS系统一套能跑通门诊全流程的医疗信息管理源码一个门诊窗口在高峰期每秒要处理七八次挂号、缴费和退号操作号源一旦并发写错患者可能拿着同一张就诊卡出现在两个诊室门口。C#医院HIS系统就是针对这类场景设计的一套完整业务系统从患者建档、门诊挂号、医生开方、收费结算到药房发药数据在同一条链路里闭环流转而不是一堆互相孤立的页面。它适合正在接触医疗信息化、想弄懂医院核心流程如何落地的开发者也适合接手内网项目但没有完整业务参考的团队。拿到这套源码你能看到挂号、处方、库存、结算如何通过数据库事务和表约束互相咬合照着一遍遍调试下来门诊业务的基本盘基本就有底了。2. 技术选型与整体架构.NET SQL Server 驱动的分层业务闭环2.1 技术栈选择为什么医疗内网项目偏爱C#与SQL Server医院信息系统的业务特点是强事务、高约束、内网部署。拆这套源码时最直接的感受是C# .NET Framework SQL Server 的组合在Windows内网环境下几乎没有额外的运行时成本ADO.NET天然和SQL Server的类型系统对齐decimal字段不会在映射过程中被悄悄变成double。具体到这套系统Web端用的是ASP.NET MVC没有引入前后端分离框架页面是Razor视图加jQuery。这种做法在小规模医院内网里非常务实——不需要Node环境不需要前端构建IIS发布后直接能用老版本浏览器也能正常打开。数据访问层没有用ORM主导而是以ADO.NET加存储过程为主。这不是技术倒退而是医疗项目里存储过程方便做权限隔离、方便DBA在数据库端排查性能问题也方便在变更时只更新数据库而不重新发布整个站点。还有一点值得注意这套系统故意把事务控制放在业务层不在控制器里写事务。控制器只负责参数校验和视图组装所有开处方、收费、发药这类跨表操作都放进Service方法内部用SqlTransaction统一管理。这样做的直接好处是数据库连接生命周期可控制一个方法里多张表的写操作要么全成功要么全回滚不会出现处方保存了但药品明细丢了一半这种尴尬情况。2.2 三层架构拆解UI、BLL、DAL的边界与依赖方向整个解决方案按五个工程拆分HIS.Models放实体和DTOHIS.DAL负责SQL执行与结果映射HIS.BLL承载业务规则和事务HIS.Web是MVC站点控制器、视图、脚本HIS.Common放日志、加密、Session认证等公共能力。依赖方向是单向的HIS.Web引用BLLBLL引用DALDAL只引用Models谁也不能反向引用。这种边界在实际开发里的意义是改数据库字段时只需要动Models和DALBLL的接口签名不变改业务规则时只动BLL的方法体Controller完全不用知道。接手这个项目时我优先看BLL层的RegisterService和ChargeService这两个文件因为门诊核心链路的状态流转都集中在它们里面。值得一提的是存储过程在这里相当于第四层。DAL方法通常只有两类一类是几行ADO.NET调用存储过程并返回DataTable或实体集合另一类是直接执行参数化SQL做简单查询。复杂查询排班列表、医生日报、药品库存报表统一走存储过程这样开发人员不在代码里拼字符串SQLSQL注入和字段拼写错误都被天然挡住了。2.3 数据库设计从患者主索引到医嘱明细的关联关系这套系统数据库名是HISDB核心表不多但每张表职责都很明确。最关键的是患者主索引和就诊对象的贯穿patients表以patient_id为主键registrations表记录一次就诊一个患者可以有多条就诊记录一条就诊记录可以带出多个处方、一个处方对应多条明细。这样门诊全流程的数据都能从registration_id一路串联起来。表名用途关键字段patients患者基本信息patient_id, name, id_card, phonedepartments科室字典dept_id, dept_namedoctors医生信息doctor_id, name, dept_idopd_schedule门诊排班号源schedule_id, doctor_id, visit_date, used_flagregistrations挂号记录一次就诊registration_id, patient_id, schedule_id, statusprescriptions处方头prescription_id, registration_id, total_amountprescription_items处方明细item_id, prescription_id, drug_id, quantity, amountdrug_stock药品库存drug_id, warehouse_id, stock_qtycharge_records收费流水charge_id, registration_id, bill_no, amount设计上有几个值得抄的点金额字段统一decimal(18,2)不允许float号源表用used_flag标记是否已占用配合锁来控制并发所有逻辑删除都带一列del_flag做业务操作时默认只查del_flag 0的数据。你会看到这些字段选择直接决定存储过程怎么写。索引设计上registrations表在patient_id和schedule_id上建非聚集索引opd_schedule表在(visit_date, doctor_id)上建复合索引因为这两类查询是门诊最高频的。外键方面prescription_items和drug_stock之间没有强外键约束而是通过drug_id做软关联理由是药房换仓库、退药时不会因为外键约束卡住事务。这个设计一开始看起来像反模式实际跑下来反而更灵活。权限这块用的是Session 自定义ActionFilter登录后把用户ID和角色ID写进Session每个控制器方法打上[AuthRequired]特性。系统管理模块维护用户表、角色表和菜单表属于比较标准的RBAC模型。改菜单权限时不需要碰数据库结构往菜单表里插一条记录再赋给角色就行。3. 核心模块拆解一次门诊就诊从挂号到药房发药的完整链路3.1 挂号模块号源锁与并发控制挂号是门诊高峰时最容易翻车的环节。这套系统把号源放在opd_schedule表里每个医生每个时段一行used_flag 0表示可挂。并发场景下两个窗口同时点挂号如果代码是先查有没有空号、再判断、再UPDATE大概率会有一次挂重。所以源码里的挂号操作直接封装成存储过程用数据库锁来保证同一时刻只有一个事务能拿到这个号源CREATE PROCEDURE [dbo].[usp_RegisterTicket] patientId INT, scheduleId INT, registerUserId INT, registrationId INT OUTPUT AS BEGIN SET NOCOUNT ON; DECLARE errorMsg NVARCHAR(200); SET registrationId 0; BEGIN TRANSACTION; SELECT registrationId id FROM dbo.opd_schedule WITH (UPDLOCK, ROWLOCK, HOLDLOCK) WHERE id scheduleId AND used_flag 0 AND del_flag 0; IF registrationId 0 BEGIN SET errorMsg N号源已被占用请刷新后重试; ROLLBACK TRANSACTION; RAISERROR(errorMsg, 16, 1); RETURN; END UPDATE dbo.opd_schedule SET used_flag 1, register_time GETDATE(), register_user_id registerUserId WHERE id scheduleId; INSERT INTO dbo.registrations(patient_id, schedule_id, status, create_time, create_user) VALUES (patientId, scheduleId, 1, GETDATE(), registerUserId); SET registrationId SCOPE_IDENTITY(); COMMIT TRANSACTION; END这段存储过程的要点在WITH子句UPDLOCK告诉SQL Server我要对选中的行加更新锁HOLDLOCK让锁一直保持到事务结束。两个提示配合起来相当于把号源行锁住第二个并发事务会阻塞在这条SELECT上等第一个提交后它读到的used_flag已经是1直接返回0业务层据此给出号源占用提示。如果用的是默认读提交隔离级别不加锁提示就会出现号源超卖。参数方面patientId和scheduleId来自挂号页面选择registerUserId从当前登录Session里取registrationId是输出参数返回新建挂号记录的自增ID。如果患者档案不存在插入registrations时外键会直接报错这种场景不要用RAISERROR掩盖让SQL Server抛外键错误反而更利于排查。3.2 医生工作站处方录入与事务包裹医生工作站的核心是开处方。一次保存动作会同时写一张处方头和若干条明细任何一条明细失败都不能留下半张处方。这里直接在BLL层用SqlTransaction把所有DAL调用包住public int SavePrescription(int registrationId, ListPrescriptionItem items) { if (items null || items.Count 0) throw new ArgumentException(处方明细不能为空); using (var conn new SqlConnection(_connString)) using (var tx conn.BeginTransaction()) { conn.Open(); try { var headerId _dal.InsertPrescription(conn, tx, registrationId, DateTime.Now); decimal total 0m; foreach (var item in items) { if (item.DrugId 0 || item.Quantity 0) throw new ArgumentException(药品或数量不合法); if (item.Amount 0) throw new ArgumentException(金额不能为负); _dal.InsertPrescriptionItem(conn, tx, headerId, item); total item.Amount; } // 金额重新回写处方头不信任前端传过来的总价 _dal.UpdatePrescriptionTotal(conn, tx, headerId, total); tx.Commit(); return headerId; } catch { tx.Rollback(); throw; } } }这段代码有几个值得注意的设计事务紧跟连接创建绝不跨方法传递打开状态的连接所有业务校验放在事务内、写库前一旦发现非法数据立即抛异常回滚处方总金额在后端重新累加并回写前端传的total_price完全不信任。这三个习惯能挡住绝大多数前台看着正常、后台数据库脏数据的翻车现场。如果你准备把这段代码改成支持医保预审只需要在InsertPrescriptionItem之前调用医保接口把返回的预审编码写进明细表新增列即可事务边界不用动。3.3 收费结算金额计算与单据号生成收费模块最大的坑是流水号重复。月底财务对账时发现两条单据号一样基本都是因为用SELECT MAX(bill_no)1生成单号。这套系统的做法是维护一张bill_seq序列表用UPDATE语句原子地取号DECLARE cur INT; UPDATE dbo.bill_seq WITH (ROWLOCK, UPDLOCK) SET cur current_no current_no 1 WHERE seq_key OPD_CHARGE; SELECT ZF CONVERT(VARCHAR(8), GETDATE(), 112) RIGHT(000000 CAST(cur AS VARCHAR(6)), 6);UPDATE语句在设置current_no的同时把自增后的值赋给本地变量锁粒度精细到一行并发下两个会话不会拿到相同的号。单据号格式包含日期和六位序列例如ZF20250613000042方便财务按天对账。这里有一个细节序列表的初始值和步长可以配置如果某天单据量超过999999需要提前把位数扩到8位或更换序列键。收费金额计算放在BLL的ChargeService里先累加明细金额再根据患者类型打折最后四舍五入到分。这里必须用Math.Round(value, 2, MidpointRounding.AwayFromZero)而不是默认的银行家舍入否则某些金额会少一分钱。3.4 药房发药库存扣减与条件更新发药时同样不能先查库存再更新因为查询和更新之间永远存在时间窗口。源码里的扣减语句直接在UPDATE里带上库存条件让数据库约束库存永远不为负UPDATE dbo.drug_stock WITH (ROWLOCK) SET stock_qty stock_qty - quantity WHERE drug_id drugId AND warehouse_id warehouseId AND stock_qty quantity; IF ROWCOUNT 0 BEGIN RAISERROR(N库存不足发药失败, 16, 1); RETURN; END注意UPDATE条件里的stock_qty quantity这一行是整个语句的灵魂。当库存不足时UPDATE影响0行通过ROWCOUNT判断就能立即报错。如果有多个药房窗口并发发同一盒药数据库的行锁会串行化这些UPDATE后到的那次读到已被扣减的库存自然失败。这种方式比先查再用程序判断健壮得多。发药成功后还要回写处方明细表的dispense_flag和发药人、发药时间这一步要和上面的UPDATE放进同一个事务否则药出了窗口但电脑上没有记录月底盘库永远对不上。4. 项目落地源码结构、数据库初始化与本地运行4.1 源码目录结构与模块映射拿到源码先别急着编译按目录把工程对应关系理清后面调试会快很多。这套系统的解决方案结构如下HISSolution/ ├── HIS.Models/ # 实体与DTO │ ├── Entities/ │ └── DTO/ ├── HIS.DAL/ # 数据访问全部参数化SQL │ ├── PatientDAL.cs │ ├── RegisterDAL.cs │ ├── PrescriptionDAL.cs │ ├── ChargeDAL.cs │ └── DrugDAL.cs ├── HIS.BLL/ # 业务规则与事务边界 │ ├── PatientService.cs │ ├── RegisterService.cs │ ├── PrescriptionService.cs │ └── ChargeService.cs ├── HIS.Web/ # MVC站点 │ ├── Controllers/ │ ├── Views/ │ ├── Content/ │ └── Web.config ├── HIS.Common/ # 日志、加密、Session ├── database/ │ ├── 01_init.sql # 建库建表 │ ├── 02_base_data.sql # 科室、医生、药品 │ └── 03_procedures.sql # 存储过程 └── docs/ └── 设计说明.md对照第2章的架构来看Controllers里只做参数接收和视图返回Service方法里才是真正的业务规则DAL只负责和数据库打交道。调试时如果遇到数据没保存成功先看Service里有没有包事务如果遇到SQL语法报错直接去database目录看对应脚本而不是在C#代码里找字符串拼接。4.2 数据库初始化脚本顺序与基础数据数据库脚本的执行顺序是固定的先用SSMS或sqlcmd执行01_init.sql建库建表再执行02_base_data.sql写入基础数据最后执行03_procedures.sql创建存储过程。因为存储过程引用了表结构顺序颠倒会直接报错提示找不到对象。基础数据包括科室字典内科、外科、儿科、若干医生账号、药品字典和初始库存、管理员账号。首次登录后第一件事就是改默认密码这个坑几乎每个人都会踩。02脚本里药品库存既是字典又是库存表初始库存数据要从零开始逐条插入不能只插药品字典否则挂号、开方都能走通但到发药环节库存为0全部报库存不足。4.3 连接配置与本地调试Web.config里的连接字符串是全局的单点配置所有DAL类都从这里取connectionStrings add nameHISDb connectionStringServer.;DatabaseHISDB;User Idsa;Password你的密码;MultipleActiveResultSetstrue; providerNameSystem.Data.SqlClient / /connectionStringsServer.表示本机默认实例如果有命名实例写成ServerPCNAME\SQLEXPRESS。MultipleActiveResultSetstrue允许一个连接上开多个DataReaderMVC页面加载多个下拉框时避免连接池耗尽。本地调试优先用SQL Server身份验证不要用Integrated Security因为IIS应用程序池的账号和Visual Studio调试账号往往不一致容易出现不能登录这类让人误以为是代码问题的错误。调试技巧F5跑起来后先用admin登录系统管理模块给当前账号分配门诊角色的菜单权限再进挂号页面。因为这套系统的权限过滤是请求级别的角色没配菜单时页面能打开但接口会返回401不是代码坏了。4.4 部署到服务器IIS与局域网访问内网部署的步骤很固定我一般按这个顺序操作在HIS.Web上右键发布到文件夹IIS新建网站物理路径指向发布文件夹应用程序池选集成模式给网站绑定端口如8081目录浏览关闭防火墙放行8081端口内网其他机器用 http://服务器IP:8081 访问把Web.config里的Server.改为Server数据库服务器IP前提是网络和防火墙都打通如果服务器是Windows Server装IIS时记得勾选ASP.NET功能否则站点起来直接报500.19或500.21错误这是部署阶段最常见的坑。发布前还要检查Views目录是否包含预编译视图如果启用了Razor预编译发布时选预编译此站点选项避免首次访问页面时编译超时。5. HIS系统开发避坑指南门诊并发、结算误差与数据回滚5.1 一号多挂并发挂号导致号源超卖现象用并发工具同时发起多个挂号请求模拟50个线程抢同一个号源数据库里出现多条used_flag 1的排班记录。原因当时的实现是先查询排班状态再UPDATE两步之间没有锁两个事务同时读到 unused然后都执行了UPDATE。解决改为封装好的存储过程加UPDLOCK/HOLDLOCK提示让锁粒度精确到行。改动后同样的压测场景只剩一个成功其余返回号源已被占用。5.2 发药后库存变成负数现象药房两个窗口同时给患者发同一种药盘库时发现库存变成-2。原因发药代码先SELECT stock_qty判断大于0再执行UPDATE减1判断和更新的缝隙被另一个发药事务插入。解决UPDATE带上stock_qty quantity条件同时判断ROWCOUNT等于0就抛异常回滚。这个写法从根上杜绝了负库存比在C#代码里加lock更可靠。5.3 结算金额对不上账现象同一个处方在A窗收费是102.35元在B窗变成102.34元月底对账怎么都对不上。原因数据库金额字段用floatSQL计算时累加顺序不同导致浮点误差。解决金额列统一改成decimal(18,2)C#代码里的金额类型全部用decimal累加用Math.Round(value, 2, MidpointRounding.AwayFromZero)。改完再压测就稳定了。5.4 收费单据号重复现象月底财务对账出现两条bill_no完全相同的收费记录查日志发现两条记录生成时间差了不到一秒。原因单据号用SELECT MAX(bill_no)1生成两个并发事务同时查到了同一个最大值。解决引入bill_seq序列表用UPDATE取号原子自增。这个方案不需要额外建SQL Server SEQUENCE普通版数据库也能用。5.5 Session超时导致医生开方内容丢失现象医生写一处方超过20分钟点击保存被踢回登录页之前输入的内容全没了。原因系统默认Session超时时间是20分钟自定义过滤器检测到Session过期就跳登录页。解决Web.config里把SessionState超时调到60分钟同时在处方编辑页加一个每10分钟一次的隐藏请求保持Session续期。有条件的话在服务端草稿箱落库这样即使超时也不会丢内容。6. 二次开发思路从门诊模块扩展到住院与统计报表6.1 扩展方向从门诊延伸到住院与报表这套系统的核心链路止于门诊二次开发最自然的方向是住院模块。registrations表已经有status字段可以扩展为门诊和住院两种就诊类型住院医嘱复用prescription_items结构增加一个医嘱执行状态列就能接护士站的三查七对流程。报表系统则建议直接用存储过程聚合导出Excel不要在前端做复杂计算因为医院财务对数据口径的追溯要求很高。改造权限模型时把现有用户-角色模型扩展成用户-角色-菜单-科室的四层结构数据权限按dept_id过滤。这样住院护士只能看本科室病区门诊医生只能看自己的号源不需要改Controller代码只在ActionFilter里加上科室范围判断即可。6.2 一个具体技巧用序列表封装挂号锁号操作如果你要复用这套系统的并发控制思路最值得带走的是UPDATE取号这个技巧。无论是号源、单据号还是库存扣减只要涉及同一资源的并发写操作都可以用UPADTE带条件来实现原子操作。CREATE TABLE dbo.bill_seq ( seq_key VARCHAR(50) PRIMARY KEY, current_no INT NOT NULL );业务层调用时把取号和业务写入放进同一个事务。取号过程不要单独提交事务否则单据号会出现空洞财务对账时会问东问西。从那以后我每次拿到任何一套HIS源码都会先做三件事打开数据库看锁提示打开BLL看事务边界打开Controller看有没有直接拼SQL。这三步走完代码能不能接得住门诊高峰基本心里有数。这套系统的存储过程和事务设计做得相对完整跑一遍挂号到发药全流程你会对医疗信息系统的数据一致性有更具体的感知。希望帮到你。本文还有配套的精品资源点击获取