医匠透析 HDIS · 开放接口对接规范 v1(草案)
签名鉴权、推送接口、患者标识、返回码与联调步骤。适用于医院信息科及 HIS / LIS / PACS 厂商对接工程师。
本规范面向医院信息科,以及负责 HIS / LIS / PACS 对接的厂商工程师。
接入方在检验、检查报告审核发布后,通过 HTTPS 调用本规范定义的接口,将报告推送至 HDIS。报告供透析科室在系统中查阅,并作为各地血液透析质控中心上报的数据来源。
对接采用接入方主动推送的方式。HDIS 不接入医院内网,不访问接入方数据库,不部署前置机。
0. 版本与状态
| 项 | 值 |
|---|---|
| 规范版本 | v1.0.3-draft(2026-09-15) |
| 字典版本 | 以 GET /open/v1/dict/lab-items 返回的 version 为准 |
| 状态 | 草案。接口路径与字段已确定;正式发布前如有调整,将提前通知并保留旧版本兼容期 |
1. 基本原则
- 只推送原始结果。 Kt/V、URR、nPCR、钙磷乘积、体表面积、β2 微球蛋白下降率等派生指标,需要结合透析时长、超滤量、透后体重等治疗数据计算,由 HDIS 完成。接入方只需推送透前/透后尿素、钙、磷等原始检验值。
- 每份报告须有稳定的唯一编号。 每份报告以
externalId(接入方系统中的报告号或条码号)标识。以相同externalId再次推送视为覆盖更新,不会产生重复记录。重试、补推和历史数据回补均依赖该编号,同一份报告的externalId不得变化;如每次推送临时生成编号,同一份报告会在 HDIS 中重复出现。 - 患者未匹配时照常接收。 无法匹配到患者的报告进入「待认领」队列,由科室人工确认,接口仍返回成功,无需重新推送。
- 批量推送逐条处理。 同一请求中的报告逐条校验、逐条入库,个别报告出错不影响其余报告,也不会整批回滚。响应中逐条返回处理结果,出错的报告附带错误说明。接入方应按结果逐条处理,不应因个别失败而整批重推。
- 不接收影像文件。 检查报告只推送报告文本和结构化测值,不接收 DICOM 图像或 JPEG 预览图,接口也不提供文件上传。医生阅片通过接入方 PACS 自身的阅片入口进行。
2. 接入准备
2.1 基础地址
| 部署方式 | 基础地址 |
|---|---|
| 云端部署 | https://hdis.api.yijiangsoft.com/open/v1 |
| 院内离线部署 | 院内 HDIS 服务器地址,由医匠透析项目组另行通知 |
完整请求地址为基础地址加接口路径。以联调自检接口为例:
GET https://hdis.api.yijiangsoft.com/open/v1/ping签名时使用的「请求路径」包含 /open/v1 前缀,如 /open/v1/ping,见 §3.1。
2.2 凭据
接入申请通过后,HDIS 为每家医院分配凭据,凭据不得跨医院复用:
| 项 | 说明 |
|---|---|
appId | 应用标识,明文,随请求头发送 |
appSecret | 应用密钥,仅用于在接入方本地计算签名,不得出现在任何请求中 |
联调阶段使用沙箱凭据,可自由推送测试数据;联调通过后切换为生产凭据。两类凭据使用同一基础地址,调用 GET /open/v1/ping 时返回的 sandbox 字段标明当前凭据类型。
接入方需同时提供服务器出口 IP(可多个),用于配置 IP 白名单。出口 IP 变更前请提前告知,否则请求将被拒绝。
2.3 接口文档
接口定义同时发布在 Apifox 在线文档,可查看全部字段、请求示例并导出 OpenAPI 文件。
每个 appId 在 HDIS 中绑定唯一一家医院(租户)。数据归属哪家医院仅由 appId 决定,请求中携带的任何机构标识均不影响归属。
3. 鉴权:请求签名
每个请求须携带以下四个请求头:
| 请求头 | 说明 |
|---|---|
X-HDIS-AppId | 分配的 appId |
X-HDIS-Timestamp | 毫秒时间戳,与服务器时间偏差不得超过 ±5 分钟,请确保服务器时钟准确 |
X-HDIS-Nonce | 随机字符串,不超过 64 个字符,10 分钟内不得重复,建议使用 UUID |
X-HDIS-Signature | 签名值,小写十六进制 |
3.1 待签名串
待签名串由以下六行以 \n 连接而成,最后一行末尾不加换行符:
{HTTP方法大写}
{请求路径}
{规范化查询串}
{请求体的SHA-256十六进制小写}
{Timestamp}
{Nonce}- 请求路径:如
/open/v1/lab/reports,不含域名和查询串。 规范化查询串:以实际发送的查询串原文(
?之后的部分)为输入,只调整顺序,不做编码或解码:- 按
&拆分为若干名=值; - 按参数名解码后的字典序升序排列;参数名相同时,按原始值排序;
- 使用原始的参数名与参数值(保留发送时的百分号编码),拼接为
k1=v1&k2=v2; - 不含
=的参数(如?flag)视为空值,输出为flag=; - 无查询参数时为空字符串,该行留空。
注意:规范化时不得再次编码。 查询参数的 URL 编码在构造请求 URL 时已完成,规范化阶段不改变任何字节。如将
%E5%BC%A0再次编码为%25E5%25BC%25A0,计算出的签名将与服务端不一致,含中文或空格的查询参数会全部返回 701;纯 ASCII 参数不受影响,因此该问题在初期联调中不易发现。示例见 §3.3。- 按
- 请求体哈希:对请求体的原始字节计算 SHA-256。
GET等无请求体的请求对空字节数组计算,结果固定为e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。
3.2 签名
signature = HMAC-SHA256(key = appSecret, data = 待签名串) → 转小写十六进制3.3 签名示例
以下示例可用于核对本地实现。
appId = hdis_demo
appSecret = 0123456789abcdef
方法 = POST
路径 = /open/v1/lab/reports
查询串 = (空)
请求体 = {"reports":[]}
Timestamp = 1756684800000
Nonce = 7f3d0c1e-0000-4000-8000-000000000001待签名串(⏎ 表示换行符,仅作示意,不属于待签名串内容):
POST⏎
/open/v1/lab/reports⏎
⏎
486a8f07711a55700e1ed31c536217c17f3135cc6340b4830bcb4f87bbc7072a⏎
1756684800000⏎
7f3d0c1e-0000-4000-8000-000000000001第 4 行是 {"reports":[]}(14 字节)的 SHA-256。如本地计算结果不同,请检查请求体字节,常见原因有:带 UTF-8 BOM、含多余空白字符、未使用 UTF-8 编码。
该示例的签名为 560a44adb48f4714966a0719ee9d62c806278880a82ee5a7d4c5f6fcd1a49484。
含中文查询参数的示例
纯 ASCII 查询参数无法检验规范化查询串的实现是否正确,请同时使用本示例核对。请求如下:
GET /open/v1/reconcile/lab?name=张三&date=2026-09-01HTTP 客户端实际发送的查询串(张三 经 UTF-8 百分号编码):
name=%E5%BC%A0%E4%B8%89&date=2026-09-01按 §3.1 规范化:按参数名排序(date 在 name 之前),保留原有编码,得到第 3 行:
date=2026-09-01&name=%E5%BC%A0%E4%B8%89appId = hdis_demo
appSecret = 0123456789abcdef
Timestamp = 1756684800000
Nonce = 7f3d0c1e-0000-4000-8000-000000000002待签名串:
GET⏎
/open/v1/reconcile/lab⏎
date=2026-09-01&name=%E5%BC%A0%E4%B8%89⏎
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855⏎
1756684800000⏎
7f3d0c1e-0000-4000-8000-000000000002签名:
047f33f0e1b6dba5fdc78a09c9c2ac344a1220709d6f6de05363eaedd3b94dc7如计算结果不同,请先检查第 3 行,常见原因有:
%E5%BC%A0%E4%B8%89被再次编码为%25E5%25BC%25A0...;- 参数未按参数名排序;
- 中文未经编码直接写入。
排查签名问题可使用以下工具:
- 签名参考实现
sign-demo.mjs(随凭据提供):执行node sign-demo.mjs --selftest运行自检,加--dry参数只打印待签名串、不发送请求。 GET /open/v1/ping:返回服务端计算出的待签名串,可逐行比对。
3.4 Java 示例
static String sign(String appSecret, String method, String path, String canonicalQuery,
byte[] body, long timestamp, String nonce) throws Exception {
MessageDigest sha = MessageDigest.getInstance("SHA-256");
String bodyHash = hex(sha.digest(body == null ? new byte[0] : body));
String toSign = String.join("\n", method.toUpperCase(), path, canonicalQuery,
bodyHash, String.valueOf(timestamp), nonce);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return hex(mac.doFinal(toSign.getBytes(StandardCharsets.UTF_8)));
}
static String hex(byte[] b) {
StringBuilder sb = new StringBuilder(b.length * 2);
for (byte x : b) sb.append(Character.forDigit((x >> 4) & 0xF, 16)).append(Character.forDigit(x & 0xF, 16));
return sb.toString();
}
/**
* 规范化查询串(§3.1)。入参为实际发送的查询串原文(不含 ?),
* 如 "name=%E5%BC%A0%E4%B8%89&date=2026-09-01"。只调整顺序,不做编码或解码。
*/
static String canonicalQuery(String rawQuery) {
if (rawQuery == null || rawQuery.isEmpty()) return "";
List<String[]> pairs = new ArrayList<>();
for (String part : rawQuery.split("&")) {
if (part.isEmpty()) continue;
int eq = part.indexOf('=');
pairs.add(eq < 0 ? new String[]{part, ""}
: new String[]{part.substring(0, eq), part.substring(eq + 1)});
}
// 按解码后的参数名排序(使 %61 与 a 排序一致),拼接时仍使用原始的名与值。
pairs.sort(Comparator.<String[], String>comparing(p -> URLDecoder.decode(p[0], StandardCharsets.UTF_8))
.thenComparing(p -> p[1]));
StringJoiner sj = new StringJoiner("&");
for (String[] p : pairs) sj.add(p[0] + "=" + p[1]);
return sj.toString();
}3.5 C# 示例
static string Sign(string appSecret, string method, string path, string canonicalQuery,
byte[] body, long timestamp, string nonce) {
using var sha = SHA256.Create();
var bodyHash = Convert.ToHexString(sha.ComputeHash(body ?? Array.Empty<byte>())).ToLowerInvariant();
var toSign = string.Join("\n", method.ToUpperInvariant(), path, canonicalQuery,
bodyHash, timestamp.ToString(), nonce);
using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(appSecret));
return Convert.ToHexString(mac.ComputeHash(Encoding.UTF8.GetBytes(toSign))).ToLowerInvariant();
}
// 规范化查询串(§3.1)。入参为实际发送的查询串原文(不含 ?),只调整顺序,不做编码或解码。
static string CanonicalQuery(string rawQuery) {
if (string.IsNullOrEmpty(rawQuery)) return "";
var pairs = rawQuery.Split('&', StringSplitOptions.RemoveEmptyEntries).Select(part => {
var eq = part.IndexOf('=');
return eq < 0 ? (Name: part, Value: "") : (Name: part[..eq], Value: part[(eq + 1)..]);
})
// 按解码后的参数名排序,须使用 StringComparer.Ordinal;区域相关的默认比较器会得到不同的顺序。
.OrderBy(p => Uri.UnescapeDataString(p.Name.Replace("+", " ")), StringComparer.Ordinal)
.ThenBy(p => p.Value, StringComparer.Ordinal);
return string.Join("&", pairs.Select(p => $"{p.Name}={p.Value}"));
}4. 通用约定
4.1 请求与响应
- 传输协议:HTTPS,
Content-Type: application/json; charset=utf-8。 - HTTP 状态码固定为 200,业务结果以响应体中的
code为准:
{ "code": 200, "msg": "操作成功", "data": { }, "timestamp": 1756684800123 }4.2 返回码
| code | 含义 | 处理方式 |
|---|---|---|
| 200 | 成功,包括部分成功;逐条结果见 data.results | 按逐条结果处理 |
| 400 | 参数错误(JSON 格式错误、缺少必填项、超出批量上限) | 修正数据后再推送,不应原样重试 |
| 700 | appId 不存在或已停用 | 联系医匠透析项目组,不应重试 |
| 701 | 签名校验失败 | 调用 /open/v1/ping 比对待签名串 |
| 702 | 时间戳偏差超过 ±5 分钟 | 校准服务器时钟 |
| 703 | Nonce 重复 | 每次请求生成新的 Nonce |
| 704 | 来源 IP 不在白名单内 | 向医匠透析项目组报备出口 IP |
| 705 | 超出调用频率限制 | 按 §4.4 退避后重试 |
| 500 / 600 | HDIS 服务端错误 | 可以重试;接口幂等,重试不会产生重复数据 |
4.3 时间与数值格式
日期时间:
yyyy-MM-dd HH:mm:ss,24 小时制,东八区,不带时区后缀。注意:日期时间格式严格匹配。 以下写法均无法解析:
2026-09-01 07:35:00.000(带毫秒)、2026-09-01T07:35:00(ISO 格式的T分隔符)、2026-09-01T07:35:00+08:00(带时区)、2026/09/01 07:35:00。时间字段格式不符的报告将返回 400 或REJECTED,不会入库。部分 LIS 的时间字段默认带毫秒,推送前须转换为上述格式。- 日期:
yyyy-MM-dd,同样严格匹配,不接受2026/09/01、20260901等写法。 - 数值:可使用 JSON 数字或字符串。带
<、>的结果(如<0.01)以字符串原样传入,HDIS 保留原文。 - 空值:值未知的字段直接省略,不以
""或0占位。
4.4 批量与限流
- 单次请求不超过 100 份报告,请求体不超过 4 MB,超限返回 400。
- 每份报告的检验项或测值不超过 500 项,超出时该份报告返回
REJECTED。 - 调用频率默认上限为 60 次/分钟,按
appId计。回补历史数据前请提前告知,可临时调高上限。 - 频率按请求次数统计,失败请求同样计入,包括签名错误、时间戳超限、Nonce 重复而被拒绝的请求。凭据配置有误时,请先排查原因再重试;高频重试会耗尽配额并返回 705,增加排查难度。
- 返回 705 时,按 1 秒、2 秒、4 秒、8 秒的间隔退避重试。
4.5 推送时机
- 建议在报告审核发布时推送,实现准实时同步。
- 也可定时批量推送,如每 15 分钟推送一次新发布的报告。
- 报告修订(修改结果、撤回后重发):以相同
externalId重新推送,HDIS 覆盖原记录并保留修改记录。 - 新医院上线需回补历史数据时:使用同一接口分批推送,HDIS 按
externalId去重。
5. 患者标识
5.1 患者字段
每份报告必须包含 patient 对象,以身份证号作为主要标识:
"patient": {
"idCard": "330102199001011234",
"name": "张三",
"gender": "M",
"birthDate": "1990-01-01",
"hisPatientId": "0000123456",
"visitNo": "ZY2026090100123",
"visitType": "INPATIENT"
}| 字段 | 必填 | 说明 |
|---|---|---|
idCard | 是(无身份证号时见 §5.3) | 18 位身份证号,明文传输(全程 HTTPS)。服务端只保存不可逆摘要和脱敏展示值,不保存明文,不写入日志 |
name | 是 | 姓名,人工认领时用于核对 |
gender | 否 | M / F / U |
birthDate | 否 | 身份证号完整时可不传 |
hisPatientId | 否 | 接入方系统的患者主键。建议传入:人工认领一次后,后续报告可直接按此编号匹配 |
visitNo | 否 | 住院号或门诊号,仅在人工认领时供核对,不参与自动匹配(HDIS 患者档案不含住院号字段) |
visitType | 否 | INPATIENT 住院 / OUTPATIENT 门诊 / PHYSICAL 体检 |
5.2 匹配规则
HDIS 按以下顺序精确匹配,前一级未命中时进入下一级:
idCard:比对身份证号的不可逆摘要,在本院唯一命中一名患者时匹配成功。hisPatientId:该编号此前已人工认领时,直接匹配到所认领的患者。name+gender+birthDate:三项完全一致且唯一命中时匹配成功;缺少任一项则不进行此级匹配。
已认领的 hisPatientId 优先于身份证号匹配。 hisPatientId 一经人工认领,此后携带该编号的报告直接归入所认领的患者,不再按身份证号匹配。人工认领视为最终确认,可纠正身份证号录入错误、一人多档等情况。如发现认领有误,请联系医匠透析项目组更正,重新推送不会改变匹配结果。
各级均未命中,或某一级命中多名患者(表示 HDIS 中存在重复档案)时,报告进入待认领队列,不做推测性归档。visitNo、visitType 仅在待认领页面展示供核对,不参与自动匹配。
HDIS 不做模糊匹配,不按姓名相似度或同音字推测患者。检验结果归入错误患者的后果远比数据暂缺严重。
5.3 待认领
未匹配时的响应:data.results[].matched 为 false,patientRef 为空。报告已正常保存,科室在 HDIS「待认领」页确认后自动归入对应患者,无需重新推送;以相同 externalId 重推只会覆盖同一条待认领记录。
无身份证号的患者(如新生儿、无证件患者、历史档案缺失):可不传 idCard,但必须传 hisPatientId,并尽量提供 name、gender、birthDate。此类报告直接进入待认领队列,由科室确认,不会被丢弃。
同一患者可能产生两条待认领记录。 待认领记录按推送时提供的标识建立:携带 hisPatientId 的报告按 hisPatientId 归集,仅携带 idCard 的报告按身份证号归集。同一患者先后以这两种标识推送时,会形成两条待认领记录,需分别认领;认领后均归入同一患者,数据不会重复。每次推送都携带稳定的 hisPatientId 即可避免这种情况。
6. 接口
6.1 推送检验报告 POST /open/v1/lab/reports
一份检验报告由报告头和若干检验项组成。
请求示例
{
"reports": [
{
"externalId": "LIS20260901000123",
"reportNo": "20260901000123",
"patient": { "idCard": "330102199001011234", "name": "张三", "hisPatientId": "0000123456" },
"panelName": "肾功能+电解质",
"specimen": "SERUM",
"collectedAt": "2026-09-01 07:35:00",
"reportedAt": "2026-09-01 09:12:00",
"status": "FINAL",
"labName": "检验科",
"reporter": "李四",
"reviewer": "王五",
"items": [
{ "itemCode": "0231", "itemName": "血红蛋白", "value": 98, "unit": "g/L", "refLow": 115, "refHigh": 150, "abnormalFlag": "L" },
{ "itemCode": "0455", "itemName": "血钾", "value": 5.8, "unit": "mmol/L", "refLow": 3.5, "refHigh": 5.3, "abnormalFlag": "H" },
{ "itemCode": "0912", "itemName": "尿素", "value": 22.4, "unit": "mmol/L", "refLow": 2.9, "refHigh": 8.2, "abnormalFlag": "H" },
{ "itemCode": "1180", "itemName": "HBsAg", "value": "阴性", "resultType": "QUALITATIVE" }
]
}
]
}报告头字段
| 字段 | 必填 | 说明 |
|---|---|---|
externalId | 是 | 接入方系统中永久不变的报告唯一标识(报告号或条码号),用作幂等键 |
reportNo | 否 | 报告单号,供医护人员查看 |
patient | 是 | 见 §5 |
panelName | 否 | 报告名称(组合项目名称),如「肾功能」「血常规」 |
specimen | 否 | 标本类型:SERUM 血清 / PLASMA 血浆 / WHOLE_BLOOD 全血 / PERIPHERAL_BLOOD 外周血 / VENOUS_BLOOD 静脉血 / ARTERIAL_BLOOD 动脉血 / URINE 尿液 / SPUTUM 痰 / ASCITES 腹水 / PLEURAL_FLUID 胸水 / CSF 脑脊液 / SECRETION 分泌物 / CATHETER 导管 / CATHETER_BLOOD 导管血 |
collectedAt | 是 | 采样时间。HDIS 据此确定采样时点,并将报告关联到对应的透析治疗,须准确填写,不得以报告发布时间代替 |
reportedAt | 否 | 报告发布时间 |
status | 否 | FINAL 正式 / PRELIMINARY 初步 / CORRECTED 修订 / CANCELLED 作废;缺省为 FINAL |
labName reporter reviewer | 否 | 检验科室、检验者、审核者 |
items | 是 | 至少包含 1 项 |
检验项字段
| 字段 | 必填 | 说明 |
|---|---|---|
itemCode | 是 | 接入方院内的项目代码,无需转换为 HDIS 编码,映射关系由 HDIS 配置 |
itemName | 是 | 院内项目名称,新项目首次出现时据此进行人工映射 |
value | 是 | 数值或文本;<0.01 等结果以字符串原样传入 |
unit | 否 | 院内原始单位,请勿换算后传入 |
refLow refHigh refText | 否 | 参考范围;定性项目使用 refText,如「阴性」 |
abnormalFlag | 否 | H 偏高 / L 偏低 / N 正常 / HH 危急高 / LL 危急低 |
resultType | 否 | NUMERIC / QUALITATIVE / TEXT;缺省时按 value 的类型推断 |
hdisCode | 否 | HDIS 标准项编码(见 §6.3)。传入后无需配置映射 |
未映射的项目照常保存。 itemCode 尚未配置映射时,结果照常入库并按院内名称展示,暂不纳入趋势图和质控上报。映射配置完成后历史数据自动补齐,无需重新推送。
响应示例
{
"code": 200, "msg": "操作成功",
"data": {
"total": 2, "accepted": 2, "rejected": 0,
"results": [
{ "externalId": "LIS20260901000123", "status": "CREATED", "matched": true,
"patientRef": "P202600123", "itemsMapped": 3, "itemsUnmapped": 1 },
{ "externalId": "LIS20260901000124", "status": "UPDATED", "matched": false,
"pendingReason": "身份证号在本院透析患者中未找到,已进待认领" }
]
}
}status 取值:
| 值 | 含义 |
|---|---|
CREATED | 新建 |
UPDATED | 按 externalId 覆盖更新 |
IGNORED | 内容与已有记录完全相同,未做变更 |
REJECTED | 该条报告有误,原因见 error |
6.2 推送检查报告 POST /open/v1/exam/reports
适用于超声、X 线、心电图、骨密度等检查。只接收报告内容,不接收影像。
请求示例
{
"reports": [
{
"externalId": "PACS20260828000077",
"patient": { "idCard": "330102199001011234", "name": "张三" },
"examType": "ECHO",
"examName": "经胸超声心动图",
"bodySite": "心脏",
"method": "经胸二维超声",
"examedAt": "2026-08-28 10:20:00",
"reportedAt": "2026-08-28 11:05:00",
"findings": "左房内径 41mm,左室舒张末期内径 52mm,室间隔厚度 12mm……",
"conclusion": "左室肥厚,二尖瓣轻度返流,左室舒张功能减低。",
"reporter": "赵六",
"items": [
{ "hdisCode": "EF", "value": 58, "unit": "%" },
{ "hdisCode": "LVIDd", "value": 52, "unit": "mm" },
{ "hdisCode": "IVSd", "value": 12, "unit": "mm" },
{ "hdisCode": "MITRAL_REGURG", "value": "轻度", "resultType": "QUALITATIVE" }
]
}
]
}| 字段 | 必填 | 说明 |
|---|---|---|
externalId | 是 | 检查号或申请单号,须稳定唯一 |
examType | 是 | 取值见 §6.3 返回的 examTypes;无对应类型时填 OTHER_EXAM,并填写 examName |
examedAt | 是 | 检查时间 |
findings | 否 | 检查所见 |
conclusion | 是 | 诊断结论,临床查阅的主要内容 |
items | 否 | 结构化测值;只提供报告文本也可正常使用 |
reportUrl | 否 | 接入方 PACS 阅片页面的链接,医生点击后跳转至接入方系统查看影像。HDIS 不下载、不缓存、不代理影像 |
items[].hdisCode 使用 HDIS 标准项编码(如 EF、LVIDd、IMT);也可与检验报告相同,传入 itemCode + itemName 经映射对应。
6.3 拉取标准项字典 GET /open/v1/dict/lab-items
返回全部标准检验、检查项目及当前字典版本号 version,用于建立映射对照。可通过 ?category=CBC 按类别筛选。接入方可保存 version,与下次返回值比较,判断字典是否有更新。
{ "code": 200, "data": {
"version": "1.0.0",
"items": [ { "code": "HGB", "name": "血红蛋白", "category": "CBC",
"valueType": "numeric", "unit": "g/L", "aliases": ["HGB","Hb","血色素"] } ],
"examTypes": [ { "code": "ECHO", "name": "心脏超声" } ]
} }6.4 日对账 GET /open/v1/reconcile/lab?date=2026-09-01
建议每日调用一次。接口返回 HDIS 当日接收的报告数量、externalId 清单及摘要。接入方与本地当日发布的报告比对,如有遗漏,自行补推即可。
{ "code": 200, "data": {
"date": "2026-09-01", "labReportCount": 128, "examReportCount": 14,
"unmatchedCount": 2,
"externalIdDigest": "sha256:3f9a…",
"externalIds": ["LIS20260901000123", "…"]
} }6.5 联调自检 GET /open/v1/ping
只读接口,不修改任何数据。返回服务端识别到的 appId、应用名称 appName、所属医院编号 tenantId、凭据类型 sandbox、服务器时间 serverTime,以及验签时使用的待签名串 stringToSign;签名校验失败时,可用它逐行比对。
{ "code": 200, "msg": "操作成功", "data": {
"appId": "hdis_demo", "appName": "示例医院检验科", "tenantId": "1", "sandbox": true,
"serverTime": "2026-09-01 08:00:00",
"stringToSign": "GET\n/open/v1/ping\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n1756684800000\n7f3d0c1e-0000-4000-8000-000000000001"
}, "timestamp": 1756684800123 }7. 字段字典
标准项完整清单(183 项,含单位、别名、类型)见标准检验项字典,机器可读版本通过 GET /open/v1/dict/lab-items 获取,内容以接口返回为准。
接入方无需按此字典改造 LIS,推送院内的 itemCode + itemName 即可。字典用于配置映射时对照。
8. 联调步骤
建议按以下顺序联调,一般半个工作日可完成:
- 调用
GET /open/v1/ping,确认签名、IP 白名单和服务器时钟均正常。 - 在沙箱环境推送 1 份检验报告,患者使用 HDIS 提供的测试身份证号,确认返回
matched: true。 - 在沙箱环境推送 1 份无法匹配患者的报告,确认返回
matched: false且接口返回成功,接入方的重试逻辑不会将其判为失败。 - 以相同
externalId再次推送,确认返回UPDATED或IGNORED,未产生新记录。 - 一次推送 100 份报告(其中 2 份为有意构造的错误数据),确认部分成功,且接入方能按逐条结果处理。
- 调用
reconcile对账接口,确认报告数量一致。 - 切换为生产凭据,先推送一天数据,双方分别对账无误后再全量开启。
9. 服务范围说明
- HDIS 不主动拉取数据,不访问接入方数据库,不部署前置机,不要求接入方开放入站端口。
- 不接收影像文件,不提供 DICOM 存储与转发。
- 不回写 HIS(不下达医嘱、不写入收费)。如有此需求,须另行立项并取得医院书面授权。
- 仅支持 §3 规定的签名鉴权,不支持明文密码、Token 等其他凭据形式。
10. 变更记录
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0.3-draft | 2026-09-15 | §2 补充基础地址(云端部署地址;院内离线部署地址另行通知),明确沙箱与生产凭据使用同一地址;§4.4 补充单份报告不超过 500 项;§6.3 删除实际不支持的 ?version= 参数说明;§6.5 更正返回字段并补充响应示例;§6.1 列出标本类型取值,删除与实现不符的「缺省为 SERUM」。接口行为无变更 |
| v1.0.2-draft | 2026-09-15 | 文字修订:统一术语与行文;第 5 节拆分为患者字段、匹配规则、待认领三个小节;status 取值改为表格。接口、字段与签名算法均无变更 |
| v1.0.1-draft | 2026-09-02 | 补充说明,接口与签名算法无变更:(1)§3.1 明确规范化查询串以实际发送的原文为输入,不再次编码;(2)§3.3 增加含中文查询参数的签名示例,并补充第一个示例的签名值;(3)§3.4、§3.5 增加查询串规范化的参考实现;(4)§4.3 明确日期时间格式严格匹配,带毫秒、ISO T 分隔符或时区后缀均会被拒绝;(5)§4.4 明确调用频率按 appId 统计,失败请求同样计入;(6)§5 补充两条匹配规则:已人工认领的 hisPatientId 优先于身份证号匹配,同一患者以两种标识推送会产生两条待认领记录 |
| v1.0.0-draft | 2026-09-01 | 首版草案:签名鉴权、检验与检查报告推送、患者身份证号匹配、字典与对账 |
附:Node.js 参考实现
以下代码节选自签名参考实现 sign-demo.mjs。完整文件随凭据提供,支持 --selftest(运行自检)和 --dry(只打印待签名串,不发送请求)。
export function canonicalQuery(rawQueryString = '') {
const raw = typeof rawQueryString === 'string' ? rawQueryString : buildQueryString(rawQueryString);
if (!raw) return '';
const pairs = [];
for (const part of raw.split('&')) {
if (!part) continue;
const eq = part.indexOf('=');
pairs.push(eq < 0 ? [part, ''] : [part.slice(0, eq), part.slice(eq + 1)]);
}
// 按解码后的参数名排序(使 %61 与 a 排序一致),拼接时仍使用原始的名与值。
const decoded = new Map();
const nameForSort = (n) => {
if (!decoded.has(n)) {
let v;
try {
// 与 Java URLDecoder 一致,将 + 视为空格,否则含 + 的参数名排序结果会不同。
v = decodeURIComponent(n.replace(/\+/g, ' '));
} catch {
v = n; // 百分号编码不合法时按原文排序,不中断执行;此类请求验签会失败。
}
decoded.set(n, v);
}
return decoded.get(n);
};
pairs.sort((a, b) => {
const na = nameForSort(a[0]);
const nb = nameForSort(b[0]);
if (na !== nb) return na < nb ? -1 : 1;
if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
return 0;
});
return pairs.map(([k, v]) => `${k}=${v}`).join('&');
}
export function stringToSign({ method, path, query = '', body = null, timestamp, nonce }) {
const raw = body == null ? Buffer.alloc(0) : Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8');
const bodyHash = crypto.createHash('sha256').update(raw).digest('hex');
return [method.toUpperCase(), path, canonicalQuery(query), bodyHash, String(timestamp), nonce].join('\n');
}
export function sign(appSecret, toSign) {
return crypto.createHmac('sha256', Buffer.from(appSecret, 'utf8')).update(toSign, 'utf8').digest('hex');
}