---
title: FRIDAY 推送数据契约
description: FRIDAY SaaS 按形态 A 推送的 9 个 source 及字段定义;宿主推自洽的业务表(自己的引用 inline 进相关行),PAC 做跨宿主临床归一。
icon: FileJson
---

> 本文是 [Push 通道](/docs/integration/channel-push)**形态 A** 在 FRIDAY SaaS 宿主的具体化:每个 `source` 的 JSON 字段清单。
> 字段形状与 PAC 已完成的存量摄入**完全一致**——宿主按此推送,与存量自动衔接:
> 同行按幂等键去重,行更新(`updated_gmt_at` 变)自动版本演进。**重叠无害,宁多勿漏**。
> 时间字段的可空语义与 `created_gmt_at` 兜底见 §1 通用约定。

---

## 1. 通用约定(所有 source 适用)

```jsonc
// POST /pac/v1/push/rows
{
  "source": "med_emr_info",     // 本文各节的 source 名
  "rows": [ { /* 原生行,字段见各节 */ } ]
}
```

| 约定 | 说明 |
|---|---|
| **id 类字段一律字符串** | `patient_id` / `id` / `appointment_id` 等统一 String(数值型会被 PAC 校验拒收——存量摄入实测踩过的坑) |
| **时间格式** | 统一传 naive 北京墙钟 `"YYYY-MM-DD HH:mm:ss"`(PAC 按 `Asia/Shanghai` 解释)。实证(2026-07-20):MySQL `datetime` 存的就是北京墙钟(`_gmt_` 命名系惯例误导),直出即可;**Mongo `Date` 是北京墙钟伪装成 UTC 存储**(小时分布双证)——从 Mongo 读出后**须直读 UTC 字段还原墙钟**,切勿 `toISOString()` 带 `Z` 推送(会被按真 UTC 多加 8 小时) |
| **`updated_gmt_at`** | **可空,缺失不影响变更捕获**。PAC 的变更识别是**内容驱动**的:按 `(subjectId + 内容 hash + 状态 + 时间锚)` 判定,不看 `updated_gmt_at`——同一 `subjectId` 内容变了就升版本,没变就幂等去重。所以只要 `subjectId` 稳定、内容如实推,时间字段缺不缺都不丢更新。`updated_gmt_at` 仅用于**乱序防护**(补推的旧快照晚到时不覆盖新版本);为 null 时该保护退回"按到达顺序",对正常单向推送无影响。**无需宿主侧 COALESCE 兜底,重推安全。** |
| **空值** | 传 `null` 或省略字段;不要传 `"NULL"` 字符串 |
| **金额单位** | **元**(decimal 原值,实证:洁治单价均值 ¥310/已结算客单均值 ¥1,730);PAC 侧转分 |
| **投递语义** | at-least-once:未收到 2xx 确认就重推;重复由 PAC 幂等键吸收 |

> **职责边界(重要)**:宿主**在推送前解析好自己的引用**,把结果 inline 进相关业务行 ——
> ① 私有字典码(诊断 `linkCode→std_code`、影像 `class_code→类型名`)② 1:N 派生标量(默认联系号 + 归属)
> ③ 头-行属性(计划头的诊所/方案名下放到计划行)。这些**都是宿主自己的 within-host join**,数据在它手里、成本极低。
> PAC 只做**跨宿主的临床归一**(共享 K 码/modality/治疗类别字典、金额单位、时区、结算 status 切分、变更检测/版本)。
> 好处:每张 source 自洽,增量推变更行天然成立,无跨表依赖。
>
> **两条配套纪律**:① **整表照推,宿主不做任何行过滤**——status/金额范围等切分全部由 PAC 完成
> (单一真理源,与 file / cold-import 同口径),故本文各表**不再规定「推送范围」**;
> ② **列名一律沿用宿主原表列名**——包括 inline 进来的列(`contacts_tel`/`relationship`/`std_code`/
> `class_name`/`organization_id`/`plan_name`),PAC 不发明新名,宿主无需为 PAC 做字段改名。

---

## 2. 患者(2 个 source)

### `customer_basic_info` — 患者主档

> **部分更新语义**:只更新本次提供的字段,未提供的不覆盖存量;push 无"清空字段"语义,需清空走全量通道。
>
> **联系电话已 inline(不再单独推 customer_contacts)**:宿主按 `is_default↓ → contacts_type↑(1=本人优先) → id↑`
> **挑出默认号**,把号码与归属 inline 进本表的 `contacts_tel` / `relationship` 两列(沿用 customer_contacts 原列名)。PAC 现只消费这一个有效号 + 归属
> (存 `patient.phone` 与 `contactPhone.isSelf`),不需要完整通讯录 → 联系方式 1:N 表无需摄入。改号 = 重推本表。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 患者主键(= PAC externalId;其他表 `patient_id`/`customer_id` 指向它) |
| `name` | string | | 姓名 |
| `sex` | string | | `1`=男 `2`=女,其余置空 |
| `birthday` | string(date) | | 生日 |
| `file_number` | string | | 病历号/档案号(客服沟通用) |
| `tenant_id` | string | ✅ | 品牌 GUID(宿主原生列) |
| `organization_id` | string | | 建档诊所 |
| `contacts_tel` | string | | **默认联系号(宿主 inline)**——按上述规则挑出的有效号码(= customer_contacts 原列名) |
| `relationship` | string/number | | **该号归属(宿主 inline,= customer_contacts 原列名)**——`PhoneRelationshipEnum`:`1`本人 `2`爸爸 `3`妈妈 `4`爷爷 `5`奶奶 `6`朋友 `7`配偶 `8`子女 `9`其他(PAC 存 `isSelf`=码=='1') |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | 建档/更新时间 |

### `customer_referee_circle` — 转介绍圈(患者-患者关系)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 行主键 |
| `tenant_id` | string | ✅ | 品牌 GUID |
| `organization_id` | string | | 诊所 |
| `customer_id` | string | ✅ | 本人患者 id |
| `referee_patient_id` | string | ✅ | 关系人患者 id(也是在册患者) |
| `referee_relationship` | string/number | | 关系码(官方枚举 `RecommendRelationshipEnum`:`1`配偶 `2`爸爸 `3`子女 `4`兄弟 `5`爷爷 `6`孙子女 `7`朋友 `8`其他亲属 `9`姐妹 `10`妈妈 `11`奶奶 `12`姐弟 `13`兄妹 `14`其他 `15`外公 `16`外婆 `17`外孙子女;**语义=本行 customer 是 referee 的 X**) |
| `type` | string/number | | 关系类型标记 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | | |

---

## 3. 预约(1 个 source)

### `appointment_base` — 预约(全状态推,PAC 侧过滤)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 预约主键 |
| `patient_appointment_id` | string | ✅ | → 患者 id |
| `organization_id` | string | ✅ | 诊所 |
| `tenant_id` | string | ✅ | 品牌 GUID |
| `appointment_date` | string(date) | ✅ | 预约日期 |
| `appointment_start` | string(time) | ✅ | 开始时刻(与 date 拼成预约时刻) |
| `appointment_status` | string | ✅ | `1`预约正常 `2`改约 `3`取消 `4`已到诊 `5`已就诊 `6`已结算 `7`到诊患者变更 `8`walk-in变更 `9`已爽约 `10`草稿(官方注释;PAC 当前消费 1-6/8/9,7 与 10 丢弃) |
| `doctor_user_id` | string | | 医生 id |
| `appointment_time_length` | string/number | | 时长(分钟) |
| `in_time` | string(datetime) | | 到诊时刻(状态 4/5/6 时,PAC 据此产接诊事件) |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

---

## 4. 病历链(2 个 source)

### `med_emr_info` — 病历正文(Mongo 平铺;一张表喂诊断/治疗/建议/复查/病历全链)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `emr_sub_id` | string | ✅ | 小病历号 uuid(病历主键;临床事实同诊聚合锚) |
| `emr_id` | string | ✅ | 病历号(如 `JY0A000231`;影像 `med_check.emr_id` 指向它) |
| `appointment_id` | string | | 关联预约 id(有则挂回预约链) |
| `tenant_id` | string | ✅ | 品牌 GUID(宿主原生 tenantId) |
| `organization_id` | string | ✅ | 诊所 |
| `organization_name` | string | | 诊所名称 —— **诊所展示名的主要来源**(PAC 据此派生 id→名字典;缺失则前端显示 GUID) |
| `patient_id` | string | ✅ | → 患者 id |
| `user_id` / `user_name` | string | | 接诊医生 id / 姓名 |
| `clinic_time` | string(datetime) | ✅ | 就诊时刻 |
| `visit_indicator` | number | | `1`初诊 `2`复诊 |
| `status` | number | ✅ | `3`已完成 `4`归档 `2`未完成草稿(整表照推,PAC 侧只取 3/4) |
| `diag` | array | | 诊断数组,元素 `{ "toothPosition": "41;42", "value": "慢性牙髓炎", "std_code": "<宿主已解析的标准码或空>" }`。**`std_code` 由宿主 inline**——原 `linkCode` 经自家 `std_diag` 解析所得,沿用 std_diag 原列名;K 开头 PAC 截 K 大类,非 K/空按自由文本 |
| `treat` | array | | 本次治疗数组,元素同上结构(`value`=治疗名) |
| `dispose` | array | | 处置叙述数组,元素 `{ "toothPosition", "value" }` |
| `examine` | array | | 检查所见数组,元素同上 |
| `dispose_text` / `examine_text` | string | | 上两数组的 JSON 字符串形态(病历自由文本位;数组非空时必带) |
| `illness_desc` | string | | 主诉 |
| `pre_illness` / `past_hist` / `gen_cond` | string | | 现病史 / 既往史 / 全身情况 |
| `auxi_exam` | string | | 辅助检查 |
| `doc_order` | string | | 医嘱 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

> 牙位格式:FDI 牙位号,多牙分号分隔,可带牙面字母(`"42 L;43 D"` / `"11;12;13"`)。

> **`std_diag` 不再单独推**——它是纯翻译字典(`linkCode→std_code`),离开诊断行无独立意义。
> 宿主在导出/推 `med_emr_info` 时,把 `diag[].linkCode` 用自家 `std_diag` 解析成 `std_code` **inline 进 diag 元素**即可。

### `med_check` — 影像档案(metadata,不含文件本体)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 行主键 |
| `tenant_id` | string | ✅ | 品牌 GUID |
| `organization_id` | string | ✅ | 诊所 |
| `organization_name` | string | | 诊所名称(同上,诊所名字典来源之一) |
| `patient_id` | string | ✅ | → 患者 id |
| `emr_id` | string | | → 病历号(`med_emr_info.emr_id`) |
| `class_name` | string | | **影像类型名(宿主 inline)**——原 `class_code` 经自家 `std_check_class` 解析成类型名(口内像照片/小牙片/全景片/CT影像截图/口腔扫描…);PAC 再映射成 modality |
| `file_name` / `file_type` | string | | 文件名 / 类型 |
| `file_url` | string | | OSS 相对路径 |
| `files_size` | number | | 字节数 |
| `shooting_time` | string(datetime) | ✅ | 拍摄时间 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

> **`std_check_class` 不再单独推**——同 `std_diag`,纯翻译字典。宿主把 `med_check.class_code`
> 用自家字典解析成 `class_name` **inline 进 med_check 行**即可。

---

## 5. 结算链(2 个 source)

> **结算原表照抄推送,宿主不做任何 status/金额过滤或分流**——消费 / 整单退费 / 行级退费明细的
> 切分全部由 PAC transforms 完成(单一真理源,与 file / cold-import 同口径)。宿主推 2 张原表:
> `patient_settlement`(结算头,全 status)、`patient_settlement_spec`(结算明细,全量)。
> **支付金额取结算头 `net_receipts_this`(实收);支付通道明细(`settlement_modes`)暂不摄入**
> (总值在结算头,PAC 当前无需通道拆分;若后续要"支付方式分析"再加)。
>
> `status` 全值照推,PAC 侧识别(`PatientSettlementEntity` 实体注释):`0`未结算 `1`已结算
> `2`只生成uuid `3`含退费行 `4`整单反向冲减 `5`重新结算(废弃历史) `6`医生未提交 `7`流程结束
> `8`欠款补缴克隆单。PAC 取:**消费**=`status∈{1,3}` 且 `receivable_this≥0`;**整单退费**=`status=4`
> 或(`status=3` 且 `receivable_this<0`);**退费明细**=`patient_settlement_spec.is_refund=1`。
> 其余(`0/2/5/6/7/8`)PAC 丢弃(`8` 原单克隆入消费会双计应收、`7` 零金额流程关单)。

### `patient_settlement` — 结算头原表(全 status 照推,含消费与整单退费)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `uuid` | string | ✅ | 结算单主键 |
| `tenant_id` | string | ✅ | 品牌 GUID |
| `organization_id` | string | ✅ | 诊所 |
| `patient_id` | string | ✅ | → 患者 id |
| `doctor_id` | string | | 开单医生 |
| `status` | string/number | ✅ | 全值照推(见 §5 引言;PAC 侧切分) |
| `receivable_this` | number(元) | ✅ | 应收(PAC 计患者价值/LTV 用);**可为负 = 退费表达**(status=4 或部分品牌 status=3 负额) |
| `net_receipts_this` | number(元) | | 实收(退费行为负,PAC 取绝对值) |
| `billing_date` | string(datetime) | | 开单时间 |
| `registration_id` | string | | 关联接诊号 |
| `ref_settlement_id` | string | | **退费行(status=4)= 被退的原结算单 uuid**(PAC 据此把退费挂回原消费);消费行一般空 |
| `settlement_serial_num` | string | | 结算流水号 |
| `reason` | string | | 备注/原因 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

### `patient_settlement_spec` — 结算明细原表(全量照推,含行级退费)

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 明细行主键 |
| `tenant_id` / `organization_id` | string | ✅ | 品牌 / 诊所 |
| `settlement_id` | string | ✅ | 所属结算单 uuid(退费挂回原消费) |
| `patient_id` | string | ✅ | **宿主已反填真 patient_id(inline)**——原生 spec.patient_id 部分品牌是诊所本地 id、不可信(实测元和王永全列与结算头不符),导出时按 `settlement_id` 从结算头取真值反填;PAC 直接用 |
| `cure_name` / `service_project_name` | string | | 项目名(退费行=被退项目) |
| `receivable_this` / `net_receipts_this` | number(元) | | 金额(`is_refund=1` 时为被退金额,正值) |
| `is_refund` | string/number | ✅ | `1`=退费行(PAC 只取此值切退费明细) `0`=正常明细 `2`=其他 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

---

## 6. 治疗计划与咨询(2 个 source)

### `customer_treat_plan_item` — 治疗计划(行;头属性已 inline)

> **计划头 `customer_treat_plan` 不再单独推**——PAC 只从头取"诊所 + 方案名"两个属性给计划行,
> 头不是 PAC 实体。宿主把头的 `organization_id` / `plan_name` **inline 进每条计划行**即可;
> 头的 `expect_cost`/`desired_effect` PAC 暂不消费(将来做计划价值/LTV 再约定加)。

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 行主键 |
| `tenant_id` | string | ✅ | 品牌 GUID |
| `customer_id` | string | ✅ | → 患者 id |
| `organization_id` | string | ✅ | **诊所(宿主 inline,取自计划头)**——缺失该行会被 PAC 丢弃(计划事实必须归属诊所) |
| `plan_name` | string | | **方案名(宿主 inline,取自计划头)** |
| `treat_plan_id` | string | | → 计划头 id(留作溯源,PAC 不再靠它 lookup) |
| `tooth_position` | string | | 牙位 |
| `mode_name` | string | ✅ | 术式名(PAC 翻 11 类治疗类别) |
| `charge_min` / `charge_max` | number(元) | | 价格区间 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

### `customer_consult` — 咨询

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `id` | string | ✅ | 咨询主键 |
| `tenant_id` | string | ✅ | 品牌 GUID |
| `organization_id` | string | ✅ | 诊所 |
| `customer_id` | string | ✅ | → 患者 id |
| `task_director_id` / `task_director` | string | | 跟进人 id / 姓名 |
| `treatment_plan` | string | | 咨询中聊到的治疗方案 |
| `preferential_way` | string | | 优惠方式 |
| `unsettled_reason` | string | | **未成交原因**(强召回信号) |
| `unsettled_reason_description` | string | | 未成交原因补充 |
| `other_conditions` | string | | 其他情况 |
| `potential_treatment` | string | | 患者意向(潜在治疗) |
| `data_sources` | string/number | | 数据来源标记 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |

---

## 7. 对接自测(预检 + 推送记录)

对接期不必盲推 —— PAC 提供两个自助工具,**测试环境** `https://pac.jarvismedical.asia`:

### 7.1 预检:`?dryRun=1`(强烈建议先跑)

```
POST /pac/v1/push/rows?dryRun=1
```
跑**完整管线**(transforms + 装配 + 归一 + 租户解析)但**不落任何库、不触发重算**,响应告诉你"若正式推会发生什么",并带**样本 canonical** —— 直接核对 PAC 把你的原生行翻译成了什么:

```jsonc
{"code":0,"data":{
  "dryRun": true,
  "accepted": 6,               // 收到 6 行
  "transactionsWritten": 5,    // 其中 5 行会落库(差值 = 被 PAC 规则过滤,如噪音 status)
  "failed": 0,
  "mappingMisses": 0,          // ⚠️ >0:有原值没映射上(落 _default)
  "suspectFields": 0,          // ⚠️ >0:疑似列改名/缺列
  "samples": [ { "resource": "payment", "canonical": { "externalId": "…", "amount": 40000, … } } ]
}}
```

**自测通过标准**:`failed=0` 且 `mappingMisses=0` 且 `suspectFields=0`,再看 `samples` 里的字段值对不对 —— 都 OK 才去掉 `?dryRun=1` 正式推。

### 7.2 推送记录:事后可查(两种查法)

**① 免登录 —— 用已有 push 凭据验签(联调推荐)**
```
GET /pac/v1/push/logs?limit=50
Headers: X-PAC-Host-Id / X-PAC-Timestamp / X-PAC-Signature
```
鉴权方式与推送完全一致,**不必为联调另开账号**。GET 无 body,签名对**空串**计算:
`hex(HMAC-SHA256("<timestamp>.", secret))`(注意 timestamp 后那个点)。
这**不是公开接口** —— 没有正确签名拿不到任何数据。

**② 登录态 —— 宿主自助页同源接口**
```
GET /pac/v1/admin/host/self/push-logs?limit=50
```

两者返回**同一份数据**:按时间倒序最近 N 批(默认 50、上限 200),每批含
`source / status / dryRun / fetched / transactionsWritten / duplicates / failed / errorMessage`。
当时的同步响应没留存也能事后查:**哪批错了、错在哪**。预检批次由 `dryRun:true` 标识,不与正式推送混淆。

### 7.3 其他可用面

| 用途 | 地址 |
|---|---|
| 交互式 API 文档(可试调) | `/api/docs` |
| 本契约文档 | `/docs/integration/friday-push-payload` |
| 宿主自助页(患者数 / 24h 交易 / 上次 push / 失败率) | `/admin/host` |
| 队列面板(看 push 触发的画像重算) | `/admin/queues` |

> **常见错误码**:`10106` 缺 header / 签名不匹配(或时间戳偏差 >5min)· `10001` source 拼错
> (响应会列出可选源表名)· `10002` body 校验失败 · `10003` 并发超限(退避重发即可,幂等安全)
> · `30802` 整批失败,疑似关键字段缺名/改名。

---

## 8. 语义澄清记录(2026-07,源码 + 数据双证)

以下曾是开放问题,现已定案 —— 推送方无需再确认,列此备查:

| 项 | 结论 | 依据 |
|---|---|---|
| 时间口径 | MySQL `datetime` = 北京墙钟(`_gmt_` 命名系惯例误导);**Mongo `Date` 是北京墙钟伪装成 UTC** | `clinicTime`/`createdGmtAt` 小时分布呈营业双峰;按真 UTC 解释则半数病历落深夜 |
| 金额单位 | 元(`decimal(9,2)`) | 洁治单价均值 ¥310、已结算客单均值 ¥1,730 |
| `relationship`(默认号归属) | `PhoneRelationshipEnum` 1-9(见 §2;宿主挑默认号后 inline) | customer 服务枚举类 |
| `referee_relationship` | `RecommendRelationshipEnum` 17 码,**语义 = 本行 customer 是 referee 的 X**(PAC 侧按逆关系映射) | 枚举类 `reverseValue()` + 双方年龄差数据 |
| `med_emr_info.treat` vs `dispose` | `treat` = 本次治疗(→ 结构化治疗事实);`dispose` = 处置叙述(→ 病历自由文本) | EMR 接口 DTO 字段注释 |
| `patient_settlement.status` 全 9 值 | `0`未结算 `1`已结算 `2`只生成uuid `3`含退费行 `4`整单反向冲减 `5`重新结算(废弃历史) `6`医生未提交 `7`流程结束 `8`欠款补缴克隆单 | 实体注释 + `savePatientSettlementQk()` 实现 |
| 为何不收 `7`/`8` | `8` 是原单克隆(`receivable_this` 原样复制、`payment_arrears` 才是本次补缴额),入消费会**双计应收**;`7` 为零金额流程关单 | 同上 |

### 仍需推送方留意(非阻塞)

| 项 | 说明 |
|---|---|
| `organization_name` 覆盖 | 目前仅病历/影像表带诊所名,**只覆盖有病历记录的诊所**;其余诊所前端回退显示 GUID。若贵方有组织树接口(如 `regional_nodes_structure`),提供 `诊所 id → 名称` 全量表可补齐 |
| 品牌名 | PAC 当前**不需要**品牌中文名(无展示位,内部按品牌 id 标识)。将来若有展示需求,再约定推品牌主档(`tenant_apply.tenant_info` 的 `tenant_id`/`name`)——贵方 `tenant_name` 未冗余进业务表,只能走主档 |
| 私有字典不单独推 | `std_diag`(生产多品牌合计 1.5万~3万+行)/ `std_check_class`:纯翻译字典,**由宿主解析好 inline** 进 `diag[].std_code` / `med_check.class_name`。既省掉大表全量重推,也省掉 PAC 侧持久化字典存储——PAC 只留跨宿主的 `_shared` 临床字典 |
| 支付通道 | `settlement_modes`(约 24% 结算单多通道拆付)暂不摄入:总值已在结算头 `net_receipts_this`,PAC 当前无支付方式分析需求。将来要"医保占比/分次支付"再摄入通道明细 |
| 测试环境数据特征 | 部分租户为开发沙盒(诊所名如"XX专用诊所勿动"),字典类映射(治疗类别关键词等)待**生产数据**回流后再校准一轮 |
