4. 接口契约设计
接口契约是跨系统数据一致性的“法律文本”。它不描述某个系统内部怎么实现,而是规定两个系统之间“怎么说话”:请求长什么样、响应长什么样、字段如何对应、版本如何演进、出错如何表达、重复请求如何处理。契约一旦模糊,联调阶段就会出现“字段名一样但含义不同”“空值被当成零”“超时重试导致重复写入”等典型问题。本节围绕这五个要素,给出可落地的设计方法。
一、请求与响应结构:先定骨架,再填字段
检测结果接口通常承载“结果上报”和“结果查询”两类语义。以结果上报为例,请求体建议采用三层结构:报文头(header)承载元信息,业务体(body)承载检测结果,扩展区(ext)承载非通用字段。报文头至少包含:requestId(全局唯一请求标识)、tenantId(租户/机构)、timestamp(ISO 8601 带时区)、version(契约版本)、sourceSystem(来源系统)。业务体包含样本标识、检测项目、结果值、单位、参考区间、检测时间、仪器编号等。
响应结构应统一为“状态 + 数据 + 错误”三部分,避免有的接口返回布尔值、有的返回字符串。推荐固定字段:code(业务码)、message(可读描述)、data(成功时的载荷)、requestId(回显,便于链路追踪)。这样调用方只需一套解析逻辑,就能处理所有接口。
POST /api/v1/lab-results
{
"header": {
"requestId": "req-20240612-000871",
"tenantId": "HOSP-A",
"timestamp": "2024-06-12T09:31:20+08:00",
"version": "1.2",
"sourceSystem": "LIS-01"
},
"body": {
"sampleId": "S202406120031",
"testCode": "GLU",
"resultValue": "5.62",
"unit": "mmol/L",
"refRange": "3.90-6.10",
"resultTime": "2024-06-12T09:28:00+08:00",
"instrumentId": "AU5800-03"
},
"ext": { "operator": "zhangsan" }
}
二、字段映射:同名不同义是最大陷阱
字段映射不是简单改名,而是“语义对齐”。常见冲突有三类:单位不一致(mg/dL 与 mmol/L)、编码体系不一致(本地项目码与标准 LOINC 码)、空值语义不一致(“未检测”“检测失败”“结果为阴性”都可能被写成空字符串)。设计时应建立映射表,明确源字段、目标字段、转换规则和默认值策略。
| 源字段 | 目标字段 | 转换规则 | 空值策略 |
|---|---|---|---|
| test_code | testCode | 本地码→LOINC 映射表 | 映射失败则拒绝 |
| result | resultValue | 去空格、统一小数位 | 空串→null |
| unit | unit | UCUM 标准单位 | 缺失则拒绝 |
| ref_low/ref_high | refRange | 拼接为“低-高” | 缺失则置 null |
关键原则:能拒绝就不要猜测。对于单位缺失、项目码无法映射、结果值非数值的情况,应返回明确错误码,而不是用默认值兜底,否则会把错误数据“洗白”后写入下游。
三、版本管理:兼容优先,破坏性变更必须升版本
版本号建议采用“主版本.次版本”。次版本只做向后兼容的增量,例如新增可选字段、新增错误码;主版本用于破坏性变更,例如删除字段、修改字段类型、改变必填性。版本应体现在 URL 或报文头中,二者选其一并保持一致。若放在 URL,如 /api/v1/...,则路由清晰;若放在 header 的 version,则同一 URL 可承载多版本,但调试时不够直观。检测类系统通常建议 URL 带主版本、header 带次版本,兼顾可读性与灵活性。
版本升级时,旧版本至少保留一个过渡周期,并在响应中通过 Deprecation 或 Sunset 头提示下线时间。调用方应记录当前使用的版本,避免“悄悄升级”导致解析失败。
四、错误码:分层设计,便于定位与重试判断
错误码建议分为三层:传输层(HTTP 状态码)、业务层(如 RESULT_001)、字段层(指出具体字段)。业务错误码应具备可枚举、可文档化、可判断是否重试三个特征。例如:
40001参数校验失败——不可重试,需修正请求。40901重复请求——幂等命中,可视为成功。50001下游暂时不可用——可重试,建议指数退避。42201项目码无法映射——不可重试,需人工维护映射表。
响应中应同时给出 message 和 field,例如 {"code":"40001","message":"resultValue 必须为数值","field":"body.resultValue"},这样调用方无需翻文档就能定位问题。
五、幂等性:让重复请求不产生重复结果
跨系统调用中,超时重试几乎不可避免。幂等性设计的目标是:同一业务请求执行多次,结果与执行一次一致。最常用的方案是幂等键 + 去重表。幂等键由调用方生成,通常取 requestId 或“样本号+项目码+检测时间”的哈希。服务端收到请求后,先查去重表:若键已存在且处理成功,直接返回上次结果;若键存在但处理中,返回“处理中”状态;若不存在,则写入并处理。
幂等处理伪代码:
key = header.requestId
record = idempotentRepo.find(key)
if record == SUCCESS:
return record.response
if record == PROCESSING:
return {code: "40902", message: "请求处理中"}
idempotentRepo.insert(key, PROCESSING)
try:
result = handle(body)
idempotentRepo.update(key, SUCCESS, result)
return result
except:
idempotentRepo.update(key, FAILED)
raise
需要注意:幂等键必须有有效期,通常与业务重试窗口一致,例如 24 小时;过期后可清理,避免去重表无限膨胀。同时,幂等只保证“不重复产生副作用”,不保证“每次返回完全相同的响应体”,但应尽量回显首次结果。
六、把五个要素串起来:一份可执行的契约检查清单
在实际项目中,建议在接口评审时逐项核对以下内容,避免遗漏:
- 请求结构:header/body/ext 是否分层?requestId 是否全局唯一?timestamp 是否带时区?
- 响应结构:是否统一为 code/message/data/requestId?成功与失败是否同一套解析逻辑?
- 字段映射:单位、编码、空值三类冲突是否都有明确规则?映射失败是否拒绝而非兜底?
- 版本管理:主版本与次版本分别放在哪里?破坏性变更是否升主版本?旧版本是否有下线提示?
- 错误码:是否分层?是否可枚举?是否标注“可重试/不可重试”?是否给出 field 定位?
- 幂等性:幂等键是什么?去重表状态机是否完整?有效期与清理策略是否明确?
接口契约设计不是一次性文档,而是随业务演进的活协议。请求响应结构决定“能不能通”,字段映射决定“通得对不对”,版本管理决定“能不能持续通”,错误码决定“出问题能不能快速定位”,幂等性决定“重试会不会出事”。五者缺一,跨系统数据一致性就缺少可靠保障。