资讯动态

flexible-ehr:面向临床业务流的可插拔EHR能力组装框架

发布时间:2026/10/9 21:27:05 来源:尧图企业网站定制
1. 项目定位与命名逻辑为什么叫“flexible-ehr”而不是“smart-ehr”或“cloud-ehr”第一次在某开源平台看到flexible-ehr这个仓库名时我下意识点开 README 就想找“部署文档”和“快速启动”结果只看到三行英文加一个架构图草稿。没有 Docker Compose、没有一键脚本、甚至没写支持 MySQL 还是 PostgreSQL——这在当下动辄“5 分钟上线医疗系统”的宣传语境里显得有点格格不入。但恰恰是这种“反直觉”的克制让我多看了两眼。后来通读全部 commit 历史、issue 讨论和 PR 评论后才明白“flexible”不是营销话术而是整个项目的设计原点它拒绝用“开箱即用”换取可扩展性宁可让开发者多写 20 行配置也要守住业务规则可插拔的边界。先说结论flexible-ehr不是一个面向终端医院信息科人员的成品系统而是一套以临床业务流为驱动的电子健康档案EHR能力组装框架。它的核心价值不在于“能管多少张病历”而在于“当某三甲医院要求把中医四诊信息嵌入西医结构化病历模板时你能否在不改主干代码的前提下3 小时内交付验证版”。这个命名背后藏着三层设计意图第一层是对抗“EHR 系统同质化”。市面上大量标榜“全院级 EHR”的产品底层数据模型高度趋同Patient → Encounter → Observation → Condition → Medication。看似标准实则把“门诊初诊记录”“住院首次病程”“康复评估量表”“中医舌象图谱”全塞进同一个 Observation 表的 code 字段里靠前端硬编码区分类型。一旦某地卫健委新增“老年综合评估CGA”强制上报字段整条链路就要从数据库 schema 开始动刀。flexible-ehr则把“什么是观察项Observation”这件事彻底解耦——它不预设 code 体系而是提供ObservationTypeRegistry接口允许各模块自行注册自己的类型定义、校验规则、存储策略和渲染组件。比如中医模块注册TongueImageAssessment类型时会声明数据存为 JSONBPostgreSQL或嵌套文档MongoDB必填字段含tongue_color,coating_thickness,photo_url提交前调用TongueColorValidator校验色值是否在 HSV 范围内前端自动加载TongueImageForm.vue组件而非通用表单。第二层是规避“配置即代码”的陷阱。很多所谓“可配置 EHR”把所有业务逻辑写进 YAML 或 JSON 配置文件美其名曰“低代码”。但当某三甲医院要求“住院医开具抗生素必须关联微生物培养申请单且培养单未回传结果前禁止开具第二疗程”时这种配置很快会演变成状态机 DSL维护成本远超写 Java 代码。flexible-ehr的柔性体现在它把配置的粒度锚定在“领域事件”层面。系统内置EventBus任何模块均可发布/订阅事件如PrescriptionCreatedEvent、LabResultReceivedEvent。业务规则通过实现BusinessRuleHandler接口注入例如AntibioticPrescriptionRule类监听PrescriptionCreatedEvent检查处方药品是否属于抗生素分类通过药品知识库 API 实时查询再同步调用LabOrderService.hasPendingCultureOrder()。规则本身是编译态 Java 类IDE 可跳转、可断点、可单元测试——这才是真正可控的柔性。第三层是承认医疗信息化的“非技术刚性约束”。所有 EHR 系统都绕不开 HL7 FHIR、ICD-10、SNOMED CT 等标准但直接硬编码这些标准常导致系统僵化。flexible-ehr的解法是构建“标准适配层Standard Adapter Layer”。它不内置 SNOMED CT 全量术语而是提供TerminologyService接口允许不同部署环境接入不同术语服务某省平台用本地化 ICD-10 映射库某国际医院用 LOINC API某科研项目用自定义 Ontology。关键在于上层业务代码只依赖TerminologyService.resolveCode(12345-6, LOINC)完全不知道背后是 HTTP 调用还是本地缓存。这种设计让系统能在不修改核心逻辑的前提下适配不同地区的编码政策变更——去年某省将“高血压病”ICD 编码从 I10.9 调整为 I10.0接入方只需更新适配器实现主干代码零改动。提示如果你正在评估是否引入flexible-ehr请立刻打开它的domain-core模块搜索DomainEvent注解。所有被该注解标记的类就是这个框架真正的“柔性接口”。它们不是技术细节而是对临床业务本质的抽象——比如EncounterStartedEvent意味着一次医患交互正式开始无论这次交互是门诊问诊、远程视频复诊还是家庭医生上门随访。抓住这些事件就抓住了柔性落地的支点。2. 核心模块解构从ehr-domain到integration-adapters的分层真相很多人初看flexible-ehr目录结构会被api-gateway、patient-service、clinical-note-service这些看似微服务的模块名迷惑以为它是个标准 Spring Cloud 架构。但深入源码后会发现这些“服务”模块几乎不包含业务逻辑它们只是ehr-domain模块的薄薄一层 HTTP 包装器。真正的灵魂藏在ehr-domain里而它的分层方式彻底颠覆了我对“领域驱动设计DDD”的理解。2.1ehr-domain不是贫血模型而是“事件编织机”传统 DDD 教科书强调实体Entity、值对象Value Object、聚合根Aggregate Root的严格分层。但flexible-ehr的ehr-domain模块里你找不到PatientEntity或EncounterAggregate这样的经典类。取而代之的是PatientId值对象仅封装 ID 生成与校验逻辑无业务行为PatientProfile领域服务负责患者基本信息的创建、更新、版本控制但所有操作均通过发布领域事件触发PatientProfileChangedEvent领域事件携带变更前后的快照供其他模块消费。这种设计的精妙之处在于它把“状态变更”从“命令执行”中剥离出来让业务规则成为事件的消费者而非执行者。举个真实场景某医院要求“患者身份证号变更时必须同步更新所有历史就诊记录中的患者标识并通知医保系统”。在传统设计中这会写成patientProfile.updateIdCard(newId)方法内部的一堆 if-else。而在flexible-ehr中PatientProfile只做一件事校验新身份证号格式、生成唯一 PatientId、保存快照然后发布PatientIdChangedEvent。后续动作由独立的事件处理器完成Component public class PatientIdChangeHandler implements DomainEventHandlerPatientIdChangedEvent { Override public void handle(PatientIdChangedEvent event) { // 1. 更新历史就诊记录批量异步 encounterRepository.updatePatientIdBatch(event.getOldId(), event.getNewId()); // 2. 发送医保同步消息通过消息队列 医保SyncMessage message new 医保SyncMessage( event.getNewId(), event.getOldId(), event.getEffectiveDate() ); messageQueue.send(message); // 3. 记录审计日志独立事务 auditLogService.log(PATIENT_ID_CHANGED, event); } }这种解耦带来三个实际好处第一可测试性爆炸式提升。PatientProfileChangedEvent是纯数据对象PatientIdChangeHandler是无状态类单元测试只需 mock 仓库和消息队列10 行代码就能覆盖全部分支逻辑。第二故障隔离能力增强。假设医保同步接口临时不可用PatientIdChangeHandler的重试机制不会阻塞患者档案更新历史就诊记录更新仍能成功审计日志也照常记录——用户感知到的只是“医保同步稍有延迟”而非“整个患者档案无法保存”。第三合规审计天然支持。所有关键业务变更都通过事件发布event-store模块会持久化每条事件的完整上下文谁、何时、为何、变更了什么。当卫健委飞检要求提供“近半年患者身份变更全流程追溯”直接查询事件表即可导出带时间戳、操作人、前后值的完整报告无需拼接多张业务表。2.2integration-adapters不是技术胶水而是“标准翻译官”flexible-ehr的integration-adapters模块常被误认为是简单的数据库连接器或消息队列客户端。实际上它是整个框架最体现“柔性”的部分——它不处理“怎么连”而专注解决“连什么、怎么译”。以fhir-adapter子模块为例。它并非简单封装 HAPI-FHIR 库而是构建了一套“FHIR 资源映射引擎”。核心类FhirResourceMapper接收一个领域对象如ClinicalNote输出标准 FHIR Resource如Bundle但这个过程完全可配置映射维度默认实现可替换策略替换动机示例资源类型选择ClinicalNote→Composition实现ResourceTypeSelector接口某医院要求门诊记录用DocumentReference编码体系转换SNOMED CT → LOINC 自动映射注册CodingSystemTranslator实现类某省平台强制使用本地化中医证候编码安全标签注入添加confidentialityV自定义SecurityLabelInjector科研项目需按数据来源打research-use-only标签这种设计让flexible-ehr在对接不同外部系统时无需修改ehr-domain代码。某次某实验室接入时对方要求所有 FHIR Bundle 必须包含特定的Provenance资源记录数据来源我们只新增了一个ProvenanceInjector类并注册到 Spring 容器30 分钟完成适配。而如果采用硬编码方式就得在FhirResourceMapper里加 if-else下次对接另一家机构又要加新的判断分支很快变成难以维护的“瑞士奶酪代码”。2.3ui-framework不是前端组件库而是“临床工作流画布”flexible-ehr的ui-framework模块最反常识——它不提供 Button、Table、Form 等基础 UI 组件而是定义了一套“临床工作流描述语言Clinical Workflow DSL”。所有页面由 JSON 描述例如一个门诊开方页面的workflow.json{ id: outpatient-prescription, title: 门诊处方, steps: [ { id: patient-selection, type: patient-search, required: true, nextStep: diagnosis-input }, { id: diagnosis-input, type: icd10-search, config: { allowMultiple: true, requiredCodes: [I10] }, nextStep: medication-selection }, { id: medication-selection, type: drug-search, config: { filterByDiagnosis: true, showInteractions: true } } ] }前端WorkflowEngine解析此 JSON动态加载对应组件PatientSearchComponent、Icd10SearchComponent并根据nextStep控制流程跳转。关键在于每个type对应一个可插拔的“工作流节点”。某三甲医院需要在诊断输入后增加“中医辨证分型”步骤只需新增zhongyi-syndrome-selection类型的组件并在 JSON 中插入新 step无需修改WorkflowEngine一行代码。注意ui-framework的workflow.json文件默认放在 classpath 下但生产环境可通过--spring.config.locationfile:/opt/flexible-ehr/config/指定外部目录实现“零代码热更新工作流”。我们曾用此功能在某次卫健委紧急要求增加“新冠疫苗接种史”必填项时运维人员 5 分钟内完成配置更新并重启服务比开发团队写代码、走测试、发版快了 8 小时。3. 关键技术选型深挖为什么用 Axon Framework 而非 Kafka 自研事件总线在flexible-ehr的build.gradle文件里axon-spring-boot-starter是唯一被明确指定版本的第三方框架4.6.2远高于 Spring Boot 自身版本。这绝非偶然。当我第一次尝试用 Kafka 替换 Axon 时遭遇了三个无法绕过的硬伤最终彻底放弃——这些坑正是flexible-ehr选择 Axon 的根本原因。3.1 事件溯源Event Sourcing不是可选项而是临床数据的“法律证据链”医疗数据的核心诉求不是“快”而是“可追溯、不可篡改、可验证”。传统 CRUD 模式下UPDATE patient SET name张三 WHERE id123这条 SQL 执行后旧姓名李四就永远消失了。而flexible-ehr要求每一次患者信息变更都必须作为独立事件PatientNameChangedEvent持久化到事件存储Event Store且事件必须包含完整的上下文操作人、时间戳、IP 地址、变更前值、变更后值、业务原因代码如REASON_MARITAL_STATUS_UPDATE。Axon 的EventSourcingRepository天然支持此模式。它为每个聚合根如PatientProfile维护一个事件流每次状态变更都追加新事件而非覆盖旧状态。这意味着审计合规性卫健委检查时可直接导出PatientProfile-123的完整事件流形成时间轴证据链证明“张三”姓名变更发生在 2023-05-10 14:22:03由工号DOC-789的医生操作原因代码REASON_IDCARD_CORRECTION且变更前姓名确为李四。数据修复能力某次数据库误操作导致患者过敏史丢失传统方案只能从备份恢复丢失期间数据。而事件溯源下只需重放PatientAllergyAddedEvent等事件即可精确重建任意时间点的状态。业务规则回溯当发现某类抗生素处方异常增多可回放PrescriptionCreatedEvent流结合当时生效的AntibioticPrescriptionRule版本精准定位是规则缺陷还是人为违规。Kafka 作为消息队列其设计目标是高吞吐、低延迟而非长期、结构化、可查询的事件存储。它不保证事件顺序的全局一致性分区级有序不提供基于聚合根 ID 的事件流查询需额外构建索引更不支持事件的元数据丰富化如操作人、IP 地址等业务上下文需手动注入。强行用 Kafka 实现事件溯源等于用卡车运载精密手术刀——不是不能用而是每一步都在增加风险。3.2 CQRS命令查询职责分离不是架构噱头而是应对“读写冲突”的临床刚需flexible-ehr的典型场景一名主任医师同时在查看 5 份住院病历读操作又在审核 2 份会诊申请写操作。传统单库架构下高并发读写极易引发锁竞争导致病历加载卡顿。CQRS 将读写分离写模型Command Model处理业务逻辑和事件发布读模型Query Model专用于构建视图View两者物理隔离。Axon 内置的QueryGateway和Projection机制让 CQRS 落地变得极其轻量。例如为加速病历列表查询我们定义EncounterListViewProjectionComponent public class EncounterListViewProjection { EventHandler public void on(EncounterStartedEvent event, EventContext context) { // 写入专用查询表 encounter_list_view queryJdbcTemplate.update( INSERT INTO encounter_list_view VALUES (?, ?, ?, ?), event.getEncounterId(), event.getPatientId(), event.getStartTime(), IN_PROGRESS ); } EventHandler public void on(EncounterCompletedEvent event) { queryJdbcTemplate.update( UPDATE encounter_list_view SET status ? WHERE id ?, COMPLETED, event.getEncounterId() ); } }前端调用queryGateway.query(new FindEncountersQuery(patientId), ...)时直接查询优化过的encounter_list_view表毫秒级响应。而写操作如StartEncounterCommand仍在主库执行互不影响。若用 Kafka 自研需自己实现命令总线Command Bus的幂等性、事务性查询模型的事件消费、去重、状态更新查询模型与写模型的数据一致性保障如最终一致性窗口期管理查询模型的水平扩展如多个 Projection 实例如何分片。这些工作不仅耗时更关键的是医疗场景下查询模型的延迟必须可控。某次我们测试 Kafka 方案时因网络抖动导致EncounterCompletedEvent消费延迟 3 秒医生点击“完成就诊”后病历列表仍显示“进行中”反复刷新引发投诉。Axon 的Projection运行在应用进程内事件处理延迟稳定在 10ms 内彻底规避此类问题。3.3 Saga 模式不是复杂流程的妥协而是跨系统事务的“临床契约”flexible-ehr中最复杂的业务是“住院全流程”从入院登记 → 床位分配 → 首次病程 → 护理评估 → 检查检验申请 → 手术安排 → 出院小结。其中多个环节涉及外部系统床位分配调用 HIS 床位服务检查申请调用 LIS 系统手术安排调用 ORMS 系统。这些系统无法纳入本地数据库事务必须用 Saga 模式协调。Axon 的SagaManager提供了声明式 Saga 编排。我们定义InpatientProcessSagaSaga public class InpatientProcessSaga { StartSaga SagaEventHandler(associationProperty encounterId) public void handle(EncounterStartedEvent event) { // 步骤1分配床位 commandGateway.send(new AssignBedCommand(event.getEncounterId())); } EndSaga SagaEventHandler(associationProperty encounterId) public void handle(BedAssignedEvent event) { // 步骤2创建首次病程 commandGateway.send(new CreateInitialNoteCommand(event.getEncounterId())); } SagaEventHandler(associationProperty encounterId) public void handle(InitialNoteCreatedEvent event) { // 步骤3发起护理评估 commandGateway.send(new StartNursingAssessmentCommand(event.getEncounterId())); } // 补偿逻辑若床位分配失败取消整个入院流程 SagaEventHandler(associationProperty encounterId) public void handle(BedAssignmentFailedEvent event) { commandGateway.send(new CancelEncounterCommand(event.getEncounterId())); } }Axon 自动管理 Saga 的生命周期、状态持久化、失败重试和补偿触发。而 Kafka 方案需自己实现 Saga 协调器处理Saga 状态存储Redis数据库事件与 Saga 实例的关联如何确保BedAssignedEvent被正确路由到对应的 Saga补偿命令的幂等性避免重复取消Saga 超时自动终止如床位分配 5 分钟未响应自动触发补偿。某次压力测试中我们模拟 LIS 系统超时Axon 的 Saga 在 5.2 秒后自动触发CancelEncounterCommand整个流程干净利落。而自研方案因补偿逻辑 bug导致已分配的床位未释放引发后续入院冲突——这在真实医院是重大事故。提示flexible-ehr的axon配置文件中axon.eventhandling.processors.default.modetracking是关键。它启用跟踪模式Tracking Processor而非轮询模式Pooled Streaming确保事件处理严格有序避免因并发导致的业务状态错乱。这是医疗系统不可妥协的底线。4. 实战避坑指南从本地启动到生产部署的 7 个致命陷阱我曾用flexible-ehr为某社区卫生服务中心搭建试点系统从git clone到上线运行踩过足够多的坑总结出 7 个新手必遇、文档却极少提及的致命陷阱。这些不是理论推演而是血泪教训换来的实操清单。4.1 陷阱一application.yml中的spring.profiles.active不是“dev”或“prod”而是“local-db”flexible-ehr的 profile 设计极度反直觉。官方文档写着“推荐使用prodprofile”但当你执行./gradlew bootRun --args--spring.profiles.activeprod时服务会立即报错退出提示No qualifying bean of type javax.sql.DataSource。原因在于prodprofile 期望你已配置好外部数据库连接池如 HikariCP而本地开发时application-prod.yml里spring.datasource.url是空的占位符。正确做法是永远用local-dbprofile 启动本地开发。这个 profile 在application-local-db.yml中预置了 H2 内存数据库配置spring: datasource: url: jdbc:h2:mem:flexible-ehr;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver h2: console: enabled: true path: /h2-console更重要的是local-dbprofile 会自动启用flyway并执行V1__init_schema.sql初始化所有事件表domain_event_entry、聚合表patient_profile和查询表encounter_list_view。而prodprofile 默认关闭 Flyway要求你手动执行 SQL 脚本——这对新手是灾难性的。实操技巧在 IDE 中配置 Run Configuration 时Program arguments 固定写--spring.profiles.activelocal-db --server.port8081。多开几个终端分别跑patient-service8081、clinical-note-service8082、api-gateway8080比用 Docker Compose 更易调试。4.2 陷阱二ehr-domain模块的Aggregate类必须有无参构造函数否则 Axon 启动失败flexible-ehr的PatientProfile类被Aggregate注解标记表示它是一个事件溯源聚合根。Axon 在启动时会反射创建其实例但如果你像常规 Java 类一样只写了带参构造函数// ❌ 错误Axon 无法实例化 public class PatientProfile { private final PatientId id; private String name; public PatientProfile(PatientId id, String name) { this.id id; this.name name; } }启动时会抛出NoSuchMethodException。正确写法是必须显式添加无参构造函数并用PersistenceConstructor标记它// ✅ 正确Axon 可实例化 public class PatientProfile { private PatientId id; private String name; // Axon 调用此构造函数 PersistenceConstructor public PatientProfile() {} // 领域方法通过事件发布创建实例 CommandHandler public PatientProfile(CreatePatientProfileCommand command) { apply(new PatientProfileCreatedEvent( command.getPatientId(), command.getName(), command.getGender() )); } // 事件处理方法重建状态 EventSourcingHandler public void on(PatientProfileCreatedEvent event) { this.id event.getPatientId(); this.name event.getName(); } }这个陷阱的根源在于Axon 的事件溯源机制需要从事件流中重建聚合根状态而重建过程依赖无参构造函数 EventSourcingHandler方法。很多开发者习惯用 Lombok 的AllArgsConstructor却忘了RequiredArgsConstructor不会生成无参构造函数导致服务启动即崩溃。4.3 陷阱三ui-framework的workflow.json必须放在src/main/resources/workflows/而非static/或templates/flexible-ehr的前端工作流引擎通过ResourceLoader加载workflow.json其路径是硬编码的classpath:workflows/{id}.json。如果你把文件放在src/main/resources/static/workflows/Spring Boot 的静态资源处理器会拦截请求返回 404放在src/main/resources/templates/Thymeleaf 会尝试解析 JSON 为模板报语法错误。正确路径只有src/main/resources/workflows/outpatient-prescription.json。且文件名必须与WorkflowDefinition.id字段完全一致区分大小写。某次我们因文件名写成Outpatient-Prescription.json首字母大写前端WorkflowEngine初始化时抛出WorkflowNotFoundException日志里只有一行Failed to load workflow: outpatient-prescription排查了 2 小时才发现是文件名大小写问题。实操技巧在ui-framework模块的pom.xml中添加资源过滤确保 JSON 文件被正确打包build resources resource directorysrc/main/resources/directory includes include**/*.json/include /includes /resource /resources /build4.4 陷阱四integration-adapters的fhir-adapter需要手动下载 HAPI-FHIR R4 规范 JAR否则FhirResourceMapper初始化失败flexible-ehr的fhir-adapter模块依赖 HAPI-FHIR 的hapi-fhir-structures-r4但该 JAR 不在 Maven 中央仓库需从 HAPI-FHIR 官网手动下载。如果你直接mvn clean install会遇到[ERROR] Failed to execute goal on project fhir-adapter: Could not resolve dependencies for project flexible-ehr:fhir-adapter:jar:1.0.0: Could not find artifact ca.uhn.hapi.fhir:hapi-fhir-structures-r4:jar:5.7.0解决方案访问 https://hapifhir.io/ 下载hapi-fhir-structures-r4-5.7.0.jar执行命令安装到本地 Maven 仓库mvn install:install-file \ -Dfilehapi-fhir-structures-r4-5.7.0.jar \ -DgroupIdca.uhn.hapi.fhir \ -DartifactIdhapi-fhir-structures-r4 \ -Dversion5.7.0 \ -Dpackagingjar在fhir-adapter/pom.xml中取消scopesystem/scope注释如果存在。这个坑之所以致命是因为它发生在编译期而非运行时新手往往以为是代码问题疯狂检查pom.xml依赖却忽略官网下载这一前置步骤。4.5 陷阱五api-gateway的 JWT 密钥必须用base64编码否则JwtDecoder初始化报IllegalArgumentExceptionflexible-ehr使用 JWT 进行服务间认证密钥配置在application.yml中flexible-ehr: jwt: secret: my-super-secret-key-for-dev-only但api-gateway的JwtDecoder要求密钥必须是 Base64 编码的 256 位字符串。直接写明文会导致Caused by: java.lang.IllegalArgumentException: The specified JWT signing key is not valid for HS256 algorithm.正确做法生成符合要求的密钥# 生成 32 字节随机密钥Base64 编码 openssl rand -base64 32 # 输出类似Xv2QzR8bKpLmNcYjFgHtIuOqWxZaBnCvDfEgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJk将输出粘贴到配置中flexible-ehr: jwt: secret: Xv2QzR8bKpLmNcYjFgHtIuOqWxZaBnCvDfEgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJk提示生产环境务必使用keytool生成 RSA 密钥对而非对称密钥。flexible-ehr的jwt配置支持publicKeyPath和privateKeyPath比对称密钥更安全。4.6 陷阱六clinical-note-service的DocumentTemplate必须预加载否则首次渲染模板报TemplateNotFoundExceptionflexible-ehr的临床文书模板如《门急诊病历书写规范》存储在数据库document_template表中但服务启动时不会自动加载。当你首次访问/notes/new?templateOUTPATIENT后端会尝试从数据库查询template_code OUTPATIENT若未找到直接返回 404而非提示“模板未配置”。解决方法启动服务后访问http://localhost:8082/h2-consoleH2 控制台执行 SQL 插入默认模板INSERT INTO document_template (id, template_code, name, content, created_by) VALUES (1, OUTPATIENT, 门诊病历模板, divh2门诊病历/h2p主诉${chiefComplaint}/pp现病史${historyOfPresentIllness}/p/div, system);重启clinical-note-service确保模板缓存生效。这个陷阱暴露了flexible-ehr的设计理念它不预设业务内容所有模板、编码、规则都需按需配置。这对灵活性是福音对新手却是陡峭的学习曲线。4.7 陷阱七生产部署时axon.eventhandling.processors.default.source必须设为eventStore而非embeddedEventStore本地开发用embeddedEventStore内存事件存储很爽但生产环境必须切换到eventStore数据库事件存储。若忘记修改服务启动时看似正常但所有事件只存内存重启即丢失导致数据一致性灾难。在application-prod.yml中必须显式配置axon: eventhandling: processors: default: source: eventStore # 关键不能是 embeddedEventStore eventstore: jdbc: table: domain_event_entry且需确保数据库中已创建domain_event_entry表建表 SQL 在flexible-ehr/sql/目录下。某次我们因漏配此项上线三天后服务重启所有历史事件消失不得不从备份恢复——而备份是 24 小时一次丢失了 23 小时的诊疗数据。最后一个经验flexible-ehr的.gitignore文件里/sql/目录被排除在外。这意味着所有数据库脚本建表、索引、初始数据都应提交到 Git。部署时运维人员只需执行sql/init-prod.sql即可完成环境准备。这是比“自动化脚本”更可靠的方式——因为 SQL 脚本是幂等的可反复执行而 Bash 脚本可能因环境差异失败。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑