3. FHIR订阅机制详解

FHIR 的订阅机制解决的是一个非常具体的问题:当某个临床事件发生后,如何让关心它的系统在不需要反复轮询的情况下及时获知。FHIR R4 用 Subscription 资源来描述“谁在什么条件下、通过什么通道、接收什么通知”。理解订阅,核心就是理解三件事:criteria 决定“什么事件算数”,channel 决定“通知怎么送”,status 决定“这条订阅当前是否生效”。

Subscription 资源结构

一个典型的 R4 Subscription 资源包含以下关键字段:

  • status:订阅的生命周期状态,取值 requested、active、error、off、entered-in-error。
  • reason:人类可读的订阅原因,便于运维排查。
  • criteria:订阅的过滤条件,通常是一个带查询参数的 FHIR 搜索 URL,例如 Observation?code=http://loinc.org|8867-4&status=final。
  • channel:通知通道,包含 type、endpoint、payload、header 等子字段。
  • end:订阅的过期时间,可选。

其中 channel.type 在 R4 中支持 rest-hook、websocket、email、sms、message 等。工程上最常用的是 rest-hook 和 websocket。rest-hook 表示服务端在事件触发时向订阅方提供的 HTTP(S) 端点发起 POST 请求;websocket 表示服务端与订阅方维持一条 WebSocket 长连接,事件发生时通过该连接推送消息。

criteria:把“事件”翻译成搜索条件

criteria 的本质是一个 FHIR 搜索表达式。服务端在资源发生 create、update 或 delete 时,会用该表达式去匹配变更后的资源。例如订阅“所有新产生的最终状态心率观察”,criteria 可写为:

Observation?code=http://loinc.org|8867-4&status=final

需要注意几个工程细节。第一,criteria 中的搜索参数必须被服务端支持,否则订阅可能进入 error 状态。第二,criteria 通常只对“资源级”变更生效,若需要更细粒度的条件(如仅当 valueQuantity 超过阈值),R4 的 criteria 表达能力有限,往往需要服务端扩展或改用 R5 的 SubscriptionTopic。第三,criteria 的匹配发生在服务端,订阅方无法在通道层再做过滤,因此条件应尽量精确,避免推送风暴。

channel:rest-hook 与 WebSocket 的差异

rest-hook 的工作方式是:服务端在事件触发时,向 channel.endpoint 指定的 URL 发送一个 HTTP POST,请求体通常是 Bundle(type=history 或 notification),其中包含触发事件的资源或仅包含通知头。订阅方必须返回 2xx,否则服务端可能重试或把订阅置为 error。rest-hook 的优点是实现简单、跨网络友好、易于与现有 Web 服务集成;缺点是每次事件都要建立 HTTP 连接,高并发下开销较大,且需要订阅方暴露可被服务端访问的端点。

WebSocket 的工作方式是:订阅方先与服务端建立 WebSocket 连接,服务端在事件发生时通过该连接推送消息。它的优点是长连接、低延迟、服务端可主动推送;缺点是连接需要维护、断线重连逻辑复杂、部分网络环境对 WebSocket 支持不佳。选择时通常遵循:低频、跨组织、需要审计的场景优先 rest-hook;高频、同一信任域、需要低延迟的场景可考虑 WebSocket。

FHIR 订阅:criteria 匹配 → channel 推送 → status 管理 FHIR 服务端 资源变更事件 create / update / delete criteria 匹配 Observation?code= 8867-4&status=final Subscription.status requested → active error / off entered-in-error channel 通道 rest-hook HTTP POST → endpoint payload: Bundle 需返回 2xx 失败可重试 / 置 error websocket 长连接,服务端主动推送 低延迟、高频场景 需断线重连与心跳 订阅方系统 接收通知 解析 Bundle / 消息 校验来源与签名 业务处理 入库 / 触发工作流 幂等与去重 反馈与恢复 重连 / 重新激活订阅 订阅状态回写 / 重新激活

订阅状态管理

Subscription.status 不是装饰字段,而是控制订阅是否真正生效的开关。典型生命周期如下:

  1. requested:订阅方创建 Subscription 后,服务端尚未确认或尚未激活。
  2. active:服务端已接受并开始按 criteria 推送。
  3. error:推送失败、endpoint 不可达或 criteria 无法解析,服务端标记异常。
  4. off:订阅被主动关闭或已过期。
  5. entered-in-error:订阅本身录入错误,不应再使用。

工程上要特别关注 error 状态。rest-hook 场景下,如果订阅方端点连续返回 5xx 或超时,服务端可能重试若干次后把订阅置为 error。此时订阅方需要主动查询 Subscription 资源,确认状态并重新激活。WebSocket 场景下,连接断开后服务端可能无法推送,订阅方应实现指数退避重连,并在重连成功后检查订阅是否仍为 active。

建议在集成层为每个订阅维护一个“订阅健康检查”任务:定期读取 Subscription.status,若为 error 或 off,则根据 reason 和 end 字段决定是重新激活还是重建订阅。

一个可落地的订阅配置示例

假设我们需要订阅“最终状态的心率 Observation”,并通过 rest-hook 推送到内部事件网关。可构造如下 Subscription:

{
  "resourceType": "Subscription",
  "status": "requested",
  "reason": "心率最终结果实时入湖",
  "criteria": "Observation?code=http://loinc.org|8867-4&status=final",
  "channel": {
    "type": "rest-hook",
    "endpoint": "https://gateway.example.org/fhir/notify",
    "payload": "application/fhir+json",
    "header": ["Authorization: Bearer <token>"]
  },
  "end": "2025-12-31T23:59:59Z"
}

服务端接受后会把 status 置为 active。当一条 status=final 且 code=8867-4 的 Observation 被创建或更新时,服务端向 endpoint 发送 POST,请求体为包含该 Observation 的 Bundle。订阅方收到后应先校验来源,再按 Observation.id 做幂等处理,避免重复入库。

criteria 中的搜索参数必须与服务端能力一致。若服务端不支持 status 或 code 的联合过滤,订阅可能无法激活或频繁进入 error。上线前应先用普通搜索请求验证 criteria 的可执行性。

选型与运维要点

rest-hook 与 WebSocket 并非互斥,很多集成架构会同时使用:低频、跨组织的订阅走 rest-hook,院内高频、低延迟的订阅走 WebSocket。无论哪种通道,都应把 Subscription 当作一等资源来管理:记录其 id、criteria、channel.endpoint、status 和 end,纳入监控与告警。对于 rest-hook,端点必须具备快速返回 2xx 的能力,重业务逻辑应异步化;对于 WebSocket,必须实现心跳、断线重连和消息去重。只有把 criteria 的精确性、channel 的可靠性和 status 的可观测性三者结合起来,FHIR 订阅才能在真实医疗集成中稳定运行。