---
title: Push 通道
description: 宿主侧推送患者数据到 PAC 的对接文档,含两种接入形态、HMAC 鉴权与幂等防重。
icon: Send
---

> 宿主系统将患者数据推送至 PAC,存量与增量使用同一通道。全程 HTTPS。

---

## 1. 接入形态

|               | **形态 A:推原生行(推荐)**        | **形态 C:推结构化事件**           |
| ------------- | ------------------------ | ----------------------------- |
| 发送内容          | 数据库原生表行,字段照抄     | 按 PAC 契约组装的事件 JSON |
| 需要了解           | 自己的数据字典            | PAC 的字段与枚举契约(§6)              |
| 字段映射          | PAC 完成                | 宿主按契约组装,码值由 PAC 映射      |
| 一次性成本         | 提供一份数据字典 | 按契约开发组装逻辑                       |
| 后续变更           | 映射调整在 PAC 侧完成,无需重发数据 | 契约变更需双方同步 |

多数系统选 A。鉴权、幂等、推送顺序等规则两形态一致。

---

## 2. 数据范围(两形态通用)

PAC 需要以下主体的数据(取值见[事件枚举](/docs/event-enums)):

| 主体 | 说明 | 级别 |
|---|---|---|
| `patient` | 患者主档 | 必推 |
| `appointment` | 预约及状态变更 | 必推 |
| `encounter` | 接诊记录 | 必推 |
| `emr` | 电子病历(诊断、治疗内容的载体,自由文本或结构化均可) | 必推 |
| `diagnosis` / `treatment` / `recommendation` | 诊断 / 治疗 / 医生建议;独立存在时分开推,包含在病历内时随 `emr` | 必推 |
| `image` | 影像 metadata 与描述(不含文件本体) | 必推 |
| `order` | 收费单及明细 | 必推 |
| `payment` / `refund` | 收款 / 退费流水 | 必推 |
| `recharge` | 充值、储值、福利卡 | 推荐 |
| `complaint` | 投诉记录 | 推荐 |
| `referral` | 转介绍记录 | 推荐 |
| `consultation` / `visit_registration` / `visit_reception` | 咨询 / 挂号 / 接待 | 有则推 |

这些数据在宿主系统中存于哪些表、如何命名,由宿主对照自查:

- **形态 A**:找出覆盖上述主体的表,连同数据字典(§5.2)交给 PAC;一张表覆盖多个主体(如病历表内含诊断与治疗)直接推该表,拆分由 PAC 完成;
- **形态 C**:按主体组装事件(`subjectType` 即主体名)。

---

## 3. 组织标识(两形态通用)

每条数据用三个 id 标识归属,与[登录授权](/docs/integration/auth-login)同一套组织 id:

| 字段 | 含义 | 取值 |
|---|---|---|
| `tenantId` | 集团 id | 同登录授权的 `tenantId`,单集团用固定值 |
| `sourceUnit` | 命名空间 id(品牌 / 区域等) | 集团内为患者号消歧:同号不同命名空间视为不同人。单一命名空间可省 |
| `clinicId` | 诊所 id | 业务发生诊所 |

形态 A 中即原生行里的对应列,在数据字典中标明是哪列;形态 C 在事件中显式携带。

---

## 4. 鉴权(HMAC-SHA256,两形态通用)

| Header            | 值                                                       |
| ----------------- | ------------------------------------------------------- |
| `X-PAC-Host-Id`   | PAC 分配的 host UUID                             |
| `X-PAC-Timestamp` | 当前 Unix 秒(容忍 ±5 分钟)                                 |
| `X-PAC-Signature` | `hex( HMAC-SHA256( secret, "{timestamp}.{rawBody}" ) )` |

- `secret` 由 PAC 接入时一次性下发,双方各自保存原文;
- 参与签名的 body 与实际发送的 body 必须是同一串字节(`JSON.stringify` 一次,签它、发它);
- 密钥轮换:PAC 下发新 secret 后过渡期新旧都接受,切换完成后旧的作废。

```js
const body = JSON.stringify(payload);
const ts   = Math.floor(Date.now()/1000);
const sig  = require('crypto').createHmac('sha256', SECRET).update(`${ts}.${body}`).digest('hex');
// headers: X-PAC-Host-Id / X-PAC-Timestamp=ts / X-PAC-Signature=sig ; body 原样发送
```

---

## 5. 形态 A:推原生行

```jsonc
// POST /pac/v1/push/rows
{
  "source": "tb_emr",        // 源表名,与数据字典一致
  "rows": [
    { /* 该表的一行,原生字段照抄;嵌套结构(如诊断数组)无需拆分 */ }
  ]
}
```

### 5.1 每行需包含的列

列名不作要求,在数据字典中标明各列的角色:

| 角色 | 示例列名 | 用途 |
|---|---|---|
| 业务主键 | `id` | 记录身份,幂等去重 |
| 患者外键 | `patient_id` | 关联患者 |
| 组织归属 | `brand`、`clinic_id` | 对应 §3 三个 id;单集团、单命名空间时只需诊所列 |
| 末次修改时间 | `updated_date` | 幂等、版本、乱序裁决;需随内容变更而更新 |

### 5.2 数据字典

接入时提供一份,内容要求见[数据请求清单 §3](/docs/integration/data-request-checklist):
表名与含义、字段与类型、表间关联、枚举值含义、软删除字段。

---

## 6. 形态 C:推结构化事件

```jsonc
// POST /pac/v1/push/events
{ "events": [ /* 1~500 条,可混合不同 subjectType */ ] }
```

契约由三部分组成:

| 部分 | 内容 | 位置 |
|---|---|---|
| envelope | 每条事件的公共字段 | §6.1 |
| `subjectType` × `action` | 主体与动作枚举 | [事件枚举](/docs/event-enums) |
| `payload` | 各主体的字段定义 | [主体标准化字段](/docs/canonical-fields) |

### 6.1 envelope

| 字段            | 必填   | 说明                                                                   |
| ------------- | ---- | -------------------------------------------------------------------- |
| `subjectType` | ✅    | 主体类型 |
| `action`      | ✅    | 动作,与 `subjectType` 成对 |
| `subjectId`   | ✅    | 本记录在宿主系统的 id,同一记录每次相同                                          |
| `patientId`   | 条件 | 所属患者 id;`subjectType=patient` 时可省                               |
| `tenantId`    | ✅    | 集团 id(§3) |
| `sourceUnit`  | 条件 | 命名空间 id(§3),多命名空间时必带                                           |
| `clinicId`    | ✅    | 诊所 id                                                               |
| `occurredAt`  | ✅    | 发生时间(格式见 §7.1)                   |
| `updatedAt`   | 推荐 | 末次修改时间,随内容变更而更新;缺省按 `occurredAt` |

### 6.2 payload 示例(diagnosis)

```jsonc
{
  "subjectType":"diagnosis", "action":"diagnosis_recorded", "subjectId":"dx_88f1",
  "patientId":"P10086", "tenantId":"grp_001", "sourceUnit":"B001", "clinicId":"C001",
  "occurredAt":"2025-12-06 13:51:44", "updatedAt":"2025-12-25 18:38:21",
  "payload": {
    "emrId": "emr_88f1",
    "doctorId":"D5725", "doctorName":"（医生名）",
    "items": [
      { "name":"菌斑性龈炎", "tooth":"",      "code":"" },   // name 为原始诊断名,码值由 PAC 映射
      { "name":"恒牙深龋",   "tooth":"17;37", "code":"K02.800x008" }
    ]
  }
}
```

---

## 7. 通用规则

### 7.1 金额与时间

**按宿主系统的原样发送,由 PAC 统一转换**。接入时提供两项配置:

| 配置 | 说明 |
|---|---|
| `amountUnit` | 金额单位:`yuan`(元)或 `fen`(分) |
| `timezone` | IANA 时区,如 `Asia/Shanghai`;无时区的时间按此解释 |

两形态同一套转换。带时区的时间(`+08:00` / `Z`)直接使用。

### 7.2 幂等与乱序

宿主只需保证两点:`subjectId` 稳定(同一记录每次相同)、`updatedAt` 随内容变更而更新。

| 场景 | PAC 行为 |
|---|---|
| 重推完全相同的记录 | 跳过,不重复 |
| 内容变更后重推 | 升版本,旧版作废 |
| 同请求内重复条目 | 折叠 |
| 旧版本晚到(补推与实时交错等) | 按 `updatedAt` 裁决,忽略旧版 |

### 7.3 推送顺序

无要求。表与表、批与批之间任意;业务数据先于患者主档到达时,PAC 先建档、主档到达后补全同一人;同一记录的新旧版本乱序到达由 `updatedAt` 裁决。

### 7.4 删除(软删)

物理删除无法被感知,关键记录请使用逻辑删除:

- 形态 A:置软删标记列并更新末次修改时间,照常推送该行;
- 形态 C:重发该记录,`action` 用对应的 `*_cancelled`,并更新 `updatedAt`。

---

## 8. 存量导入

历史数据使用同一通道按批重放,契约与增量完全一致:

1. 推样本批(几十行),检查响应中 `mappingMisses` / `suspectFields`(§9.1),大于 0 联系 PAC;
2. 通过后分批推送(单批 ≤500 行,可多路并行,超过服务端并发额度返 `10003` 退避即可),推完转入增量。

召回场景需要 24-36 个月历史,存量首推必做。百万行量级约 1 小时内;
**千万行以上**建议改用一次性文件导入(按数据字典导出各表文件交 PAC 处理),之后增量走 push。

---

## 9. 回执与重试

### 9.1 回执

统一返回 `{ code, msg, data }`,HTTP 恒为 200(仅进程级故障返回 5xx)。按 `code` 判断结果:

| 场景                 | `code`  | 处理                                                                     |
| ------------------ | ------- | ----------------------------------------------------------------------- |
| 成功                 | `0`     | — |
| 验签失败 | `10106` | 检查 secret、时钟、签名串 |
| 字段校验失败 / 批量超限(条数或字节) | `10002` | 修正后重发(字节超限见 §10,减小单批行数) |
| source 表名不识别            | `10001` | 核对表名 |
| 数据形态漂移(含整批全部失败)   | `30802` | 核对数据字典,修正后原批重推 |
| 并发超限 | `10003` | 退避后重发本批 |
| PAC 内部错误            | `9xxxx` | 退避重试 |
| 超时 / 网络错误 / HTTP 5xx  | —       | 退避重试 |

未收到响应时直接重试,幂等保证安全。

形态 A 成功响应中的自检字段:

| 字段 | 含义 | 大于 0 时 |
|---|---|---|
| `failed` | 本批**没进 PAC** 的行数 | 零星可忽略,持续升高联系 PAC |
| `factsFailed` | 行进了、但**派生不出可用事实**的条数 | 看 `factRejects` 定位到自己源库那一行 |
| `mappingMisses` | 存在未映射的原值 | 暂停推送,发样本给 PAC |
| `suspectFields` | 与历史相比列发生增减 | 确认表结构是否变更,同步 PAC |

`factsFailed > 0` 时,`factRejects` 给出定位明细(最多 10 条,同一种错通常整批同因):

```json
{
  "transactionsWritten": 199,
  "factsFailed": 1,
  "factRejects": [
    {
      "type": "payment_record",
      "subjectId": "payment_record:be67598c35ec11eebdaf00163e342921",
      "transactionId": "940e83e0-1dd8-451c-a494-c15e1512eac5",
      "issues": ["amount_cents: Too small: expected number to be >=0"]
    }
  ]
}
```

`subjectId` 的冒号后面就是**贵方的主键**,据此回源库定位。上例是一张实收为负的红冲结算单
——`failed` 为 0(行本身收下了),但它派生不出合法的消费事实。

出于隐私,`factRejects` 只给定位信息与字段级原因,**不回传记录原文**。

### 9.2 补漏

每晚将最近 N 天数据全量重推一遍:已推的跳过、漏推的补上、变更的升版本。
PAC 侧对推送中断有告警。

---

## 10. 限制

| 项 | 约定 |
|---|---|
| 批量(条数) | 两形态 ≤ 500 条/请求 |
| 批量(字节) | **请求体 ≤ 10 MB**,超出返 `10002`,减小单批行数后重发 |
| 并发 | 同 host 有并发额度,超限返 `10003`,退避重发即可 |
| 编码 | UTF-8 |
| 重试 | 幂等,可安全重试,建议指数退避 |

⚠️ **两条上限都要满足,先到先约束** —— 行的大小按表差很多,别只按条数切批:

| 表类型 | 单行典型大小 | 10 MB 约能装 | 建议单批 |
|---|---|---|---|
| 病历 / 诊断(含自由文本:主诉、检查、处置、医嘱) | 1.5-2.2 KB | ~5000 行 | **50-100 行** |
| 预约 / 接诊 / 结算(纯结构化字段) | ~400 B | ~25000 行 | 500 行(条数先到顶) |

病历类按 500 条切批很容易做出 700 KB 以上的包;虽然仍在 10 MB 内,但单请求处理时间会显著变长
(实测有整批超时被客户端断连的情况),所以建议按上表压到 50-100 行。

---

## 11. 接入清单

- [ ] 获取 PAC 分配的 `Host-Id` 与 `secret`
- [ ] 实现 HMAC 签名(`{ts}.{rawBody}`,同一串字节签名并发送)
- [ ] 确认 `amountUnit` 与 `timezone`(§7.1)
- [ ] 选择形态:A 交数据字典;C 按 §6 契约开发
- [ ] A:主体对应表齐全(§2)、每表四类列齐全(§5.1)
- [ ] C:envelope 字段齐全,payload 符合[主体标准化字段](/docs/canonical-fields)
- [ ] `subjectId` 稳定,`updatedAt` 随内容变更
- [ ] 组织三 id 正确(§3)
- [ ] 删除走软删(§7.4)
- [ ] 本地 outbox 持久化 + 异步 worker(业务写库与入队同事务)
- [ ] 按 `code` 处理响应(§9.1),`10003` 退避重发
- [ ] 存量:样本批验证后分批重放(§8)
- [ ] 每晚幂等补推最近 N 天(§9.2)

---

## 附:完整请求示例(形态 C,curl)

```bash
TS=$(date +%s)
BODY='{"events":[{"subjectType":"diagnosis","action":"diagnosis_recorded","subjectId":"dx_88f1","patientId":"P10086","tenantId":"grp_001","sourceUnit":"B001","clinicId":"C001","occurredAt":"2025-12-06 13:51:44","updatedAt":"2025-12-25 18:38:21","payload":{"emrId":"emr_88f1","doctorId":"D5725","items":[{"name":"菌斑性龈炎","tooth":"","code":""},{"name":"恒牙深龋","tooth":"17;37","code":"K02.800x008"}]}}]}'
SIG=$(printf '%s' "$TS.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST "$PAC/pac/v1/push/events" -H "Content-Type: application/json" \
  -H "X-PAC-Host-Id: $HOST_ID" -H "X-PAC-Timestamp: $TS" -H "X-PAC-Signature: $SIG" --data "$BODY"
```
