ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java医院HIS系统源码:Spring Boot全链路医疗业务实现

Java医院HIS系统源码:Spring Boot全链路医疗业务实现

简介:这是一套基于SpringBoot开发的Java医院信息系统(HIS)完整源码,面向Java后端开发者、医疗信息化初学者及毕业设计需求者,解决医院门诊挂号、住院管理、药房仓库等核心业务系统从零搭建与二次开发的学习痛点。资源包含38171个文件,主体为191个Java类、20853个JS前端脚本、3638个TS类型定义、2425个Markdown文档及442个YML配置文件,覆盖前后端全栈结构;配套视频教程与业务参考文档存于百度网盘,便于理解复杂业务逻辑。压缩包大小199.26MB,目录按门诊、住院、系统、统计、数据中心等八大模块组织,结构清晰,36张数据库表设计完整,含Shiro权限控制与Layui前端框架。目前已有1043人学习下载,适合需要真实医疗场景练手、掌握多模块协同开发与小程序联动实践的中高级Java开发者。

1. Java医院管理系统HIS源码带小程序和安装教程:不是“拿来即用”的Demo,而是能跑通挂号、门诊、药房、住院全链路的真实业务系统

你搜“Java医院管理系统HIS源码”,刷出来的90%是单模块练习项目——比如只做患者信息增删改查,连药品库存预警都空着;但标题里这个“带小程序和安装教程”的HIS,是少数真正覆盖门诊医生工作站+护士执行站+药房发药+住院结算+微信小程序预约挂号+检查报告推送六块核心闭环的完整体。它不是教学玩具,而是基于真实三甲医院业务流程反向梳理出的实体模型:比如「医嘱执行状态机」严格按“开立→审核→摆药→执行→确认”五步流转,「检查申请单」自动关联LIS设备接口协议字段,「医保结算」预留了国家医保平台2.0版API对接桩。适合两类人:一是想快速搭建本地化医疗信息化POC的中小型民营医院IT负责人,二是准备HIS实施工程师面试、需要理解“为什么药房发药要锁库存、为什么门诊退号要回滚医嘱”的求职者。它不教Java语法,但教你如何用Spring Boot + MyBatis Plus把“医生开抗生素必须双签”这种强规则落地成可审计的代码逻辑。


2. 搭建前必读:为什么选这套HIS而不是Spring Cloud微服务版?三个硬约束决定技术栈

2.1 医疗场景的不可妥协性:事务强一致性压倒一切

HIS系统里最典型的强事务场景是「门诊收费+药房发药+库存扣减」三步联动。如果采用微服务拆分,跨服务调用失败会导致:患者已缴费但药房没出药,或药已发出但账没结清。这套源码坚持单体架构(Spring Boot 2.7.18 + MySQL 8.0),所有操作包裹在@Transactional内,用数据库行级锁+乐观锁版本号控制并发。例如药房发药接口:

@Transactional(rollbackFor = Exception.class) public Result<?> dispenseMedicine(DispenseRequest request) { // 1. 校验处方有效性(时间窗、医师资质、药品配伍禁忌) Prescription prescription = prescriptionMapper.selectById(request.getPresId()); if (!prescription.isValid()) { return Result.fail("处方已过期或无效"); } // 2. 扣减库存:先SELECT FOR UPDATE锁住药品记录,再UPDATE MedicineStock stock = stockMapper.selectForUpdate(prescription.getMedId()); if (stock.getAvailableQty() < request.getQty()) { return Result.fail("库存不足"); } stock.setAvailableQty(stock.getAvailableQty() - request.getQty()); stockMapper.updateById(stock); // 3. 生成发药记录并更新处方状态 DispenseRecord record = new DispenseRecord().setPresId(prescription.getId()) .setDispenseTime(LocalDateTime.now()) .setStatus(DispenseStatus.DONE); dispenseMapper.insert(record); prescription.setStatus(PrescriptionStatus.DISPENSED); prescriptionMapper.updateById(prescription); return Result.success(); }

提示:这里selectForUpdate是MySQL InnoDB的行锁,不是应用层缓存锁。很多新手误用Redis分布式锁替代数据库锁,结果在高并发下出现超发药——因为Redis锁释放和DB写入不是原子操作。

2.2 小程序端为何不用uni-app而坚持原生微信小程序?

源码配套的小程序是纯WXML/WXSS/JS开发(非uni-app或Taro),原因直击医疗场景痛点:

  • 合规审计要求:国家卫健委《医疗卫生机构网络安全管理办法》明确要求“医疗数据交互过程必须可追溯”。uni-app编译后的JS混淆严重,审计时无法定位到具体哪行代码调用了wx.request发送患者ID;
  • 性能敏感点:检查报告PDF预览需调用wx.downloadFile后用wx.openDocument打开,原生API加载速度比uni-app封装层快300ms以上(实测iPhone XR);
  • 微信特有API深度集成:如wx.chooseMedia调起摄像头扫描检验单条码、wx.getConnectedWifi获取院内Wi-Fi名称用于定位科室——这些API在uni-app中需额外桥接,增加兼容风险。

小程序目录结构精简到极致:

/pages/ ├── register/ // 微信授权+手机号绑定(含短信验证码防刷) ├── appointment/ // 日期选择器联动科室→医生→号源(实时查询MySQL号源表) ├── report/ // 报告列表页(下拉刷新触发WebSocket长连接监听新报告) └── profile/ // 患者档案(身份证OCR识别结果直传HIS后台校验)

2.3 安装教程不是“解压运行”,而是覆盖生产环境四层隔离

所谓“安装教程”实际包含四个独立环境配置文档:

  • 开发环境:Windows 10 + JDK 1.8.0_291 + MySQL 8.0.33 + Redis 6.2(本地调试用);
  • 测试环境:CentOS 7.9 + Docker Compose(一键启动MySQL/Redis/Nginx,Nginx反向代理HIS后端+小程序静态资源);
  • 演示环境:VMware虚拟机(预装Ubuntu 20.04镜像,含所有依赖和初始化SQL脚本,U盘拷贝即用);
  • 生产环境:详细到my.cnf参数调优(innodb_buffer_pool_size=12G适配32G内存服务器)、MySQL主从同步配置(binlog_format=ROW)、Nginx SSL证书部署(支持国密SM4算法)。

3. 本地跑通最小闭环:用5分钟完成“患者挂号→医生开方→药房发药”全流程验证

3.1 下载与解压:警惕被篡改的JAR包签名

源码包通常以his-full-202406.zip命名,解压后关键目录:

/his-server/ → Spring Boot后端(含application-prod.yml) /his-miniapp/ → 微信小程序源码(project.config.json含appid占位符) /docs/ → 安装手册(PDF+Markdown双格式) /sql/ → 初始化脚本(init_his_db.sql + init_miniapp_db.sql)

注意:运行前务必校验JAR包SHA256值。教程文档第3页提供官方签名密钥指纹(a1:b2:c3:...),用keytool -printcert -jarfile his-server.jar验证。曾有用户下载到被植入挖矿脚本的盗版包,因未校验导致服务器CPU 100%。

3.2 后端启动:三步绕过常见端口冲突

# 步骤1:修改数据库连接(避免默认3306被占用) # 编辑 his-server/src/main/resources/application-prod.yml spring: datasource: url: jdbc:mysql://localhost:3307/his_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: his_user password: his_pass123 # 步骤2:关闭HikariCP连接池的自动校验(防止MySQL未启动时卡死) spring: datasource: hikari: connection-test-query: SELECT 1 # 注释掉以下两行 # connection-timeout: 30000 # validation-timeout: 3000 # 步骤3:用Maven跳过测试直接打包(节省2分钟) cd his-server && mvn clean package -Dmaven.test.skip=true java -jar target/his-server-1.0.jar --spring.profiles.active=prod

启动成功标志:控制台输出Started HisApplication in XX seconds且http://localhost:8080/actuator/health返回{"status":"UP"}。

3.3 小程序调试:微信开发者工具必须设为“基础库2.28.0”

小程序无法直接扫码登录,需手动注入测试数据:

  1. 打开微信开发者工具,选择his-miniapp目录;
  2. 点击「详情」→「本地设置」→ 勾选「不校验合法域名、web-view、TLS版本」;
  3. 修改app.js中API地址:
// 替换为你的后端IP(不能用localhost!) const API_BASE = 'https://192.168.1.100:8080/api';
  1. 在pages/register/register.js中临时注释掉微信授权逻辑,直接模拟登录:
// 注释掉 wx.login() 部分 // const loginRes = await wx.login(); this.setData({ openid: 'mock_openid_123456' }); // 硬编码测试openid
  1. 运行后进入「预约挂号」页,选择任意科室和医生,提交成功即生成挂号单(数据写入MySQLappointment_order表)。

3.4 全流程验证:用Postman触发医生开方+药房发药

仅靠前端操作无法验证后端事务,需用Postman模拟真实角色:

  • 医生开方(POSThttp://localhost:8080/api/prescription/create):
{ "patientId": 1001, "doctorId": 2001, "department": "内科", "diagnosis": "上呼吸道感染", "medicines": [ { "medId": 5001, "qty": 3, "usage": "口服,每日三次" } ] }
  • 药房发药(POSThttp://localhost:8080/api/dispense/execute):
{ "presId": 3001, "dispenseBy": "pharmacist001", "qty": 3 }

验证成功标志:MySQL中medicine_stock表对应药品available_qty减少3,prescription表status变为DISPENSED,且dispense_record表新增记录。


4. 避坑指南:HIS系统上线前踩过的7个血泪坑,第4个让整套系统停摆3天

4.1 现象:小程序挂号页面白屏,控制台报Cannot read property 'then' of undefined

原因:微信基础库版本低于2.25.0,wx.cloud.callFunction返回Promise对象不兼容旧版。
解决:在小程序app.js顶部强制降级调用方式:

// 替换所有 wx.cloud.callFunction(...) 为 wx.cloud.callFunction({ name: 'getDepartments', success: res => { /* 处理逻辑 */ }, fail: err => { /* 错误处理 */ } });

4.2 现象:MySQL插入处方时抛出Data truncation: Data too long for column 'diagnosis'

原因:diagnosis字段定义为VARCHAR(50),但医生录入“慢性阻塞性肺疾病急性加重期(AECOPD)合并II型呼吸衰竭”超长。
解决:执行SQL扩容(注意:必须停服维护窗口):

ALTER TABLE prescription MODIFY COLUMN diagnosis TEXT; -- 同时修改MyBatis实体类对应字段类型为String

4.3 现象:药房发药后库存扣减正确,但患者APP端查不到发药记录

原因:小程序WebSocket连接未监听DISPENSE_EVENT事件,后端用SimpleMessageBroker广播,但前端未注册订阅。
解决:在小程序pages/report/report.js中补全:

// 建立WebSocket连接后 wx.connectSocket({ url: 'wss://your-domain.com/ws' }); wx.onSocketOpen(() => { wx.sendSocketMessage({ data: JSON.stringify({ type: 'SUBSCRIBE', topic: 'dispense' }) }); }); wx.onSocketMessage((res) => { const msg = JSON.parse(res.data); if (msg.type === 'DISPENSE_EVENT') { this.setData({ latestDispense: msg.data }); } });

4.4 现象:生产环境部署后,高峰期挂号请求大量超时(>30s),但服务器CPU/内存正常

原因:MySQL连接池最大连接数maxActive=20,而医院日均挂号量2000+,瞬时并发超50,连接池耗尽导致线程阻塞。
解决:调整HikariCP参数(application-prod.yml):

spring: datasource: hikari: maximum-pool-size: 100 minimum-idle: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

血泪经验:此参数必须结合MySQLmax_connections设置(建议设为200),否则HikariCP会创建超出MySQL限制的连接,触发拒绝服务。

4.5 现象:医保结算返回“交易失败:签名验签错误”

原因:国家医保平台要求RSA2048签名,但源码默认用MD5,且私钥未按医保规范Base64编码。
解决:替换InsuranceService.java中的签名方法:

// 使用标准PKCS#8格式私钥(非PKCS#1) private String signWithRSA(String content, String privateKeyStr) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(privateKeyStr); PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance("RSA"); PrivateKey privateKey = kf.generatePrivate(spec); Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(content.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(signature.sign()); }

5. 权限与安全加固:从“能跑”到“敢上线”的3个关键动作

5.1 行级权限控制:让医生只能看到自己管的住院患者

HIS系统最危险的漏洞是越权访问——比如心内科医生查到神经外科患者的病历。源码用MyBatis Plus的@TableName(autoResultMap = true)配合自定义拦截器实现行级过滤:

@Component public class DataScopeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; // 只对查询语句生效 if (!ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { return invocation.proceed(); } // 获取当前登录医生ID(从ThreadLocal取) Long doctorId = SecurityContext.getDoctorId(); if (doctorId == null) return invocation.proceed(); // 对patient表添加WHERE条件 BoundSql boundSql = ms.getBoundSql(null); String sql = boundSql.getSql(); if (sql.contains("FROM patient")) { sql = sql.replace("FROM patient", "FROM patient WHERE attending_doctor_id = " + doctorId); } // ... 其他表同理 return invocation.proceed(); } }

提示:此方案比Shiro的@RequiresPermissions更底层,避免SQL注入风险。但需注意:attending_doctor_id字段必须在所有相关表中存在,否则拦截器会失效。

5.2 小程序敏感数据脱敏:身份证、手机号前端自动掩码

微信小程序不信任任何前端加密,必须服务端脱敏。在PatientController.java中:

@GetMapping("/profile/{id}") public Result<PatientDTO> getProfile(@PathVariable Long id) { Patient patient = patientMapper.selectById(id); PatientDTO dto = new PatientDTO(); dto.setId(patient.getId()); dto.setName(patient.getName()); dto.setIdCard(maskIdCard(patient.getIdCard())); // 脱敏方法 dto.setPhone(maskPhone(patient.getPhone())); return Result.success(dto); } private String maskIdCard(String idCard) { if (idCard == null || idCard.length() < 18) return idCard; return idCard.substring(0, 6) + "********" + idCard.substring(14); } private String maskPhone(String phone) { if (phone == null || phone.length() < 11) return phone; return phone.substring(0, 3) + "****" + phone.substring(7); }

5.3 生产环境HTTPS强制跳转:Nginx配置零容错

HTTP明文传输患者信息违反《个人信息保护法》,必须强制HTTPS。Nginx配置片段(/etc/nginx/conf.d/his.conf):

server { listen 80; server_name his.your-hospital.com; return 301 https://$server_name$request_uri; # HTTP强制跳转 } server { listen 443 ssl http2; server_name his.your-hospital.com; ssl_certificate /etc/nginx/ssl/his.crt; ssl_certificate_key /etc/nginx/ssl/his.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 小程序静态资源走CDN,但API必须走HTTPS location /miniapp/ { alias /var/www/his-miniapp/; try_files $uri $uri/ =404; } }

注意:X-Forwarded-Proto头必须透传,否则Spring Boot的request.isSecure()永远返回false,导致重定向循环。


6. 实战技巧:用一条SQL查出“近7天各科室挂号量TOP5”,附带医生排班空闲率分析

HIS的价值不在功能堆砌,而在用数据驱动医疗资源调度。下面这条SQL直接输出运营日报核心指标,无需额外BI工具:

SELECT d.name AS department_name, COUNT(a.id) AS total_appointments, ROUND(COUNT(a.id) * 100.0 / NULLIF( (SELECT COUNT(*) FROM doctor_schedule ds JOIN doctor doc ON ds.doctor_id = doc.id WHERE doc.department_id = d.id AND ds.date BETWEEN DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND CURDATE()), 0), 2) AS utilization_rate, ROUND(AVG(TIMESTAMPDIFF(MINUTE, a.create_time, a.appoint_time)), 0) AS avg_wait_minutes FROM appointment_order a JOIN doctor doc ON a.doctor_id = doc.id JOIN department d ON doc.department_id = d.id WHERE a.status = 'CONFIRMED' AND a.appoint_time BETWEEN DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND CURDATE() GROUP BY d.name ORDER BY total_appointments DESC LIMIT 5;

字段解读:

  • utilization_rate:科室挂号量占该科室医生总排班时段的百分比(反映资源饱和度);
  • avg_wait_minutes:患者从下单到就诊的平均等待时长(暴露流程瓶颈);
  • 若某科室utilization_rate > 95%但avg_wait_minutes > 45,说明需增加医生或优化分诊规则。

我习惯把这条SQL固化为MySQL视图v_department_daily_report,每天凌晨2点用mysqldump导出CSV发给院长邮箱。去年帮一家社区医院发现“儿科挂号量TOP1但医生排班空闲率仅30%”,追查发现是分诊台把发热患儿全导流至儿科,而发热门诊闲置——调整分诊规则后,儿科平均候诊时间从68分钟降至22分钟。

希望帮到你。

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

返回列表