4. 事件生产者设计

事件生产者是事件驱动医疗集成链路的起点。它的职责不是“把数据发出去”这么简单,而是要在EHR(电子病历系统)内部准确识别“什么发生了变化、变化是否值得通知、通知谁、以什么格式通知”。如果生产者设计不当,下游再强的消费者也只能处理噪声、重复或语义缺失的事件。本节围绕四个关键问题展开:FHIR事件从哪里触发、资源变更如何捕获、消息中间件如何选型、事件载荷如何标准化。

一、从EHR触发FHIR事件:触发点决定事件语义

FHIR本身不是消息中间件,它定义的是资源模型与交互方式。事件驱动集成中,EHR通常通过三种方式产生事件:一是业务动作触发,例如患者入院登记、医嘱开立、检验报告审核发布;二是资源状态变化触发,例如Encounter.status从in-progress变为finished;三是订阅机制触发,即FHIR Subscription或Topic-based Subscription在资源满足条件时推送通知。

设计触发点时,必须区分“业务事件”与“资源事件”。业务事件是临床语义,如“报告已审核”;资源事件是技术语义,如“DiagnosticReport资源被更新”。一个业务事件可能对应多个资源变更,例如报告审核会同时更新DiagnosticReport、Observation和Provenance。若只按资源变更发事件,下游会收到碎片化通知;若只按业务事件发,又可能丢失资源级细节。推荐做法是:以业务事件为事件类型,以FHIR资源为载荷,并在事件头中携带资源引用与版本号。

触发时机上,应避免在数据库事务提交前发事件。若事务回滚,事件已发出,会造成下游脏数据。正确做法是采用事务性发件箱(Transactional Outbox):业务数据与待发事件写入同一数据库事务,再由独立投递器读取发件箱并发送到消息队列。这样可保证“业务成功则事件最终发出,业务失败则事件不发出”。

二、资源变更捕获:CDS Hooks与审计事件

资源变更捕获有三种常见手段。第一种是数据库变更数据捕获(CDC),通过解析数据库日志获取行级变化。它侵入性低,但只能看到表字段,难以还原FHIR资源语义。第二种是应用层拦截,在EHR服务层对FHIR资源的create/update/patch操作埋点。它语义清晰,但需要改造现有代码。第三种是审计事件,利用FHIR AuditEvent或IHE ATNA审计日志,从审计流中提取资源变更。审计事件覆盖面广,但延迟较高,且通常不包含完整资源内容,只适合做补充或合规追溯。

CDS Hooks则提供了另一种“临床工作流内触发”的机制。它不是在资源变更后异步通知,而是在临床决策点同步调用。例如医生打开处方界面时,EHR向CDS服务发起order-select钩子,CDS服务返回卡片提示。CDS Hooks适合实时干预,不适合作为异步集成的事件源。若要把CDS Hooks结果转化为事件,可在钩子响应后由EHR生成一条内部事件,再投递到消息队列。

选择捕获方式时,先问三个问题:变更是否需要完整FHIR资源?可接受的端到端延迟是多少?能否改造EHR应用层?若需要完整资源且延迟要求秒级,优先应用层拦截;若无法改造且可接受分钟级延迟,可用CDC加资源重建。

三、消息队列选型:Kafka与RabbitMQ的取舍

Kafka与RabbitMQ是医疗集成中最常见的两类中间件,但它们的模型不同。Kafka是分布式提交日志,消息按主题分区持久化,消费者按offset拉取,支持重放。RabbitMQ是传统消息代理,消息被消费后通常删除,支持复杂路由、优先级和延迟队列。

维度KafkaRabbitMQ
消息保留按时间/大小保留,可重放消费后删除,重放需额外设计
吞吐量高,适合大量事件流中高,适合任务分发
路由能力主题+分区,较简单Exchange+Binding,灵活
顺序保证分区内有序队列内有序
典型场景资源变更流、审计流、数据管道任务队列、通知分发、RPC

医疗集成中,若事件需要被多个下游独立消费、需要回放以重建状态、或需要长期留存用于审计,Kafka更合适。若事件是“一次性任务”,例如发送一条短信提醒、调用一次外部接口,且需要失败重试与死信队列,RabbitMQ更直接。实际项目中也可两者并用:Kafka承载FHIR资源变更主事件流,RabbitMQ承载通知类任务。

四、事件格式标准化:CloudEvents与FHIR Bundle

事件格式标准化要解决两个层面:信封(envelope)与载荷(payload)。信封描述“谁在什么时候发了什么类型的事件”,载荷承载FHIR资源。推荐信封采用CloudEvents 1.0规范,核心字段包括:id、source、type、subject、time、datacontenttype、data。其中type建议采用反向域名,例如cn.hospital.ehr.diagnosticreport.reviewed.v1。

载荷建议使用FHIR Bundle,类型为collection或transaction。若事件只通知“发生了什么”,可只携带资源引用;若下游需要完整数据,则携带资源本体。为避免大消息,可设置阈值:资源超过256KB时只发引用,由下游按需拉取。同时必须在信封中携带resourceVersion或meta.versionId,以便下游做幂等与顺序判断。

{
  "specversion": "1.0",
  "id": "evt-20240612-000871",
  "source": "/ehr/order-service",
  "type": "cn.hospital.ehr.diagnosticreport.reviewed.v1",
  "subject": "DiagnosticReport/12345",
  "time": "2024-06-12T10:23:41+08:00",
  "datacontenttype": "application/fhir+json",
  "data": {
    "resourceType": "Bundle",
    "type": "collection",
    "entry": [
      { "resource": { "resourceType": "DiagnosticReport", "id": "12345", "status": "final" } }
    ]
  }
}

标准化还包括命名与版本策略。事件类型应带版本号,如.v1;资源结构变化时新增版本而非修改旧版本。消费者按版本路由,避免升级导致全链路中断。此外,所有事件应携带traceparent或correlationId,便于跨系统追踪。

EHR事件生产者设计:触发 → 捕获 → 传输 → 标准化 EHR 业务系统 入院登记 / 医嘱开立 报告审核 / 状态变更 CDS Hooks 决策点 资源变更捕获 应用层拦截(推荐) 数据库 CDC AuditEvent 审计流 事务性发件箱 业务数据 + 事件 同一数据库事务 投递器异步发送 消息队列 Kafka 主事件流 RabbitMQ 任务 死信 / 重试 事件格式标准化(CloudEvents 信封 + FHIR Bundle 载荷) 信封字段 id / source / type subject / time datacontenttype traceparent 载荷内容 FHIR Bundle resourceType / id meta.versionId 大资源仅发引用 版本与路由 type 带 .v1 版本 消费者按版本订阅 幂等键 = id 顺序键 = subject 触发点决定事件语义 · 发件箱保证一致性 · 队列选型匹配消费模式 · 标准化保证可路由与可重放

最后,生产者设计必须包含可观测性。每个事件应记录发送时间、队列确认时间、重试次数与失败原因。建议在发件箱投递器中暴露指标:待发送事件数、发送延迟P95、失败率。当失败率超过阈值时告警,而不是静默重试。只有把触发、捕获、传输、标准化四层都设计清楚,事件生产者才能成为可靠的数据源头,而不是集成链路中最薄弱的环节。