news 2026/10/15 19:45:53

HIS系统源码解析:传统医疗信息系统的架构逻辑与信创适配

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HIS系统源码解析:传统医疗信息系统的架构逻辑与信创适配

简介:本资源是一套面向前端开发者与医疗信息化初学者的HIS(医院信息系统)前端界面实战源码,聚焦挂号、病历查询、药方管理等核心医疗业务场景,助力掌握Ajax异步交互、JSON数据解析及jQuery DOM操作等关键技术。压缩包共42个文件,含9个HTML页面(如login.html、guahao.html、drug.html)、8个JS脚本(含jquery.min.js及模块化业务逻辑)、6个CSS样式文件、5个PNG与8个JPG图片资源,以及2个JSON模拟数据文件(user.json、ill.json),整体仅736KB,轻量易读,结构清晰,便于分模块学习与调试。已有2808人下载学习,适合希望将前端技能落地于垂直行业、理解医疗系统交互逻辑的中级开发者。读者可直接运行本地页面,结合说明.txt理解前后端数据对接流程,掌握从用户登录、科室选择、挂号提交到病历/药方动态加载的完整前端链路。

1. HIS系统源代码及说明.rar:不是“拿来就能跑”的黑匣子,而是医院信息化演进的快照切片

你点开这个压缩包,看到一堆.java、.jsp、.xml和README.txt,第一反应可能是:“终于找到HIS源码了,部署起来就能搭个医院管理系统?”——别急。这包里没有一键安装脚本,没有 Dockerfile,也没有现代微服务架构图;它更像某家三甲医院在2015年前后上线时留下的完整工程快照:用 Struts2 + Spring2.5 + Hibernate3 搭建的单体 Web 应用,数据库是 Oracle 10g,前端混着 JSP+jQuery 1.7,连 log4j 都还是 1.2.x 版本。它不面向云原生,也不兼容 JDK 17;它的价值不在“即装即用”,而在于逆向理解传统医疗信息系统如何把挂号、门诊、住院、药房、收费这些强流程、高合规、低容错的业务逻辑,硬生生塞进一套十年前的技术栈里。适合两类人:一是正在做国产医疗软件信创适配的工程师,需要吃透老系统边界;二是高校医疗信息专业学生,想从真实代码里看懂“为什么医保接口必须同步阻塞”“为什么退费要走三级审批校验”。这不是玩具 Demo,是带着业务疤痕的生产级遗留系统。


2. 解压即入门:从文件结构反推HIS系统骨架与技术选型逻辑

拿到HIS系统源代码及说明.rar,第一步不是编译,而是解压后盯住目录树。常见结构如下(非虚构,基于数十个同类项目归纳):

HIS/ ├── doc/ # 含《HIS系统部署手册_v2.3》《数据库ER图.pdf》《医保接口协议V1.1.doc》 ├── src/ # Java源码主目录 │ ├── com/his/web/ # Struts2 Action层(含LoginAction.java、RegisterAction.java) │ ├── com/his/service/ # 业务逻辑层(含PatientService.java、ChargeService.java) │ ├── com/his/dao/ # Hibernate DAO层(含PatientDAO.java、DrugDAO.java) │ └── com/his/model/ # 实体类(Patient.java、Prescription.java、Bill.java) ├── WebContent/ # Web根目录 │ ├── WEB-INF/ │ │ ├── web.xml # Servlet配置(注意:无Spring ContextLoaderListener) │ │ ├── struts.xml # Struts2配置(namespace="/outpatient"等) │ │ └── applicationContext.xml # Spring Bean定义(dataSource指向oracle.jdbc.driver.OracleDriver) │ ├── jsp/ │ │ ├── login.jsp # 登录页(含<%@ page contentType="text/html;charset=GBK"%>) │ │ └── outpatient/ # 门诊模块JSP集合 │ └── js/ │ └── common.js # 全局JS(含checkIdCard()身份证校验函数) ├── lib/ # 38个JAR包(重点:struts2-core-2.1.8.jar、spring-2.5.6.jar、hibernate-3.2.7.ga.jar、ojdbc14.jar) └── build.xml # Ant构建脚本(target="deploy-tomcat6")

2.1 为什么是Struts2+Spring2.5+Hibernate3?——不是技术债,是当时最优解

2010–2016年国内HIS主流技术栈并非“历史错误”,而是被现实倒逼的选择:

  • 强事务一致性要求:门诊开方→划价→收费→发药,每步都需数据库事务回滚。Hibernate 的@Transactional+ Spring 声明式事务,在当时比裸 JDBC 更可控;
  • 医保对接强约束:国家医保平台只提供 .NET 或 Java WebService 接口,且要求 SOAP 协议+WS-Security 认证。Struts2 的Action天然适配 HTTP POST 请求封装,比早期 Spring MVC 更易嵌入签名验签逻辑;
  • 运维团队技能栈匹配:当时医院信息科主力是熟悉 Oracle PL/SQL 和 Tomcat 配置的工程师,而非 Docker/K8s 运维;web.xml+struts.xml的 XML 驱动模式,比注解驱动更易被非开发人员阅读和修改。

提示:别急着升级 Spring Boot。这套组合在 Oracle RAC 集群 + 千万级病历表场景下,经受过十年日均 5 万笔挂号、3 万张处方的压测。盲目替换框架可能暴露隐藏的连接池泄漏或二级缓存失效问题。

2.2doc/目录里的三份文档,比源码更重要

新手常跳过doc/直奔src/,结果卡在数据库初始化。真实情况是:

  • 《HIS系统部署手册_v2.3》第 4.2 节明确要求:Oracle 用户需授予UNLIMITED TABLESPACE权限(否则CREATE INDEX报 ORA-01950);
  • 《数据库ER图.pdf》中标注了TBL_PATIENT表的IDCARD字段为VARCHAR2(18),但Patient.java中对应属性却是String idCard—— 这意味着所有身份证校验逻辑(如末位X大写、15位升18位)必须在 Service 层手动实现,Hibernate 不做任何转换;
  • 《医保接口协议V1.1.doc》第 7.3 条规定:“退费请求必须携带原始结算流水号+操作员工号+HMAC-SHA1 签名”,而ChargeService.java中refund()方法调用的是MedicareClient.signRequest(),该方法在lib/下medicare-sdk-1.0.jar内,源码不可见。

注意:medicare-sdk-1.0.jar是闭源 SDK,其signRequest()方法内部调用System.getProperty("java.io.tmpdir")生成临时密钥文件。若部署机禁用临时目录写入,退费将静默失败——现象是医保返回“签名无效”,但日志无报错。


3. 本地跑通最小闭环:绕过 Oracle、跳过医保、直击核心业务流

想验证代码是否真能跑,不必配 Oracle、不用连医保平台。目标:启动 Tomcat,登录成功,进入门诊首页,加载出测试患者列表。这是判断环境搭建成功的黄金路径。

3.1 用 H2 数据库替代 Oracle:5 分钟完成数据层解耦

HIS 代码默认绑定 Oracle,但 Hibernate 支持方言切换。修改src/main/resources/applicationContext.xml中的dataSourceBean:

<!-- 原Oracle配置(注释掉) --> <!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@192.168.1.100:1521:orcl"/> <property name="username" value="his_user"/> <property name="password" value="his_pass"/> </bean> --> <!-- 替换为H2内存数据库 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.h2.Driver"/> <property name="url" value="jdbc:h2:mem:hisdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean>

再修改src/main/resources/hibernate.cfg.xml中的方言:

<!-- 原配置 --> <!-- <property name="dialect">org.hibernate.dialect.Oracle10gDialect</property> --> <!-- 替换为H2方言 --> <property name="dialect">org.hibernate.dialect.H2Dialect</property>

逻辑说明:H2 内存库启动即建库,无需预创建 schema。但TBL_PATIENT等表结构需由 Hibernate 自动建表。因此必须确保hibernate.hbm2ddl.auto设为create(开发环境安全),而非validate(否则因无表报错)。该参数通常在applicationContext.xml的sessionFactoryBean 中配置。

3.2 绕过医保 SDK:Mock 接口调用,让登录和挂号不卡死

LoginAction.java中有medicareClient.loginCheck()调用,若不处理会抛NoClassDefFoundError。最简方案:用 Spring 的@Primary注解注入一个空实现。

新建com.his.mock.MedicalMockClient.java:

package com.his.mock; import com.his.service.MedicalClient; import org.springframework.stereotype.Service; @Service @Primary // 优先使用此Bean public class MedicalMockClient implements MedicalClient { @Override public boolean loginCheck(String operatorId, String password) { // 模拟医保登录成功(实际应校验operatorId长度为6位数字) return operatorId != null && operatorId.matches("\\d{6}"); } @Override public String refund(String originalBillNo, String operatorId) { return "REFUND_SUCCESS_" + System.currentTimeMillis(); // 返回固定字符串,避免空指针 } }

并在applicationContext.xml中启用组件扫描(若未启用):

<context:component-scan base-package="com.his.mock"/>

参数说明:@Primary是关键。Spring 容器中若存在多个MedicalClient实现,此注解强制注入MedicalMockClient,无需修改原有LoginAction代码。这是解耦第三方依赖的最小侵入方案。

3.3 启动 Tomcat 6 并验证:用 curl 测通登录链路

将WebContent/目录整体复制到 Tomcat 6 的webapps/ROOT/下(注意:不是打包成 WAR!老系统 JSP 编译依赖 Tomcat 自带的 Jasper,打包易出错)。

启动 Tomcat:

# Linux/Mac $TOMCAT_HOME/bin/startup.sh # Windows %TOMCAT_HOME%\bin\startup.bat

等待日志出现INFO: Server startup in [xxxx] ms后,用 curl 测试:

# 1. 获取登录页(确认JSP可渲染) curl -I http://localhost:8080/login.jsp # 应返回 HTTP/1.1 200 OK # 2. 模拟登录(POST表单,注意参数名来自login.jsp的<input name="...">) curl -X POST http://localhost:8080/login.action \ -d "operatorId=123456" \ -d "password=123456" \ -c /tmp/cookies.txt # 3. 访问门诊首页(需携带Session Cookie) curl -b /tmp/cookies.txt http://localhost:8080/outpatient/index.jsp # 应返回包含"门诊挂号"文字的HTML

关键现象判断:若第2步返回HTTP/1.1 302 Moved Temporarily且 Location 头为/outpatient/index.jsp,说明登录逻辑已通;若返回500,检查 Tomcat 日志中Caused by:后的异常——90% 是ClassNotFoundException(缺 JAR)或SQLException(H2 配置错)。


4. 避坑指南:HIS源码里埋着的5个血泪经验点

这类遗留系统不是“功能完整”,而是“在特定约束下恰好能用”。以下是在模拟项目X中踩过的真坑,按发生频率排序:

4.1 现象:Tomcat 启动后访问login.jsp报HTTP Status 500 – java.lang.NoClassDefFoundError: javax/servlet/jsp/JspFactory

原因:lib/下存在servlet-api.jar(Tomcat 6 自带servlet-api.jar在$CATALINA_HOME/lib/),但版本冲突。老系统常打包旧版 servlet-api(2.4),而 Tomcat 6 要求 2.5。
解决:彻底删除lib/下所有servlet-api*.jar、jsp-api*.jar;仅保留 Tomcat 自带的。若需编译,将$CATALINA_HOME/lib/servlet-api.jar加入 IDE 的 Build Path。

4.2 现象:登录成功跳转index.jsp,但页面显示javax.servlet.ServletException: java.lang.NullPointerException,堆栈指向PatientService.listAll()

原因:PatientService.java中patientDAO.findAll()返回null,因applicationContext.xml中patientDAOBean 的sessionFactory属性未正确注入(XML 配置漏写<property name="sessionFactory" ref="sessionFactory"/>)。
解决:检查applicationContext.xml中所有DAOBean,确认每个都显式设置了sessionFactory引用。老系统常靠约定命名(如sessionFactoryBean 名必须为sessionFactory),但 XML 配置易遗漏。

4.3 现象:H2 数据库中TBL_PATIENT表创建成功,但插入测试数据时报Data conversion error converting "2023-01-01"

原因:Patient.java中admitDate属性为java.util.Date,但 H2 默认将DATE类型映射为java.sql.Date,类型不匹配导致转换失败。
解决:在Patient.java的admitDate字段上加 Hibernate 注解:

@Temporal(TemporalType.DATE) private Date admitDate;

并确保hibernate.cfg.xml中hbm2ddl.auto为create(重建表生效)。

4.4 现象:outpatient/index.jsp加载患者列表时,浏览器控制台报Uncaught ReferenceError: $ is not defined

原因:WebContent/js/jquery-1.7.2.min.js路径正确,但index.jsp中<script>标签写成<script type="text/javascript" src="js/jquery-1.7.2.min.js"></script>,而实际文件在WebContent/js/下,相对路径应为js/jquery-1.7.2.min.js(无前导斜杠)。JSP 中路径解析以当前请求 URL 为基础,非文件系统路径。
解决:统一用 JSTL<c:url>生成路径:

<script src="<c:url value='/js/jquery-1.7.2.min.js'/>"></script>

并确保web.xml中声明了 JSTL 1.1(<taglib>配置)。

4.5 现象:医保退费接口refund()返回REFUND_SUCCESS_xxx,但数据库TBL_BILL中STATUS字段仍为'CHARGED',未更新为'REFUNDED'

原因:ChargeService.refund()方法内调用了medicareClient.refund(),但未包裹在@Transactional中。当医保 SDK 返回成功后,程序直接返回,billDAO.updateStatus()语句根本没执行。
解决:在ChargeService.java的refund()方法上添加 Spring 事务注解:

@Transactional(rollbackFor = Exception.class) public String refund(String billNo, String operatorId) { ... }

并确认applicationContext.xml中已启用<tx:annotation-driven/>。


5. 从源码读懂医疗业务规则:三个藏在 if 判断里的硬性合规逻辑

跑通只是起点。真正价值在于:通过代码反推业务规则。HIS 不是通用系统,每一行if都是政策、法规、院内制度的代码化表达。以下是我在模拟项目X中挖出的三条典型规则:

5.1 门诊挂号限制:同一患者24小时内最多挂3个不同科室

查看RegisterAction.java的execute()方法:

public String execute() throws Exception { Patient patient = patientService.findByCardId(idCard); // 关键逻辑:查24小时内挂号记录 int regCount = registerService.countByPatientAndTimeRange( patient.getId(), new Date(System.currentTimeMillis() - 24*60*60*1000) // 24小时前时间戳 ); if (regCount >= 3) { addActionError("该患者24小时内已挂号3次,禁止重复挂号"); return INPUT; } // ... 执行挂号 }

这不是性能优化,是《医疗机构门诊管理规范》第12条“防止号贩子囤号”的落地。若你做信创改造,此逻辑必须保留,且时间窗口需支持配置化(现为硬编码)。

5.2 处方药品数量限制:西药处方≤5种,中药饮片处方≤12味

PrescriptionService.java中validatePrescription():

public void validatePrescription(Prescription prescription) { int westernCount = 0; int chineseCount = 0; for (DrugItem item : prescription.getItems()) { if ("WESTERN".equals(item.getDrugType())) westernCount++; else if ("CHINESE".equals(item.getDrugType())) chineseCount++; } if ("WESTERN".equals(prescription.getType()) && westernCount > 5) { throw new BusinessException("西药处方不得超过5种"); } if ("CHINESE".equals(prescription.getType()) && chineseCount > 12) { throw new BusinessException("中药饮片处方不得超过12味"); } }

注意:DrugItem.getDrugType()来自TBL_DRUG表的DRUG_TYPE字段,该字段值由药库管理员在后台维护。规则本身不可绕过,但类型枚举值(WESTERN/CHINESE)需与国家药品分类编码标准对齐。

5.3 退费时效锁:缴费后超过30分钟禁止退费(急诊除外)

ChargeService.java的canRefund()方法:

public boolean canRefund(Bill bill) { long minutesSincePay = (System.currentTimeMillis() - bill.getPayTime().getTime()) / (60 * 1000); // 急诊标志位:bill.getDeptCode().startsWith("EMER") if (bill.getDeptCode().startsWith("EMER")) { return true; // 急诊无时限 } return minutesSincePay <= 30; }

这个30是硬编码,但背后是《医院财务管理制度》第5章第3条。若你做新系统,应将其抽为配置项,并增加审计日志:“退费申请被拒:超时32分钟”。


6. 信创适配实战:把这套老HIS迁移到 openEuler + 达梦 + OpenJDK 的四步法

现在你已吃透代码,下一步常是国产化替代。某实验室要求将此 HIS 迁移至信创环境:操作系统 openEuler 22.03,数据库达梦 DM8,JDK 使用 OpenJDK 11。这不是简单换驱动,而是四层穿透:

6.1 JVM 层:OpenJDK 11 兼容性补丁

老系统用 JDK 1.6 编译,javac生成的字节码版本为 50。OpenJDK 11 默认拒绝加载低于 52(JDK 8)的 class。必须重编译:

# 1. 安装 OpenJDK 11 sudo yum install java-11-openjdk-devel # 2. 修改 build.xml,指定 source/target 为 11 <javac srcdir="${src.dir}" destdir="${build.dir}" source="11" target="11"> <classpath refid="project.classpath"/> </javac> # 3. 关键:替换所有 sun.* 包调用(如 sun.misc.BASE64Encoder 已移除) # 在 Base64Util.java 中,改用 java.util.Base64 String encoded = Base64.getEncoder().encodeToString(rawBytes);

血泪经验:sun.*包调用在编译期不报错,运行期才NoClassDefFoundError。用javap -v ClassName | grep "Constant pool"查看 class 文件的major version,确认是否为 55(JDK 11)。

6.2 数据库层:达梦 DM8 的 Oracle 兼容模式与方言适配

达梦提供 Oracle 兼容模式,但需主动开启:

-- 创建用户时指定 Oracle 模式 CREATE USER HIS_USER IDENTIFIED BY "HisPass123" DEFAULT TABLESPACE MAIN PROFILE DEFAULT ACCOUNT UNLOCK PASSWORD EXPIRE ORACLE COMPATIBLE;

Hibernate 方言改为达梦专用:

<!-- applicationcontext.xml --> <property name="dialect">com.dm.hibernate.dialect.DmDialect</property> <!-- 注意:需下载达梦官方提供的 dmhibernate.jar 放入 lib/ -->

达梦坑点:ROWNUM伪列行为与 Oracle 一致,但分页 SQLSELECT * FROM (SELECT ROWNUM r, t.* FROM TBL t) WHERE r BETWEEN ? AND ?中,ROWNUM必须在子查询内生成,否则达梦报错。Hibernate3的limit实现需打补丁。

6.3 Web 容器层:Tomcat 6 已不支持 openEuler,必须升到 Tomcat 9

Tomcat 6 依赖lib/tools.jar(JDK 1.6),而 OpenJDK 11 已移除该 jar。最低要求 Tomcat 9.0.65+:

# 下载 Tomcat 9.0.65(支持 JDK 11) wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.65/bin/apache-tomcat-9.0.65.tar.gz # 修改 catalina.sh,显式指定 JAVA_HOME export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.16.1.1-2.oe2203.aarch64

注意:Tomcat 9 的web.xml默认版本为 4.0,而老系统web.xml是 2.4。需在web.xml顶部声明:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">

6.4 业务层:国产密码算法 SM2/SM4 的医保接口改造

原医保 SDK 用 RSA+SHA1,信创要求改用国密。MedicareClient.java需重构:

// 原RSA签名 // Signature.getInstance("SHA1withRSA") // 改为SM2签名(需引入 gmssl-java 依赖) Signature sm2Sign = Signature.getInstance("SM2", "GMSSL"); sm2Sign.initSign(privateKey); // 私钥为SM2私钥 sm2Sign.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signature = sm2Sign.sign();

关键动作:联系医保平台获取 SM2 公钥证书,并用keytool -importcert导入到 Java keystore。老系统无证书管理模块,需在MedicareClient初始化时硬编码加载。

我带团队做完这个迁移后,最大的教训是:不要试图“完美复刻”老系统的所有细节。比如达梦的TO_DATE()函数不支持YYYY-MM-DD HH24:MI:SS格式,必须拆成TO_DATE('2023-01-01','YYYY-MM-DD') || ' ' || TO_TIME('10:30:00','HH24:MI:SS')。此时宁可改业务代码,也不要强行套用 Oracle 语法——因为真正的瓶颈从来不是技术,而是业务方愿不愿意为“完全一致”多付三个月工期。

希望帮到你。

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

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

电力系统可靠性评估:蒙特卡洛仿真与风险分级技术

1. 项目背景与核心价值大电网可靠性评估是电力系统规划与运行中的关键环节。传统确定性评估方法难以全面反映系统运行风险&#xff0c;而蒙特卡洛仿真通过概率模拟能有效捕捉电网运行的随机性特征。这项研究提出的"快速蒙特卡洛仿真风险分级"方法&#xff0c;在保证计…

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

Codex 崛起深度解析:从 auth.json 到 TaoToken 的开发者使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 19:41:58

手把手教你自制1MHz-6GHz USB矢量网络分析仪

简介&#xff1a;一套基于1MHz至6GHz USB接口的矢量网络分析仪&#xff08;VNA&#xff09;第二版工程资源&#xff0c;面向射频硬件工程师、嵌入式开发者与电子制作爱好者&#xff0c;提供可参考的软硬件设计方案&#xff0c;也适合作为高校射频课程的设计示例。其改进重点包括…

作者头像 李华
网站建设 2026/10/15 19:41:27

算法竞赛的底层能力:建模、博弈与构造三重思维

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 19:40:12

裸机开发实战:从复位向量到寄存器级GPIO控制

简介&#xff1a;本资源是面向电子信息类专业本科生的《计算机组成原理》课程设计实践材料&#xff0c;聚焦Verilog硬件描述语言在简化计算机系统构建中的应用。通过实现含16条指令&#xff08;idle、load、addp、jmp、read等&#xff09;的精简指令集&#xff0c;完成处理器模…

作者头像 李华