简介:一套完整的医院信息系统(HIS)源码包,面向医疗信息化开发者、系统维护人员及需要二次集成的技术团队。资源基于Delphi VCL组件库构建,覆盖药房管理、护士站、药库管理、住院结算等核心业务模块,同时包含数据库脚本、配置文件与系统维护文档,可帮助理解医院业务流程与信息系统架构。包内共4290个文件,以pas源文件(1993个)和dfm窗体文件(1875个)为主,另有dpr工程文件、res资源、sql数据库脚本及少量辅助文件,整体压缩包仅6.51MB,结构紧凑。已有2596人学习浏览。对于希望研究早期HIS系统设计或进行功能扩展的开发者,这套源码提供了完整的模块划分、数据组织与界面实现参考,药房库存、护士站医嘱到住院结算的代码逻辑清晰,配套文档和调试工具也有助于快速上手和排错。
1. HIS 系统:一套完整源码包和演示版的差别,不止是多几张业务表
一套完整的 his 系统,对做医疗信息化或者高校实训的人来说,是拿来即用的基线资产。它不只是能登录、能挂号的演示工程,而是覆盖门诊、收费、药房、住院、统计五块业务,带数据库脚本、部署文档和可运行源码的成套代码包。这篇文章从模块边界、建表设计、部署参数、上线排查和验收方法五个方向拆开讲,重点放在文档里不会写、只有真跑过才懂的细节上。适合正在选型、需要一份可二次开发基线代码的开发者,也适合带学生做医疗类毕设的指导老师。
2. 模块划分:从门诊动线到住院闭环,业务边界怎么定
HIS 系统的复杂度从来不在单张表或单个接口上,而在于模块之间数据怎么交接、状态怎么流转。边界画错,后期改造成本成倍上涨;边界画对,后续加需求只是加表和加接口的问题。这一章先说门诊主线,再说住院和药房的联动,两条线理清楚,整套资源的模块地图就有了。
2.1 门诊主线:挂号、收费、医生站的数据交接点
门诊是 HIS 里最高频的业务场景,典型流程是患者建档、挂号、医生开方、收费、药房发药。这套资源里的实现方式,是用挂号单状态、处方状态、账单状态三级状态字段来串联,模块之间不允许互相直改对方的表。
挂号模块的核心职责,是把“患者、科室、医生、号源”四者的关系落成一条挂号单记录;收费模块只认处方金额,负责写账单和支付流水;医生工作站只写诊断和医嘱,不碰任何费用字段。判断边界是否清晰有个土办法:如果一个模块的操作里出现了修改别家业务状态字段的代码,那就是边界溢出了。
挂号登记的核心逻辑通常长这样:
@Transactional(rollbackFor = Exception.class) public Long register(RegisterRequest request) { // 1. 先按身份证查患者档案,存在则复用,不存在则新建档案 Patient patient = patientMapper.selectByIdCard(request.getIdCard()); Long patientId; if (patient == null) { Patient created = Patient.buildFrom(request); patientMapper.insert(created); patientId = created.getId(); } else { patientId = patient.getId(); } // 2. 生成挂号单,初始状态 0 表示待就诊 Register register = Register.builder() .patientId(patientId) .deptId(request.getDeptId()) .doctorId(request.getDoctorId()) .regFee(request.getRegFee()) .status(0) .build(); registerMapper.insert(register); // 3. 扣减号源,乐观锁防止并发下超挂 int affected = scheduleMapper.consumeOne(request.getScheduleId(), request.getVersion()); if (affected == 0) { throw new BizException("号源已被占用,请刷新后重试"); } return register.getId(); }这段代码最值得留意的就是第 3 步。号源扣减不用先查再改,而是用条件更新加乐观锁的方式一次性完成,affected 为 0 就说明别人抢先了。对 HIS 这种短事务、并发集中在号源和库存上的场景,乐观锁比 SELECT FOR UPDATE 更合适,不会把行锁一直拖到事务结束。
参数说明:request.deptId 对应科室表 base_dept 的主键;request.version 不是业务字段,是专门做并发控制的版本号,每次号源被占用后自增。如果工程里没有 version 字段,退而求其次可以用剩余号数做条件,但前提是号源表必须有索引,否则条件更新会走全表扫描,高峰期直接拖垮数据库。
另一个值得注意的坑是患者建档。很多 HIS 在身份证号为空的情况下会重复建档,后续医保上传时就翻车。常见做法是身份证号非空时强制唯一索引,空值时用手机号加姓名做辅助查重。这套资源在 base_patient 表上同时建了 id_card 唯一索引和 name+phone 联合索引,属于比较稳妥的做法。
2.2 住院闭环:医嘱执行与药房发药的联动机制
住院部分是 HIS 里最容易写乱的地方。门诊是“开方即发药”的简单链路,住院则要从医嘱开始:长期医嘱还是临时医嘱,护士什么时候执行,执行状态怎么推给药房,退药从哪里退。这套资源的处理方式是引入“预扣”和“实扣”两段式库存联动。
-- 住院发药:先预扣可用库存,发药确认时再转成实扣 UPDATE biz_drug_stock SET locked_qty = locked_qty + #{num} WHERE drug_id = #{drugId} AND stock_qty - locked_qty >= #{num}; -- 发药确认,把预扣数量转成真实扣减 UPDATE biz_drug_stock SET stock_qty = stock_qty - #{num}, locked_qty = locked_qty - #{num} WHERE drug_id = #{drugId};第一条 SQL 是预扣,把待发数量加进 locked_qty 里锁定,条件 stock_qty - locked_qty >= #{num} 保证不会把别人的库存锁超。第二条 SQL 是实扣,护士确认发药后,库存真实减少,锁定量同步释放。如果医嘱取消,就执行一条反向 SQL 把 locked_qty 减回去。
为什么不在医嘱开立时就扣库存?因为住院医嘱从开立到执行之间往往隔着几个小时的护士核对时间,提前扣会把可售库存全部锁死,药房根本没法做其他调配。预扣锁定的是“留给这个床位的量”,实扣才是真正从库存里扣除。两段式设计在 HIS 里很常见,我自己接手过好几个住院模块,凡是图省事直接扣库存的,最后都逃不过退药时账实不平的问题。
这里要注意一个细节:锁定数量用单独字段 locked_qty,而不是在 stock_qty 上做减法。原因很简单,退药、过期报废、盘点调整都需要知道“当前真实可用量”和“被占用待发量”各自是多少。把两个数值混在一个字段里,盘点和财务对账都会很难受。
3. 数据库设计:基表、业务表、日志表的分层,统计口径先定死
HIS 这类系统里,很多线上问题其实从表结构就已经埋下了。业务表和日志表混在一起,统计查询拖着在线事务一起慢;主键策略没想好,并发生成单据时直接死锁。这一章把数据字典设计、建表规范和统计口径掰开讲,看完再对照这份资源的数据库脚本去读,很多表为什么这么建就一目了然了。
3.1 数据字典:用表前缀约东读写权限和生命周期
这套资源里的表是严格按前缀分层的,这也是 HIS 工程一贯的约定。基础资料表、业务流水表、日志表各自管各自的事情,谁也别跨层去写别人的数据。
| 层级 | 表前缀 | 代表表 | 读写特征 | 生命周期 |
|---|---|---|---|---|
| 基础资料 | base_ | base_dept、base_doctor、base_drug | 低频变更,只读为主 | 长期保留 |
| 业务流水 | biz_ | biz_register、biz_prescription、biz_bill | 高频写入,禁止物理删除 | 按年归档 |
| 日志审计 | log_ | log_oper、log_login | 只追加,不做修改 | 按月清理归档 |
这个分层不是风格问题,是运维底线。业务流水表一旦混入日志字段,审计逻辑就会和在线事务纠缠在一起,后期想做读写分离都不知道该按什么规则拆表。操作日志单独落表,按月归档,既不影响业务查询,也能满足医疗系统的审计追溯要求。
以挂号流水表为例,完整的建表语句长这样:
CREATE TABLE biz_register ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '挂号单ID', patient_id BIGINT NOT NULL COMMENT '患者档案ID', dept_id BIGINT NOT NULL COMMENT '科室ID', doctor_id BIGINT NOT NULL COMMENT '接诊医生ID', register_type TINYINT NOT NULL DEFAULT 1 COMMENT '1普通 2专家 3急诊', reg_fee DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '挂号费用', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待就诊 1已就诊 2已退号 3作废', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_dept_create (dept_id, create_time), KEY idx_patient (patient_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='挂号流水表';注意 status 用的是 TINYINT 而不是 VARCHAR,这套资源里所有状态字段都用数字编码,含义靠字典表 dict_item 解释。好处是代码里能用枚举映射,SQL 里能走整数比较,比字符串状态字段省空间而且不易写错。代价是每次写查询都要联字典表才能看出状态含义,对于刚接手的人会有点不习惯,但这确实是 HIS 的老传统。
索引策略上有两个值得说的点。idx_dept_create(dept_id, create_time) 是覆盖了“按科室统计某段时间挂号量”这个高频查询;idx_patient(patient_id) 则是为了根据患者档案回溯历史挂号记录。这两个索引是挂号场景里缺一不可的,少了任何一个,报表接口迟早被慢查询拖死。
3.2 统计口径:biz_date 字段不能省,退费归属先定死
HIS 的统计模块是争议最多的地方,尤其是退号、退费的时间归属问题。比如昨晚 23:40 挂的号今天退掉,这笔费用算昨天的营收还是今天的?这套资源的答案很明确:一切以业务日期 biz_date 为准,而不是 create_time。每张账单、每笔流水都带一个 biz_date 字段,记录业务实际发生的归属日期,统计查询只认它。
-- 按业务日期归集门诊收费金额,bill_type 为 1 记收入,2 记退费 SELECT biz_date, pay_type, SUM(CASE WHEN bill_type = 1 THEN pay_amount ELSE 0 END) AS income, SUM(CASE WHEN bill_type = 2 THEN pay_amount ELSE 0 END) AS refund FROM biz_bill WHERE biz_date = #{date} GROUP BY biz_date, pay_type;这里最关键的点在于 biz_date 和 create_time 是两回事。create_time 是数据库落库时间,由系统自动生成;biz_date 是业务归属时间,由业务层显式赋值。举个实际场景:门诊收费跨天操作时,系统时间是 00:05,但患者就诊发生在昨天 23:50,这笔费用应归属到昨天,这时候只有显式的 biz_date 才能保证统计不漂移。
我在实际项目里见过不少团队为了省事,直接拿 create_time 做统计维度,报表系统跑出来的数跟收费员手工台账总是差几十块钱,查到最后都是跨天边界的问题。所以看一份 HIS 数据库脚本靠不靠谱,先看业务流水表有没有 biz_date,没有的话基本可以判断统计模块会翻车。
3.3 主键策略:雪花 ID 还是自增 ID,要看归档方式
这套资源里业务表主键全部用 BIGINT 自增。很多新入行的人觉得分布式环境下应该用雪花 ID,但 HIS 这种单体起步、后续可能做分库的系统,自增主键配合 InnoDB 聚簇索引的写入性能是最好的,归档时按主键范围切割也最方便。真要到了分库分表的规模,再做 ID 改造也不迟,不要一开始就背上复杂性。
一句话总结本章:读这套资源的数据库脚本时,按前缀分块读,重点关注 status 字段的字典映射和 biz_date 的赋值逻辑,这两个点搞清楚了,整个系统的数据流转就通了。
4. 部署落地:JDK、MySQL 字符集与连接池参数的调整顺序
从拿到源码包到真正跑起来,中间至少有一个下午的时间消耗在环境问题上。HIS 系统部署本身不难,难的是那些参数配置错了之后的表现特别隐蔽。这一章按我平时部署这类资源的标准流程写,一步步来能省掉不少无头绪的排查时间。
4.1 环境准备:JDK 版本、MySQL 字符集与应用配置
这套资源是按 JDK 1.8 及以上、MySQL 5.7 或 8.0 来设计的。启动前先确认几个硬性条件:MySQL 版本不低于 5.7,数据库默认字符集是 utf8mb4 而不是 utf8。医疗系统里患者姓名经常带生僻字,utf8 装不下,用 utf8mb4 是底线。
以 Spring Boot 工程为例,核心配置长这样:
spring: datasource: url: jdbc:mysql://localhost:3306/hisdb?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: his_app password: his_app_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 max-lifetime: 1800000 connection-test-query: SELECT 1 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true连接串上三个参数缺一不可。useUnicode=true 和 characterEncoding=utf8mb4 保证中文和生僻字不出现乱码;serverTimezone=Asia/Shanghai 是为了让 JDBC 驱动和 MySQL 服务器的时区一致,否则隐蔽的日期偏移问题会在凌晨统计时冒出来,这种时区玄学问题排查起来最耗人。
Hikari 连接池参数里,maximum-pool-size 设成 20 是基于一个经验判断:MySQL 单实例的可控并发连接数一般建议在 50 以内,应用侧留 20 个连接,再加上运维工具和其他服务的连接,不至于把数据库连接数打满。如果你的部署环境是 4 核 8G 的小机器,我建议把 maximum-pool-size 调到 10,minimum-idle 调到 3,连接数并不是越大越好。
4.2 初始化脚本:建库顺序和外键检查的坑
数据库初始化看起来就是导入 SQL,但顺序错了会卡在创建索引失败这种莫名其妙的错误上。这套资源的脚本分四个文件,导入顺序是固定的:
# 建库,指定 utf8mb4,否则后面导入表结构会继承默认字符集 mysql -h127.0.0.1 -uroot -p -e "CREATE DATABASE hisdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 先导入表结构,再导字典,最后导初始数据 mysql -h127.0.0.1 -uroot -p hisdb < 01_schema.sql mysql -h127.0.0.1 -uroot -p hisdb < 02_dict.sql mysql -h127.0.0.1 -uroot -p hisdb < 03_data.sql顺序乱掉的典型症状是 03_data.sql 里引用字典表时表还没建好,报出 record not found 这类和外键相关的错。另外,如果 01_schema.sql 里有外键约束,导入前最好加上 SET FOREIGN_KEY_CHECKS=0,导入完成后再恢复为 1,不然数据导入顺序稍微不一致,外键校验就会先挡住一批。
4.3 应用启动与访问路径:先看登录接口再谈业务功能
启动应用后先别急着打业务接口,第一步是确认登录和权限链路是通的。HIS 系统的权限模型通常是用户、角色、菜单、接口四级,登录成功后返回 token,后续请求都在 Header 里带。先登录一次,再拉一次菜单列表,能通就说明权限表初始化没问题,后面调业务接口时遇到的 401、403 就是参数问题而不是配置问题。
我一般会先用 curl 做一次最简验证,确认服务起来后响应是 JSON 而不是 404 或 500:
curl -X POST http://localhost:8080/api/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"his@demo"}'返回里有 token 字段就是正常的,如果直接报连接拒绝,先去查应用日志里端口是否被占用,而不是怀疑代码有问题。HIS 这种老体系工程最常见的部署坑不是版本不兼容,而是端口占用和配置里数据库密码带了特殊字符没转义,这两类问题看着吓人,实际修复只要几分钟。
5. 上线排查:四个高频故障的现象、根因和修复顺序
部署能启动只是第一步,真正折腾人的是在模拟业务压力时暴露出来的问题。这一章把我踩过的、以及圈子里高频出现的四个 HIS 故障写出来,每条按现象、原因、解决的顺序讲,基本覆盖了上线初期的典型翻车场景。
5.1 故障一:高峰时段收费单据丢单
现象:高峰期收费窗口反馈,患者已经扫码支付成功,但收费系统里查不到这笔账单,重新查询又出现,时有时无。原因:支付回调里的更新逻辑和收费主事务用了 REQUIRES_NEW 传播级别,回调先提交,随后主事务回滚,导致账单最终丢失。解决:把支付回调的账单更新并入收费主事务,或者在回调落一张独立的支付回调日志表,异步再更新账单状态。我一般推荐后一种,回调日志表写成功后立即返回成功,异步任务做状态补偿,既保证不丢单,也不会拖慢支付链路的响应时间。
5.2 故障二:药房库存被扣成负数
现象:库存管理页里某药品库存数量变成负数,但发药记录显示一切正常。原因:两个发药窗口并发操作同一药品时,代码用的是先查库存再更新,两条线程同时读到剩余 5 盒,各自扣了 3 盒,最后库存变成 -1。解决:把所有库存扣减改成条件更新,UPDATE ... SET stock_qty = stock_qty - #{num} WHERE drug_id = #{id} AND stock_qty >= #{num},affected 为 0 时直接提示库存不足,不做重试。同时建议加一张库存流水表,每次变动都留档,出现负数也能快速定位是哪一笔造成的。
5.3 故障三:跨天营收统计前后对不上
现象:日报表里某天的营收金额,和收费窗口当天实际收到的金额始终有几十块的误差,而且误差都发生在凌晨前后。原因:系统里存在两处时间来源,一处取 MySQL 的时间,一处取 JVM 的时间,时区配置不一致导致跨天时产生偏移;更常见的是统计 SQL 直接用 create_time 作为过滤条件,而跨天的业务单创建时间已经跳到第二天。解决:统一应用服务器和数据库服务器的时区配置,统计查询一律按 biz_date 字段过滤,时间边界用 >= #{start} AND < #{end} 的半开区间写法,杜绝 00:00 整点的边界丢数据问题。
5.4 故障四:同一患者重复建档
现象:同一患者挂三次号,系统里出现三条患者档案,身份证号完全一样,医保接口调用时报“身份证重复”。原因:建档接口没有对身份证号做唯一约束,应用层也是先查后插,并发场景下两条请求同时查不到档案,就同时插了两条。解决:base_patient 表的身份证字段加唯一索引,身份证为空的场景单独处理,用手机号和姓名做联合查重;应用层查重的代码保留,但只作为友好提示,兜底的是数据库唯一索引。
6. 接手验收:三条数据校验和一个冒烟接口,快速判断能否二次开发
拿到一套 HIS 资源,我不会急着逐张表看代码,而是先执行三条数据校验 SQL,再打一次冒烟接口,几分钟就能判断这套东西是空壳还是真能落地。
第一条校验,查基础字典表是否存在初始数据:
SELECT COUNT(*) FROM base_dept; SELECT COUNT(*) FROM base_drug;两张表返回结果为零,说明初始化数据没导全,后面业务操作全都会因为找不到有效科室和药品而报错。
第二条校验,查处方和账单是否对平:
-- 找出已就诊但没有对应收费账单的处方,正常应为空 SELECT p.id FROM biz_prescription p LEFT JOIN biz_bill b ON b.prescription_id = p.id WHERE b.id IS NULL AND p.status = 1;如果这套资源自带演示数据,这条查询应该返回空集,否则说明业务流转链路有断点,医生开了方却没法收费,这是 HIS 里最严重的逻辑缺陷。
第三条校验,看库存是否被扣成负数:
SELECT * FROM biz_drug_stock WHERE stock_qty < 0;返回结果不为空,说明初始化脚本里造的数据不干净,后续演示时会在药房模块翻车。
数据校验通过后,再打一次冒烟接口,确认登录和患者查询链路通着就行:
curl -X POST http://localhost:8080/api/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"his@demo"}' \ | python3 -m json.tool拿到 token 后调一次患者查询接口,能正常返回 JSON 就说明 Web 层、Service 层、Mapper 层、数据库整条链路都是通的,可以放心进入业务验证阶段。
从那以后我每次拿到 HIS 相关资源,都会强制自己先跑完这三条 SQL 和一次冒烟接口,再往下碰业务代码。这个习惯帮我挡掉过不少表面上完整、实际上初始化脚本都不完整的资源包,也让后续的二次开发少走很多弯路。希望这份拆解能帮你在自己的项目里少踩几个坑。
本文还有配套的精品资源,点击获取