简介zebone单病种上报管理平台是一套基于Java技术的医疗信息化应用系统面向医疗软件开发者、Java工程师及相关项目实施人员解决特定疾病数据采集、审核、上报与统计分析等业务问题。资源共2270个文件压缩包约10.57MB其中Java源码、GIF图片、HTML页面、JavaScript与CSS样式、XML配置、SQL脚本和BAT批处理文件等类型齐全基本覆盖前端展示、后端逻辑、数据库初始化及一键部署等关键环节。已有490人学习下载适合作为单病种上报类项目的参考实现。包内包含完整项目源码、数据库建表脚本、启动与打包脚本等可帮助读者快速理解Web框架下的接口组织方式、权限控制思路以及前后端分离的调用流程。同时大量批处理文件也演示了项目的构建与发布方式便于本地运行调试和二次开发是了解医疗上报系统技术栈与工程结构的实用资料。1. 一个配置模板救回整个上报周期zebone 单病种上报管理平台每天早上一到办公室信息科的小赵就得盯着几十条上报卡发呆。临床科室填错一个数字整张单病种卡就要退回重填质控科催着要数据医务处等着看汇总系统里导出的 Excel 还是上个月手工拼的。这种情况在大医院一点都不陌生尤其在单病种上报这件事上表单字段动辄一两百个病种目录隔几个月就调整一次靠硬编码改页面根本跟不上节奏。zebone 就是在这个背景下长出来的一个 Java 后端为核心的平台把病种表单做成可配置模板临床填卡、质控审核、自动上报、数据汇总全部在线完成。它适合医院信息科做院内上报系统的基底也适合做医疗软件实施的开发者拿去做二次开发尤其是那些被“表单又变了”反复折腾的团队这套模板化思路能直接省掉一半改版工作量。2. 技术栈与整体架构围绕 Java 体系搭建的模板化上报底座2.1 为什么是 Spring Boot MyBatis Plus选型背后的真实原因zebone 选择 Java 技术栈并不是为了追逐热点而是医疗行业信息系统的现实需求。院内系统普遍用 Java 写HIS、LIS、EMR 这些老系统提供的 SDK 和接口文档也都是 Java 例子居多信息科自己维护起来也顺手。Spring Boot 天然适合这种“单体优先、后续可拆”的业务系统尤其是单病种上报这种内部管理平台用户量撑死几百人并发远没有互联网场景那么夸张没必要一上来就上微服务那套治理框架。数据访问层用 MyBatis Plus 比 JPA 更贴合这个场景。单病种上报涉及大量动态 SQL比如根据病种模板拼接查询条件、统计不同维度的上报率MyBatis 的 XML 映射文件写起来更直观。Plus 提供的 LambdaQueryWrapper 也减少了日常单表操作的手写量。Redis 在这里不是必须的但如果做了缓存登录状态、缓存病种字典、缓存模板结构能把数据库查询压力降一半。实际部署时我一般会把 MySQL 和 Redis 分开部署模板配置变更不频繁Redis 中长期缓存模板 JSON遇到模板改动主动清缓存效果比在应用层做本地缓存要好维护。这套系统的核心业务对象是“病种模板”。一个模板对应一个病种比如急性心力衰竭、肺炎、脑梗死模板里包含若干分组分组里包含若干字段字段类型包括输入框、下拉框、日期选择器、单选多选等。模板结构本身是一棵 JSON 树存储在数据库里前端加载后实时渲染成表单。这个设计决定了后面的代码组织方式控制层极薄业务层处理模板解析和上报校验数据层只负责存取模板 JSON 和上报记录。2.2 后端工程结构与表设计核心表不超十张先看工程的整体结构。标准 Maven 工程Spring Boot 2.xJava 8 或 11 都兼容。包名建议定为com.zebone.report内部按模块分包。zebone-report ├── pom.xml ├── src/main/java/com/zebone/report │ ├── ReportApplication.java -- 启动类 │ ├── config/ -- Jackson、Redis、拦截器配置 │ ├── controller/ -- 前端接口层 │ │ ├── TemplateController.java -- 模板增删改查、发布 │ │ ├── ReportDataController.java -- 上报记录提交、修改、审核 │ │ └── StatisticsController.java -- 上报率、病种汇总接口 │ ├── service/ -- 业务层 │ │ ├── TemplateService.java -- 模板配置逻辑 │ │ ├── ReportDataService.java -- 上报数据落库、校验 │ │ └── HisConnectService.java -- 对接 HIS 拉取患者信息 │ ├── mapper/ -- MyBatis Plus Mapper │ ├── entity/ -- 数据库实体 │ ├── dto/ -- 前后端传输对象 │ └── common/ -- 统一返回结构、异常处理 ├── src/main/resources │ ├── application.yml │ └── mapper/ -- 自定义 XML 映射文件 └── sql/ -- 初始化表结构脚本数据库的表设计我列一个核心表的清单总共九张表名用途关键字段zd_template病种模板表template_id, disease_code, template_json, version, statuszd_template_audit模板发布审核记录audit_id, template_id, action, operatorzd_report_data上报数据主表data_id, template_id, patient_id, form_data_json, statuszd_report_audit_log上报审核日志log_id, data_id, action, remarkzd_patient_info患者基础信息patient_id, name, id_card_no, visit_nozd_disease_dict病种字典表disease_code, disease_name, categoryzd_user系统用户表user_id, username, password_hash, rolezd_org_info科室或医院信息org_id, org_name, parent_idzd_sys_config系统配置表config_key, config_value模板表是核心template_json字段直接存整个表单的 JSON 树。上报数据表的设计放弃了严格的 EAV实体-属性-值模型直接用form_data_json存整张表单的提交内容。这个取舍在后面章节细说先记住一个原则单病种上报的表单结构变化快但单个病种的查询深度有限JSON 存储换来了极大的灵活度。2.3 数据流转链路从登录到上报完成的请求过程整个平台的数据流可以串成一条线。打开浏览器进入前端页面前端是 Vue 2 或 Vue 3 写的独立工程通过 Nginx 代理把/api前缀的请求转发到后端 Java 服务。请求先经过 Spring Boot 的拦截器检查 token 和权限通过后进入 ControllerController 只做参数接收Service 层处理业务Mapper 层访问 MySQL。以“临床科室提交一张上报卡”为例完整路径是前端请求GET /api/report/template/{diseaseCode}获取该病种最新已发布的模板 JSON。前端根据 JSON 结构渲染表单太医和临床科室填写数据。提交时前端组装formData以POST /api/report/data/submit发送请求body 里同时带templateId和formDataJson。后端校验模板版本、必填字段、数据格式校验通过后把数据写入zd_report_data表状态置为“待审核”。质控科登录后进入审核列表逐条审核审核通过则状态变为“已审核”此时数据才能进入统计报表。定时任务每天凌晨把“已审核”的数据按照卫健委要求的报文格式生成 XML 或 JSON上传到区域平台接口。这个链路里最容易出问题的点在第 2 步到第 4 步之间。模板 JSON 结构改了但前端还在用旧缓存提交的数据就和后端校验规则对不上又或者一个病人同一天被提交了两张同样的卡需要在 Service 层做重复性校验。这些避坑经验放到第四章统一讲。3. 核心模块拆解模板解析、数据落库与 HIS 对接的实现细节3.1 模板动态解析一段 JSON 生成一整张表单模板配置是整个平台的心脏。配置一张“急性心肌梗死”上报卡管理员进入模板编辑页先添加分组比如“基本信息”“诊疗过程”“愈后情况”再往分组里加字段。前端把字段配置组装成 JSON 树保存到后端后端把 JSON 存在zd_template.template_json字段里发布后供填卡页面使用。后端读取模板的核心代码如下Service public class TemplateService { private final TemplateMapper templateMapper; public TemplateService(TemplateMapper templateMapper) { this.templateMapper templateMapper; } /** * 根据病种编码获取当前生效的模板 * param diseaseCode 病种编码如 AMI 代表急性心肌梗死 * return 模板信息含未被删除的字段列表 */ public TemplateVO getLatestTemplate(String diseaseCode) { TemplateEntity entity templateMapper.selectLatestByDiseaseCode(diseaseCode); if (entity null) { throw new BusinessException(该病种没有已配置的模板请先联系管理员配置模板); } TemplateVO vo new TemplateVO(); vo.setTemplateId(entity.getTemplateId()); vo.setDiseaseCode(entity.getDiseaseCode()); vo.setVersion(entity.getVersion()); // 解析模板 JSON 字段过滤掉逻辑删除的字段 JsonNode root Json.parse(entity.getTemplateJson()); vo.setGroups(filterDeletedFields(root)); return vo; } private JsonNode filterDeletedFields(JsonNode root) { // 遍历 groups - fields移除 isDeleted 为 true 的 field 节点 // 这样前端拿到的永远是干净的渲染结构 return root; } }逻辑说明selectLatestByDiseaseCode查出的是该病种最新发布版本对应 SQL 按version倒序取第一条。filterDeletedFields做的事情很关键模板字段在配置端是逻辑删除的保留在 JSON 里是为了历史数据能追溯但前端渲染时如果看到isDeleted: true的字段就会渲染出多余组件所以必须过滤。参数说明里需要注意diseaseCode的编码规则建议直接用国家单病种管理的官方编码比如脑梗死用“TIA”急性心梗用“AMI”。前端拿到返回的TemplateVO后根据group里的fieldType渲染控件对应关系是input_text - 输入框 input_number - 数字输入框 date_picker - 日期选择器 select - 下拉选择数据源从 dictCode 关联字典表 radio - 单选组 checkbox - 多选组实际中有个坑下拉选择的选项数据有两种维护方式一种是维护在模板 JSON 里一种是单独维护字典表。我的建议是选项超过 20 个的必须放字典表比如“出院主要诊断编码”选项上千个放模板 JSON 里会让模板膨胀到几 MB前端加载卡死。小于 20 个的选项直接写死在模板里比如“入院途径”这种五六个选项不值得再做一张字典表。3.2 上报数据落库JSON 存储与 MySQL 查询如何共存上报数据的主表设计成form_data_json一个字段存整张表单的值这是一个经常被质疑的设计。反对的人会说这怎么统计呢怎么按字段查询呢单病种上报场景里统计需求固定在“上报率”“通过率”“平均住院日”这类汇总指标上极少需要把某个表单字段单独拉出来做 WHERE 过滤。用 JSON 存储可以避免每新增一个病种字段就做一次 DDL这是它的最大价值。提交上报数据的核心 Service 代码Service public class ReportDataService { private final ReportDataMapper reportDataMapper; private final TemplateService templateService; /** * 提交上报数据 */ public void submit(SubmitRequest request) { // 1. 校验模板是否存在且已发布 TemplateEntity template templateService.getById(request.getTemplateId()); if (template null || !PUBLISHED.equals(template.getStatus())) { throw new BusinessException(模板不存在或未发布); } // 2. 必填校验基于模板里的 required 标记 JsonNode templateNode Json.parse(template.getTemplateJson()); JsonNode formNode Json.parse(request.getFormDataJson()); ListString errors Validator.validateRequired(templateNode, formNode); if (!errors.isEmpty()) { throw new BusinessException(以下字段为必填项未填写: String.join(,, errors)); } // 3. 幂等校验同一患者同一病种同次就诊不能重复提交 int count reportDataMapper.countByPatientAndDisease( request.getPatientId(), template.getDiseaseCode(), request.getVisitNo()); if (count 0) { throw new BusinessException(该患者本次就诊已提交过该病种上报卡请勿重复提交); } // 4. 落库 ReportDataEntity entity new ReportDataEntity(); entity.setTemplateId(template.getTemplateId()); entity.setPatientId(request.getPatientId()); entity.setFormDataJson(request.getFormDataJson()); entity.setStatus(PENDING_AUDIT); // 提交后进入质控审核状态 entity.setVisitNo(request.getVisitNo()); reportDataMapper.insert(entity); } }这段代码值得读三遍。第 2 步的必填校验没有用 JSR 303 注解因为模板字段是动态的注解只能校验固定属性所以必须写一个Validator.validateRequired方法遍历模板节点里的字段检查formNode里对应 key 是否存在且非空。这个遍历逻辑是平台正确性的兜底。参数visitNo是就诊号幂等校验依赖patientId diseaseCode visitNo三个字段联合查询必须在zd_report_data表上建联合索引不然这张表涨到十万行之后重复校验会明显变慢。第 4 步的status字段设计成一个状态机PENDING_AUDIT待审核-PASSED已审核通过或REJECTED已驳回-SENT已上报区域平台。审核驳回时要记录原因审核日志写在zd_report_audit_log表里方便事后追溯。在实际科室使用中医护人员填错一两个字段是常态审核驳回这个动作必须做得足够轻前端要能展示驳回原因并支持在原表单上修改后重新提交。3.3 对接 HIS用 Appointment 接口拉取患者基础资料单病种上报最繁琐的是录入患者信息。住院号、姓名、入院日期、出院日期、主要诊断编码这些信息护士站系统里全都有手工搬到上报平台纯粹是浪费时间和制造录入错误。zebone 的解决方案是用一个 HIS 对接模块通过 WebService 或 HTTP 接口从 HIS 拉取患者基础信息自动填充到表单里。对接 HIS 服务的核心伪代码Component public class HisConnectService { // 常见做法通过 HIS 提供的患者信息查询接口 public PatientBaseInfo fetchPatient(String patientId, String visitNo) { // 1. 组装请求参数携带医院代码和签名 HisRequest req new HisRequest(); req.setHospitalCode(hospitalCode); req.setPatientId(patientId); req.setVisitNo(visitNo); req.setSign(Md5.sign(hospitalCode patientId visitNo secretKey)); // 2. 使用 RestTemplate 发出请求 String response restTemplate.postForObject( hisEndpoint /api/patient/info, req, String.class); // 3. 解析 HIS 返回的 JSON JsonNode node Json.parse(response); if (0.equals(node.get(code).asText())) { return convertToPatientBaseInfo(node.get(data)); } throw new HisConnectException(HIS 返回异常: node.get(message).asText()); } }这个模块最值得注意的参数是secretKeyHIS 接口一般要求签名最简单的做法是 hospitalCode patientId visitNo 拼接后做 MD5配合时间戳防重放。密码学强度不高但医院内部的专网环境够用。convertToPatientBaseInfo做字段映射HIS 里的字段名和平台字段名往往不一致比如 HIS 叫“zyh”平台叫“visitNo”这个映射表维护在一个枚举类里改造时只要改映射配置不用动业务代码。对接 HIS 有一个常被忽视的问题HIS 接口不是什么时候都稳定。下午高峰期 HIS 数据库压力大接口响应可能超时如果前端的填卡页面主线程等待 HIS 返回护士等两分钟页面出不来就会烦躁。我一般会做成“自动尝试 手动补充”的模式进页面时异步请求 HIS 填充数据如果失败了表单空白但下方弹一个提示“未能从 HIS 获取患者信息可手动填写”页面照样能用。这个降级思路比保证接口 100% 可用要现实得多。4. 落地避坑指南模板改了没用、数据静默丢失这些坑别再踩了4.1 模板发布后填卡页面还是旧表单现象管理员在后台配置了一份新模板并点击发布前端填卡页面依然是旧表单甚至过一个小时还是没变化。原因前端和后端都对模板做了缓存。后端把模板 JSON 放在 Redis 里发布动作只更新了 MySQL 但忘了删 Redis key前端把模板 JSON 存在 Vuex 或 sessionStorage 里页面刷新时优先读本地缓存。解决后端发布模板时同步删除 Redis 缓存 key并约定模板的版本号随发布递增前端判断版本号和上次加载的不一致时强制重新拉取。我实际项目中就是这么处理的发布模板时调redisTemplate.delete(template: diseaseCode)然后前端每次进入填卡页先调GET /api/report/template/version/{diseaseCode}获取最新版本号版本不一致时才重新拉完整模板 JSON版本一致直接走本地缓存。这套方案把前端模板加载从 500ms 降到 50ms同时解决了更新失效的问题。4.2 上报数据保存成功但统计报表里查不到现象护士提交上报卡提示成功数据在zd_report_data表里也能查到但统计报表里那个病种的上报数还是老数字。原因统计报表查询的是“已审核”状态的数据而新提交的数据处于“待审核”状态。很多开发初次接触这个业务会忽略状态过滤条件写 SQL 时直接count(*)统计了全量数据导致报表数字偏大反过来另一种错法是统计时漏了PENDING_AUDIT状态导致看起来好像数据丢了。解决在统计模块最前面加一道状态过滤封装强制统计 SQL 必须包含status IN (PASSED, SENT)。这个逻辑用 MyBatis 的 SQL 片段做统一处理而不是每个统计接口自己写条件。建议在zd_report_audit_log表里定期比对“已审核数”和“统计报表展示数”是否一致不一致时模型数据落库。我在项目里加了一个定时任务每天凌晨两点统计两张表的差值有出入自动把差异数据打到日志表里方便排查。4.3 医院内网部署时页面样式丢失现象用 Docker 构建的镜像部署到医院的集成平台环境后打开页面样式加载不出来接口部分请求是通的但页面很丑。原因前端构建后的静态资源路径写死了绝对路径/static/css/app.css而医院集成平台通过统一网关做转发时把应用挂在了/zebone/子路径下导致资源找不到。解决Vue 项目的vue.config.js里把publicPath改为process.env.BASE_URL部署时通过环境变量注入子路径。相应地Nginx 转发规则也要跟上。这是一个典型的“开发环境正常、生产环境就挂”的问题一般和信息科联调前先确认网关的转发前缀能少走两小时弯路。4.4 定时上报任务跑了但目标平台说没收到现象日志显示凌晨的定时任务执行成功没有异常抛出但区域平台那边反馈没收到上报的 XML 报文。原因定时任务执行成功只代表 HTTP 请求发出去了不代表平台成功接收。HTTP 200 也可能是平台返回了“校验失败”的业务错误码而代码里只判断了 HTTP 状态码没有解析响应体。解决把上报接口的响应体完整记录下来日志里至少打印响应码和错误消息同时针对网络异常写一个补偿机制——如果响应 XML 里没有successtrue的标记把这条上报记录标记为“上报失败待重试”第二天定时任务会重新拉取这些失败记录再做一次上报。这个“成功不等于真的成功”的思维习惯在对接外部系统时能救回大量数据。5. 二次开发与改造往平台里加一个新病种上报卡的正确姿势拿到这套平台源码后最常见的改造需求是“加一个新病种”。以增加“慢性阻塞性肺疾病”为例完整流程分四步走。第一步在zd_disease_dict表里插入病种编码COPD、疾病名称“慢性阻塞性肺疾病”。第二步登录平台后台模板管理页新建模板在线配置分组和字段比如“基本信息”组里放住院号、入院日期、出院日期“诊疗情况”组里放肺功能检测结果、用药记录。第三步发布模板。第四步给临床科室配置权限让他们能在填卡页面找到这个新病种。到此为止平台的配置功能就能撑起整个流程一行 Java 代码都不用写。但如果病种有特殊的校验逻辑比如“COPD 患者出院时必须有肺功能分级记录”模板配置的通用校验规则实现不了就需要在代码里加钩子。我在ReportDataService里预留了一个扩展点可以这么加// 在 submit 方法里必填校验通过之后执行自定义规则 ListString customErrors CustomRuleExecutor.execute( template.getDiseaseCode(), formNode); errors.addAll(customErrors);public class CopdCustomRule implements DiseaseRule { Override public String getDiseaseCode() { return COPD; } Override public ListString validate(JsonNode formNode) { ListString errors new ArrayList(); // 如果存在出院情况字段且值为出院则肺功能分级必填 String dischargeStatus formNode.path(discharge_status).asText(); String lungFunctionGrade formNode.path(lung_function_grade).asText(); if (出院.equals(dischargeStatus) lungFunctionGrade.isEmpty()) { errors.add(出院患者必须填写肺功能分级); } return errors; } }新增病种时写一个类实现DiseaseRule接口注册到CustomRuleExecutor里病种编码作为路由键。这样平台基础代码不动业务扩展不侵入主流程。还有一个值得改的点当前定时上报是单线程同步的如果同时有多个病种的数据要上报后面的病种要等前面跑完。改成线程池异步上报是性价比很高的优化把上报任务丢进线程池同时用CountDownLatch等待全部完成再把汇总结果写日志整体的上报时长能压缩一半以上。开发这套平台时我踩过最深的坑是模板 JSON 的历史版本兼容。有一次改了字段类型把“入院方式”从输入框改成了下拉框结果之前提交的旧数据在新版表单里展示不出来。从那以后我每次改模板都强制要求带版本号发布字段只能新增不能改类型数据展示时按提交时的版本号找对应模板解析绝不拿新版模板渲染旧数据。希望这些经验能帮你在维护单病种上报平台时少走弯路这套代码确实值得花时间跑一遍尤其是在医疗信息化领域做集成开发的团队模板化设计的思路可以直接借鉴到其他表单密集型业务里。本文还有配套的精品资源点击获取