医匠透析 PadOS · 操作手册
医匠透析 PadOS 操作手册 · 开放接口对接规范

医匠透析 HDIS · 开放接口对接规范 v1(草案)

签名鉴权、推送接口、患者标识、返回码与联调步骤。适用于医院信息科及 HIS / LIS / PACS 厂商对接工程师。

关于本页本页内容与《HDIS 开放接口对接规范》同步维护,规范版本 v1(草案) · 更新日期 2026-09-15。对接过程中有疑问请联系医匠透析项目组。

本规范面向医院信息科,以及负责 HIS / LIS / PACS 对接的厂商工程师。

接入方在检验、检查报告审核发布后,通过 HTTPS 调用本规范定义的接口,将报告推送至 HDIS。报告供透析科室在系统中查阅,并作为各地血液透析质控中心上报的数据来源。

对接采用接入方主动推送的方式。HDIS 不接入医院内网,不访问接入方数据库,不部署前置机。


0. 版本与状态

项值
规范版本v1.0.3-draft(2026-09-15)
字典版本以 GET /open/v1/dict/lab-items 返回的 version 为准
状态草案。接口路径与字段已确定;正式发布前如有调整,将提前通知并保留旧版本兼容期

1. 基本原则

  1. 只推送原始结果。 Kt/V、URR、nPCR、钙磷乘积、体表面积、β2 微球蛋白下降率等派生指标,需要结合透析时长、超滤量、透后体重等治疗数据计算,由 HDIS 完成。接入方只需推送透前/透后尿素、钙、磷等原始检验值。
  2. 每份报告须有稳定的唯一编号。 每份报告以 externalId(接入方系统中的报告号或条码号)标识。以相同 externalId 再次推送视为覆盖更新,不会产生重复记录。重试、补推和历史数据回补均依赖该编号,同一份报告的 externalId 不得变化;如每次推送临时生成编号,同一份报告会在 HDIS 中重复出现。
  3. 患者未匹配时照常接收。 无法匹配到患者的报告进入「待认领」队列,由科室人工确认,接口仍返回成功,无需重新推送。
  4. 批量推送逐条处理。 同一请求中的报告逐条校验、逐条入库,个别报告出错不影响其余报告,也不会整批回滚。响应中逐条返回处理结果,出错的报告附带错误说明。接入方应按结果逐条处理,不应因个别失败而整批重推。
  5. 不接收影像文件。 检查报告只推送报告文本和结构化测值,不接收 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}

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-01

HTTP 客户端实际发送的查询串(张三 经 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%89
appId     = 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 行,常见原因有:

  1. %E5%BC%A0%E4%B8%89 被再次编码为 %25E5%25BC%25A0...;
  2. 参数未按参数名排序;
  3. 中文未经编码直接写入。

排查签名问题可使用以下工具:

  • 签名参考实现 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 请求与响应

{ "code": 200, "msg": "操作成功", "data": { }, "timestamp": 1756684800123 }

4.2 返回码

code含义处理方式
200成功,包括部分成功;逐条结果见 data.results按逐条结果处理
400参数错误(JSON 格式错误、缺少必填项、超出批量上限)修正数据后再推送,不应原样重试
700appId 不存在或已停用联系医匠透析项目组,不应重试
701签名校验失败调用 /open/v1/ping 比对待签名串
702时间戳偏差超过 ±5 分钟校准服务器时钟
703Nonce 重复每次请求生成新的 Nonce
704来源 IP 不在白名单内向医匠透析项目组报备出口 IP
705超出调用频率限制按 §4.4 退避后重试
500 / 600HDIS 服务端错误可以重试;接口幂等,重试不会产生重复数据

4.3 时间与数值格式

4.4 批量与限流

4.5 推送时机


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 按以下顺序精确匹配,前一级未命中时进入下一级:

  1. idCard:比对身份证号的不可逆摘要,在本院唯一命中一名患者时匹配成功。
  2. hisPatientId:该编号此前已人工认领时,直接匹配到所认领的患者。
  3. 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. 联调步骤

建议按以下顺序联调,一般半个工作日可完成:

  1. 调用 GET /open/v1/ping,确认签名、IP 白名单和服务器时钟均正常。
  2. 在沙箱环境推送 1 份检验报告,患者使用 HDIS 提供的测试身份证号,确认返回 matched: true。
  3. 在沙箱环境推送 1 份无法匹配患者的报告,确认返回 matched: false 且接口返回成功,接入方的重试逻辑不会将其判为失败。
  4. 以相同 externalId 再次推送,确认返回 UPDATED 或 IGNORED,未产生新记录。
  5. 一次推送 100 份报告(其中 2 份为有意构造的错误数据),确认部分成功,且接入方能按逐条结果处理。
  6. 调用 reconcile 对账接口,确认报告数量一致。
  7. 切换为生产凭据,先推送一天数据,双方分别对账无误后再全量开启。

9. 服务范围说明


10. 变更记录

版本日期变更
v1.0.3-draft2026-09-15§2 补充基础地址(云端部署地址;院内离线部署地址另行通知),明确沙箱与生产凭据使用同一地址;§4.4 补充单份报告不超过 500 项;§6.3 删除实际不支持的 ?version= 参数说明;§6.5 更正返回字段并补充响应示例;§6.1 列出标本类型取值,删除与实现不符的「缺省为 SERUM」。接口行为无变更
v1.0.2-draft2026-09-15文字修订:统一术语与行文;第 5 节拆分为患者字段、匹配规则、待认领三个小节;status 取值改为表格。接口、字段与签名算法均无变更
v1.0.1-draft2026-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-draft2026-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');
}
医匠透析 PadOS(HDIS)用户操作手册