2. FHIR资源模型与RESTful API设计

FHIR 服务端平台的核心工作,可以概括为两件事:把临床与业务数据组织成资源(Resource),再通过一套统一的 RESTful API 暴露出去。理解资源模型与 API 边界,是后续实现存储、检索、版本控制和互操作的前提。本节从资源的结构讲起,逐步过渡到 RESTful 交互规范,最后落到平台 API 的边界划分。

一、资源、元素与扩展:数据的最小组织单位

FHIR 中一切可独立寻址、可独立交换的数据单元都是资源。每个资源有唯一的资源类型(如 Patient、Observation、Encounter)和逻辑 ID,二者共同构成资源的身份。资源内部由元素(Element)组成,元素可以嵌套,形成树状结构。例如 Patient 资源包含 identifier、name、gender、birthDate 等元素,其中 name 又包含 family、given 等子元素。

FHIR 对元素做了明确的“必选/可选/基数”约束。以 Observation 为例,status 和 code 是必选(基数 1..1),value[x] 是可选(0..1),component 可重复(0..*)。服务端在写入时必须校验这些基数,否则会产生不合规数据。

当标准元素无法表达业务需求时,FHIR 提供了扩展(Extension)机制。扩展通过 extension 数组挂载在任意元素上,用 url 标识语义、用 value[x] 承载值。扩展是 FHIR 保持“80% 通用 + 20% 可扩展”的关键设计,但服务端需要决定:哪些扩展允许写入、是否参与检索、是否在返回时保留。

扩展不是“随便加字段”。生产环境中应优先使用已注册的扩展 URL,并明确其是否可被检索,否则会形成难以治理的“影子字段”。

二、引用与版本:资源之间的关系与时间维度

资源之间通过引用(Reference)建立关系。引用有两种常见形式:字面引用(如 Patient/123)和逻辑引用(包含 identifier 与 type,用于跨系统标识)。服务端在处理引用时,需要判断是“仅存储字符串”还是“校验目标存在”,这直接影响写入性能与数据一致性。

版本(Version)是 FHIR 资源的时间维度。每个资源在每次更新后生成新的版本,通过 meta.versionId 标识,并可用 meta.lastUpdated 记录时间。服务端通常需要支持版本化读取(vread)和历史查询(_history),以便审计与回溯。乐观锁也依赖版本:更新时携带 If-Match,版本不匹配则返回 409。

FHIR 资源模型与 RESTful API 交互全景 资源模型 Patient / 123 identifier: 1..* name: 0..* gender: 0..1 birthDate: 0..1 extension: 0..* meta.versionId: "3" Reference 引用 Observation/456 subject → Patient/123 encounter → Encounter/789 版本历史 v1 → v2 → v3 _history / vread RESTful 交互 GET /Patient/123 读取 · 200 / 404 POST /Patient 创建 · 201 + Location PUT /Patient/123 更新 · 200 / 409 DELETE /Patient/123 删除 · 204 / 410 GET /Patient?name=张 检索 · Bundle 结果 条件更新 / 条件删除 平台 API 边界 标准 FHIR API 资源 CRUD / 检索 / 版本 对外互操作契约 平台扩展 API 批量导入 / 审计 / 统计 非 FHIR 语义,需隔离 内部服务 API 存储 / 索引 / 消息 不直接暴露给外部 边界原则 • 标准 API 保持 FHIR 合规 • 扩展 API 独立命名空间 • 内部实现不泄漏到契约 映射 引用 暴露 隔离

三、RESTful 交互规范:动词、状态码与检索

FHIR 的 RESTful 接口遵循 HTTP 语义,但有几个必须掌握的关键点。第一,动词与资源路径的对应:GET 读取、POST 创建、PUT 更新、DELETE 删除。第二,状态码的语义:创建成功返回 201 并带 Location 头;读取不存在返回 404;更新版本冲突返回 409;删除后再次读取返回 410(Gone)而非 404,这是 FHIR 的明确约定。第三,检索通过查询参数完成,如 GET /Patient?name=张&birthdate=ge1990-01-01,返回的是 Bundle 资源,包含 total、entry 和分页链接。

服务端实现检索时,需要把查询参数映射到存储层的索引。例如 name 参数通常映射到姓名索引表,birthdate 配合 ge 前缀映射为范围查询。分页通过 _count 和 _offset(或 _page)控制,返回的 Bundle 中 link 数组提供 next、self 等链接。

四、平台 API 边界:标准、扩展与内部的分离

一个成熟的 FHIR 服务端平台,API 不应只有一层。建议划分为三类边界:标准 FHIR API 对外提供资源 CRUD、检索、版本与历史,必须严格符合 FHIR 规范;平台扩展 API 承载批量导入、审计查询、统计报表等非 FHIR 语义能力,应使用独立路径前缀(如 /platform/)与独立鉴权;内部服务 API 用于存储、索引、消息队列之间的通信,不对外暴露。

边界划分的核心判断依据是:该接口是否属于 FHIR 互操作契约。属于则必须合规,不属于则必须隔离,避免外部调用方误以为扩展接口也是标准 FHIR 接口。例如批量导入接口如果放在 /Patient/$import 下,容易被误认为 FHIR 操作,更稳妥的做法是放在 /platform/import/Patient。

常见错误:把内部数据库主键直接作为资源 ID 暴露,或让扩展 API 返回非 FHIR 结构的 JSON。前者造成 ID 语义泄漏,后者破坏客户端对 FHIR 契约的预期。

五、一个可落地的资源写入与校验流程

以创建一条 Observation 为例,服务端处理流程如下:接收 POST 请求体 → 解析 JSON 并校验资源类型 → 校验必选元素(status、code)与基数 → 校验引用目标是否存在(可配置为弱校验)→ 校验扩展 URL 是否在白名单 → 生成逻辑 ID 与 versionId=1 → 写入存储并建立检索索引 → 返回 201 与 Location 头。若任一步失败,返回 400 或 422 并附带 OperationOutcome 说明具体问题。

POST /Observation
Content-Type: application/fhir+json

{
  "resourceType": "Observation",
  "status": "final",
  "code": { "coding": [{ "system": "http://loinc.org", "code": "8867-4" }] },
  "subject": { "reference": "Patient/123" },
  "valueQuantity": { "value": 72, "unit": "beats/min" }
}

→ 201 Created
Location: /Observation/456/_history/1

这段示例中,status 与 code 是必选,subject 是引用,valueQuantity 是 value[x] 的一种具体形态。服务端返回的 Location 同时包含资源 ID 与版本,客户端可据此进行后续的版本化读取。

掌握资源模型与 API 边界后,后续的存储设计、检索优化、版本治理和互操作测试才有稳定的基础。资源是数据契约,RESTful 是交互契约,边界是治理契约,三者共同构成 FHIR 服务端平台的骨架。