Commit dc8add18 by luoqi

docs(friday): push 契约对齐迁移后原表 + 冷导入/对账文档

- friday-push-payload:结算段从「宿主分流 4 source」改为「推 3 张原表、WHERE 全在 PAC」;
  删 patient_settlement_refund/spec_refund 两个已废弃 source;新增 spec.patient_id 不可信说明;
  source 计数 15→14
- 新增 data-reconciliation:源  PAC 计数对账口径
- ingestion / meta:配套更新

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent b3e3a6c2
Pipeline #3426 failed in 0 seconds
---
title: 数据对账(设计)
description: 独立核对 PAC 侧数据与各宿主源是否一致 —— 同闸口径、去重患者数、多宿主 provider、复用企微日报。
icon: Scale
---
<Callout type="warn">
**本文是设计文档,尚未实现**。记录对账机制的口径与方案,供评审 / 落地参照。
</Callout>
## 为什么要独立对账
现有的几层防线都是**单向补漏 / 水位监控**,抓不到"该有的数据到底在不在":
| 现有机制 | 作用 | 抓不到 |
|---|---|---|
| 48h 回看窗 | 容忍 DW 落库迟到 | 超 48h / 时间戳不跳的漏拉 |
| 主档反向强拉 | 补 cohort 的主档 | 非主档信号的积压 |
| ReconcileOrchestrator | 时间窗重拉补漏(手动) | 不告诉你"差了多少" |
| DwLagMonitor | cursor 距今 >24/48h 告警 | 只看水位新旧,不看行在不在 |
尤其前述三个已知漏点 —— [无 cursor 表积压](/docs/ingestion#21-增量-cohort-与无-cursor-表)、`updatedAt` 不跳漏拉、事务/fact 不一致 —— **都是补漏和水位监控抓不到的**。缺的是一个**双向数量核对**:定期比"源侧应有 vs PAC 实有",差超阈值告警。
## 三个难点与解法
对账看似简单(两边 count 一比),实则有三个坑,逐个解:
### 难点 1:多宿主 —— 源计数从哪来因宿主而异
PAC 是多宿主,不能绑死 DW —— 但对账**只覆盖 PAC 能直连源库的宿主**:
| 宿主类型 | 例 | 能直连源? | 对账 |
|---|---|---|---|
| **pull**(直连) | jvs-dw(ClickHouse) | ✅ | ✅ PAC 主动查源计数 |
| **push**(无直连) | friday(SaaS) | ❌ PAC 够不到其库 | **不对账** |
**push 宿主不对账**(定):PAC 够不到源库,唯一办法是宿主自报,但那要它维护一套"按 PAC 口径过滤后的计数",负担与可信度都不划算 —— 直接排除。push 宿主的数据质量靠既有的 push 路径告警(疑似改表 / 失败率超阈)兜。
抽象成:**PAC 只负责「拿到 `(scope, sourceCount)` → 对比 `pacCount` → 报 diff」;`sourceCount` 由每个可直连宿主的 provider 供给**(查询源库)。将来任何可直连的新宿主接入只需实现 provider,对账主逻辑不动。
### 难点 2:摄入是筛选子集 —— 不能数原表全量
PAC 摄入的**不是原表全量**,有大量故意筛选(结算 `status IN (1,3)`、瑞泰不摄、cohort 限有就诊、诊所范围…)。拿原表 count 比,diff 里全是"故意的差",没法用。
**解法:源计数复用摄入查询本身,只换投影。** manifest 的 `sql_source` 查询**就是"PAC 应该有什么"的定义**,把 `SELECT *` 换成 `count(distinct patient)`:
```sql
sourceCount = count(distinct patient) FROM ( «摄入用的那条 sql_source 查询,原样» )
```
两边都过了同一道筛选闸,diff 只剩**真漂移**。且筛选口径**零重复维护** —— 改了 `sql_source` 的 WHERE,对账自动跟着变(用的同一条查询),不会两份逻辑慢慢跑偏。
(push 宿主不对账,见难点 1,不涉及此。)
### 难点 3:fan-out 噪声 —— 不能比行数
一条源行可能拆成 N 条 fact(诊断+治疗+建议),加上版本流,行数根本不可比。
**解法:只比「去重患者数」,不比行数。** 干净、可比、且直接对应业务关切("多少患者的 X 信号 PAC 没跟上")。
## 对账口径:同闸 + 去重患者数 + 两层
只看总患者数**不够** —— 无 cursor 表积压时患者还在 PAC 里(只是他的回访/咨询行旧了),总量层 diff=0 会漏检。故分两层:
| 层 | sourceCount | pacCount | 抓什么 |
|---|---|---|---|
| **总量** | cohort 查询的 `count(distinct patient)` | PAC 该 host 的 `count(distinct patient)` | 整体漏人 |
| **信号** | 各资源 sql_source(含各自过滤)的 `count(distinct patient)` | PAC 里有对应 `fact_type` 的 `count(distinct patient)` | 某类信号积压(定位到无 cursor 表) |
**信号层示例**:`DW 有影像分析的患者 5.0 万 vs PAC 有 image_record 的患者 4.8 万,diff 2000` → 既可比又定位到"影像信号积压"。
**覆盖范围:全 fact 类型**(定)。不挑重点,每类都对(诊断 / 治疗 / 影像 / 回访 / 咨询 / 结算 / 转介…),一眼看全哪条信号在漂;性能由下节"每日一次 + 秒级 count"兜住,全覆盖不构成负担。
## 性能
对账要跑一堆 `count(distinct patient)`,得算清楚成本再上:
- **频率:每日一次**(不是每轮增量)。跟健康日报同一时刻(09:00 沪、DW 刷新后)跑一次即可 —— 漂移是慢变量,日粒度足够。
- **DW 侧**:每类信号一条 `count(distinct patient)`,全覆盖约 10 条。ClickHouse 的 `count(distinct)` 对 13M 行通常秒级;精度不敏感处可用 `uniq`(HyperLogLog,~0.5% 误差)换更低开销,要精确用 `uniqExact`。**上线前实测这 10 条的总耗时**,确认 < 分钟级。
- **PAC 侧**:`patient_facts` 按 `fact_type` 分组 `count(distinct patient_id)`,一条走索引的聚合查询,廉价。
- **不常驻**:对账是日报里的一个步骤,跑完即释放,无常驻连接 / 内存。
每日一次 + 秒级查询 → 全覆盖的性能开销可忽略,不影响在线摄入 / 重算。
## 基线:压掉"已知的正常差"
有些差**本来就该存在**,不压会天天误报 → 告警疲劳:
- 瑞泰品牌不摄(恒差)
- 无 cursor 表短期积压(会自愈)
- 源侧脏数据 / 时间窗边界
**基线放 manifest 配置**(每宿主声明"哪些 scope 差多少以内算正常"),只有**偏离基线**才告警,不是有差就报。
```yaml
# data/<host>/manifest.yaml(拟)
reconcile:
baseline:
total_patient: { tolerance_pct: 2 } # 总量差 2% 内正常
fact.image_record: { tolerance_pct: 5 } # 影像积压容忍高些
fact.payment_record: { tolerance_abs: 200 }
```
## 汇报:复用企微,折进每日健康日报
**不新起消息流** —— 复用 `AlertService`(企微群机器人),把「数据对账」折进已有的 [每日健康日报](/docs/monitoring)(每天 09:00 沪、DW 刷新后跑)。对账项超基线时,把日报整体级别顶成 🟡/🔴(现有逻辑:取最差项)。
```
每日 09:00 健康日报(已有)增加一段「数据对账」:
for each host:
for each scope in [总量患者, 各 fact 类型患者]:
sourceCount = provider(host, scope) # pull 查询 / push 自报
pacCount = PAC 同口径 count(distinct patient)
diff, diffPct
超 manifest.reconcile.baseline → 标 🟡/🔴
推企微(复用 AlertService,超基线才顶级别)
```
## 分期落地
**先只观察企微,不建表**(定)。
| 版本 | 做什么 | 不做 |
|---|---|---|
| **v1(先做)** | 同闸口径算 diff(全 fact 类型)+ 基线压噪 + 折进健康日报推企微 | **不建表**;趋势靠每日日报肉眼比 |
| v2(以后) | 若肉眼比不够,再建 `reconcile_checks` 表存快照 → 自动趋势 + 审计历史 | — |
先跑 v1 观察企微:口径准不准、会不会噪声、diff 稳不稳。够用就不上表。
## 已定 / 待决
**已定**:push 宿主(无直连)不对账 · 全 fact 类型覆盖 · 每日一次跑 · 先不建表只观察企微。
**待决**:基线初值怎么定 —— 建议**先不设容忍、原样把 diff 报进日报观察一两周**,摸清各 scope 的"正常差"后再回填 `manifest.reconcile.baseline`,避免拍脑袋定阈值。
...@@ -93,6 +93,46 @@ pnpm sync -- --dir=./data/<host> --dry-run # 不写库,预览游标注入 ...@@ -93,6 +93,46 @@ pnpm sync -- --dir=./data/<host> --dry-run # 不写库,预览游标注入
pnpm sync -- --dir=./data/<host> --full # 忽略游标强制全量(灾后) pnpm sync -- --dir=./data/<host> --full # 忽略游标强制全量(灾后)
``` ```
### 2.1 增量 cohort 与「无 cursor 表」
增量分两步,理解成本要分开看:
1. **选 cohort**:`manifest.incremental.per_query` 声明的表,各按自己 `cursor_column`
取 `WHERE cursor > 水位` 的患者,**并集**成本轮 cohort(UNION —— 任一表有变更就纳入,
避免"预约变了但人没来"被漏)。
2. **拉资源**:对 cohort 患者,**所有**源表按 `(patient_id, brand) IN (cohort)` 拉行。
没进 `per_query` 的表(无 cursor)也在此步被拉 —— **按患者收窄,不全表扫**。
**关键推论**:无 cursor 表**不能独立把患者拉进增量**。若某患者只有无 cursor 表变了
(且 cursor 表都没动)→ 不进 cohort → 该变更**积压到该患者下次因 cursor 表再进 cohort 才补上**。
所以无 cursor 表适合**弱信号 / 展示类**(容忍延迟),不适合驱动召回的核心信号。
以 **jvs-dw(瑞尔)** 为例(2026-07 DW 实测行数):
| 源表 | 行数 | cursor | CH 排序键 | 说明 |
|---|---:|---|---|---|
| fact_client_out | 5.7M | `last_visit_time` | patient_id | 患者主档 |
| fact_emr_treatment_out | 5.1M | `updated_date` | patient_id… | 治疗/病历(召回核心) |
| fact_appointment_out | 9.1M | `updated_date` | id | 预约 |
| fact_settlement_out | 9.1M | `updated_date` | id | 结算 |
| fact_settlement_mode_out | 4.4M | `updated_date` | — | 支付通道 |
| **fact_returnvisit_out** | **13.4M** | **无** | 无 | 回访任务(展示) |
| **fact_consult_out** | **10.8M** | **无** | org… | 咨询(意向,弱信号) |
| fact_emr_image_analysis_out | 553K | 无 | emr_id | 影像 AI |
| fact_customer_referee_out | 499K | 无 | 无 | 转介 |
<Callout type="info">
**提频(如 2h 一轮)的成本**——由**空轮短路**决定,不是 cohort 大小:
- **cohort 选择每轮都跑**(含空轮):cursor 列(`updated_date` 等)通常**不是 CH 排序键
→ 无索引 → 每轮全列扫**。但只扫单列,9M 行亚秒级,几张合计几秒/轮,绝对量可忽略。
- **资源拉取只在 cohort 非空时跑**:上游多为日更 → 日内多数轮 cohort 为空 →
在选 cohort 后即**短路**(跳过资源拉取 + persona/plan 重算),不触碰上面两张最大的无 cursor 表。
- 所以提频主要增加的是**廉价的空轮探查**,重活(资源全表扫 + 重算)仍是刷新后 1–2 轮,不被放大。
真要压这点空轮扫描,治本是给 cursor 表的 `updated_date` 加 CH 数据跳过索引;收益很小,通常不值当。
</Callout>
--- ---
## 三、数据完整性 ## 三、数据完整性
...@@ -102,7 +142,15 @@ pnpm sync -- --dir=./data/<host> --full # 忽略游标强制全量(灾 ...@@ -102,7 +142,15 @@ pnpm sync -- --dir=./data/<host> --full # 忽略游标强制全量(灾
- `patient_transactions`:partial UNIQUE `(host_id, tenant_id, source_event_id) WHERE source_event_id NOT NULL` - `patient_transactions`:partial UNIQUE `(host_id, tenant_id, source_event_id) WHERE source_event_id NOT NULL`
- `patient_facts`:UNIQUE `(host_id, tenant_id, subject_id, version)` + active 行 partial UNIQUE - `patient_facts`:UNIQUE `(host_id, tenant_id, subject_id, version)` + active 行 partial UNIQUE
`source_event_id` 内含 `updatedAt`,因此源数据行级 in-place 更新会自然产生新 tx 与新 fact 版本(旧版 supersede);同行同 `updatedAt` 则幂等跳过。 `source_event_id` 内含 `updatedAt`(缺失时确定性回退 `createdAt`),因此源数据行级 in-place 更新会自然产生新 tx 与新 fact 版本(旧版 supersede);同行同 `updatedAt` 则幂等跳过。fact 层的变更判定是**内容驱动**的(比对 content hash + 状态 + 时间锚,不看 `updatedAt`),所以即使事务被幂等跳过,只要行被重新处理,内容真变了 fact 仍会升版本——变更不会因时间戳丢失而漏掉。
<Callout type="warn">
**`updatedAt` 维护缺失的两个后果**(如某些源表主档 `updated_gmt_at` 大量为 null)。当前设计有意保留 `updatedAt` 在幂等键中,**不改**;接入时按下面两点评估:
1. **pull/增量取数够不到**:增量按 `WHERE updated_date > cursor` 拉数,**内容变了但 `updatedAt` 没随之更新的行根本不会被选出来**——这道过滤在幂等之前,fact 层内容比对再准也无行可比。file 全量装载 / push(宿主自选推送)不受此限(不按 cursor 过滤)。治本靠源表维护 `updatedAt`;主档另有定期全量反拉兜底(见上「主档补全」)。
2. **事务与 fact 可能不一致 → reparse 会打回**:若内容变了但 `updatedAt` 没变,事务因 `source_event_id` 未变被幂等跳过(`rawPayload` 停在旧内容),但 fact 层仍按新内容升版本 —— 二者内容不再一致。此后 [reparse](#52-reparse--字典改了补存量无需全量重摄) 从 `transaction.rawPayload`(旧值)离线重放,会把 fact 打回旧内容。**规避**:对 `updatedAt` 不可靠的源表,不要依赖 reparse 修其存量,改走 DW 重摄 / 重推。
</Callout>
**主档补全(两条保障,确保 patient_id 不丢)**: **主档补全(两条保障,确保 patient_id 不丢)**:
...@@ -148,6 +196,7 @@ pnpm sync -- --dir=./data/<host> --full # 忽略游标强制全量(灾 ...@@ -148,6 +196,7 @@ pnpm sync -- --dir=./data/<host> --full # 忽略游标强制全量(灾
- **不连数据源**:rawPayload 在本地,比全量重摄快一个量级; - **不连数据源**:rawPayload 在本地,比全量重摄快一个量级;
- **可圈定**:按 host / subject-type / 患者集 / dry-run / 是否重算; - **可圈定**:按 host / subject-type / 患者集 / dry-run / 是否重算;
- **覆盖范围**:field/enum/keyword_mapping、transforms 算子、parser 改动;**不覆盖**改了 `sql_source` 的 SELECT 本身(需从数据源多拉列/表,那才需真重摄)。非 transform 产出的资源(如 SQL 视图类)会自动跳过。 - **覆盖范围**:field/enum/keyword_mapping、transforms 算子、parser 改动;**不覆盖**改了 `sql_source` 的 SELECT 本身(需从数据源多拉列/表,那才需真重摄)。非 transform 产出的资源(如 SQL 视图类)会自动跳过。
- **前提:rawPayload 是最新的**。reparse 只能和源行的最后一次落库一样新。若某源表 `updatedAt` 维护缺失,曾出现「内容变了但事务被幂等跳过」(见 §三 Callout),其 `rawPayload` 已滞后 —— 对这类表 reparse 会把 fact 打回旧内容,应改走重摄 / 重推而非 reparse。
```bash ```bash
# dry-run:报 scope(患者数 / txn 数),不写库 # dry-run:报 scope(患者数 / txn 数),不写库
......
--- ---
title: FRIDAY 推送数据契约 title: FRIDAY 推送数据契约
description: FRIDAY SaaS 按形态 A 推送的 15 个 source 及字段定义;与已验证的存量导出形状一致,push 无缝衔接 description: FRIDAY SaaS 按形态 A 推送的 14 个 source 及字段定义;字段形状与已验证的存量导出一致(宿主推原表,消费/退费等 WHERE 全在 PAC 侧切分)
icon: FileJson icon: FileJson
--- ---
> 本文是 [Push 通道](/docs/integration/channel-push)**形态 A** 在 FRIDAY SaaS 宿主的具体化:每个 `source` 的 JSON 字段清单。 > 本文是 [Push 通道](/docs/integration/channel-push)**形态 A** 在 FRIDAY SaaS 宿主的具体化:每个 `source` 的 JSON 字段清单。
> 字段形状与 PAC 已完成的存量摄入**完全一致**——宿主按此推送,与存量自动衔接: > 字段形状与 PAC 已完成的存量摄入**完全一致**——宿主按此推送,与存量自动衔接:
> 同行同 `updated_gmt_at` 幂等去重,行更新(`updated_gmt_at` 变)自动版本演进。**重叠无害,宁多勿漏**。 > 同行按幂等键去重,行更新(`updated_gmt_at` 变)自动版本演进。**重叠无害,宁多勿漏**。
> 时间字段的可空语义与 `created_gmt_at` 兜底见 §1 通用约定。
--- ---
...@@ -24,7 +25,7 @@ icon: FileJson ...@@ -24,7 +25,7 @@ icon: FileJson
|---|---| |---|---|
| **id 类字段一律字符串** | `patient_id` / `id` / `appointment_id` 等统一 String(数值型会被 PAC 校验拒收——存量摄入实测踩过的坑) | | **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 小时) | | **时间格式** | 统一传 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` 必须随行更新 bump** | 它是幂等键的一半:不 bump,该行的更新会被永久去重丢弃 | | **`updated_gmt_at`** | **可空,缺失不影响变更捕获**。PAC 的变更识别是**内容驱动**的:按 `(subjectId + 内容 hash + 状态 + 时间锚)` 判定,不看 `updated_gmt_at`——同一 `subjectId` 内容变了就升版本,没变就幂等去重。所以只要 `subjectId` 稳定、内容如实推,时间字段缺不缺都不丢更新。`updated_gmt_at` 仅用于**乱序防护**(补推的旧快照晚到时不覆盖新版本);为 null 时该保护退回"按到达顺序",对正常单向推送无影响。**无需宿主侧 COALESCE 兜底,重推安全。** |
| **空值** | 传 `null` 或省略字段;不要传 `"NULL"` 字符串 | | **空值** | 传 `null` 或省略字段;不要传 `"NULL"` 字符串 |
| **金额单位** | **元**(decimal 原值,实证:洁治单价均值 ¥310/已结算客单均值 ¥1,730);PAC 侧转分 | | **金额单位** | **元**(decimal 原值,实证:洁治单价均值 ¥310/已结算客单均值 ¥1,730);PAC 侧转分 |
| **投递语义** | at-least-once:未收到 2xx 确认就重推;重复由 PAC 幂等键吸收 | | **投递语义** | at-least-once:未收到 2xx 确认就重推;重复由 PAC 幂等键吸收 |
...@@ -158,17 +159,21 @@ icon: FileJson ...@@ -158,17 +159,21 @@ icon: FileJson
--- ---
## 5. 结算链(4 个 source) ## 5. 结算链(3 个 source)
> **拆分规则(宿主侧按 status/金额分流成两个 source 推)**。`status` 官方语义 > **结算原表照抄推送,宿主不做任何 status/金额过滤或分流**——消费 / 整单退费 / 行级退费明细的
> (`PatientSettlementEntity` 实体注释,比枚举类更全):`0`未结算 `1`已结算 `2`只生成uuid > 切分全部由 PAC transforms 完成(单一真理源,与 file / cold-import 同口径)。宿主只推 3 张原表:
> `3`退款 `4`插入的退款数据 `5`重新结算数据(废弃历史行) `6`医生未提交 `7`流程结束 > `patient_settlement`(结算头,全 status)、`patient_settlement_spec`(结算明细,全量)、`settlement_modes`。
> `8`欠款补缴插入数据。**0/2/5/6/7/8 均不推**——其中 `8` 是原单克隆(receivable_this >
> 原样复制,`payment_arrears`=本次补缴额,ref 挂原欠费单),入消费会双计应收;`7` 零金额流程关单。 > `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` — 消费单 ### `patient_settlement` — 结算头原表(全 status 照推,含消费与整单退费)
**推送范围:`status ∈ {1, 3}` 且 `receivable_this >= 0`。** **推送范围:全部 `status` 照推**(含退费的 `status=4`、`receivable_this` 负值行);消费/整单退费的切分由 PAC 完成。
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
|---|---|---|---| |---|---|---|---|
...@@ -177,35 +182,29 @@ icon: FileJson ...@@ -177,35 +182,29 @@ icon: FileJson
| `organization_id` | string | ✅ | 诊所 | | `organization_id` | string | ✅ | 诊所 |
| `patient_id` | string | ✅ | → 患者 id | | `patient_id` | string | ✅ | → 患者 id |
| `doctor_id` | string | | 开单医生 | | `doctor_id` | string | | 开单医生 |
| `status` | string/number | ✅ | 见上 | | `status` | string/number | ✅ | 全值照推(见 §5 引言;PAC 侧切分) |
| `receivable_this` | number(元) | ✅ | 应收(PAC 计患者价值/LTV 用) | | `receivable_this` | number(元) | ✅ | 应收(PAC 计患者价值/LTV 用);**可为负 = 退费表达**(status=4 或部分品牌 status=3 负额) |
| `net_receipts_this` | number(元) | | 实收 | | `net_receipts_this` | number(元) | | 实收(退费行为负,PAC 取绝对值) |
| `billing_date` | string(datetime) | | 开单时间 | | `billing_date` | string(datetime) | | 开单时间 |
| `registration_id` | string | | 关联接诊号 | | `registration_id` | string | | 关联接诊号 |
| `ref_settlement_id` | string | | (消费单一般为空) | | `ref_settlement_id` | string | | **退费行(status=4)= 被退的原结算单 uuid**(PAC 据此把退费挂回原消费);消费行一般空 |
| `settlement_serial_num` | string | | 结算流水号 | | `settlement_serial_num` | string | | 结算流水号 |
| `reason` | string | | 备注/原因 | | `reason` | string | | 备注/原因 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | | | `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |
### `patient_settlement_refund` — 整单退费 ### `patient_settlement_spec` — 结算明细原表(全量照推,含行级退费)
**推送范围:`status = 4` 或(`status = 3` 且 `receivable_this < 0`)。** 字段同 `patient_settlement`;其中: **推送范围:结算明细全量照推**(含 `is_refund` 各值);行级退费(`is_refund=1`)由 PAC 侧切分。
- `net_receipts_this` 负值 = 冲减金额(PAC 取绝对值)
- `ref_settlement_id` = **被退的原结算单 uuid**(status=4 必带;PAC 据此把退费挂回原消费)
### `patient_settlement_spec_refund` — 行级部分退费
**推送范围:结算明细行中 `is_refund = 1` 的行。**
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
|---|---|---|---| |---|---|---|---|
| `id` | string | ✅ | 明细行主键 | | `id` | string | ✅ | 明细行主键 |
| `tenant_id` / `organization_id` / `patient_id` | string | ✅ | 同上 | | `tenant_id` / `organization_id` | string | ✅ | 品牌 / 诊所 |
| `settlement_id` | string | ✅ | 所属结算单 uuid(退费挂回原消费) | | `settlement_id` | string | ✅ | 所属结算单 uuid(退费挂回原消费;**也是 PAC 认患者的依据**) |
| `cure_name` / `service_project_name` | string | | 被退项目名 | | `patient_id` | string | | ⚠️ **部分品牌此列为诊所本地 id、不可信**(实测元和王永品牌全列与结算头不符);PAC 不用它认患者,而是按 `settlement_id` 从 `patient_settlement` 头单继承真实 `patient_id` |
| `receivable_this` / `net_receipts_this` | number(元) | ✅ | 被退金额(正值) | | `cure_name` / `service_project_name` | string | | 项目名(退费行=被退项目) |
| `is_refund` | string/number | ✅ | 恒为 `1` | | `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) | ✅ | | | `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | |
### `settlement_modes` — 支付通道明细(每单×通道一行) ### `settlement_modes` — 支付通道明细(每单×通道一行)
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
"deployment", "deployment",
"deploy-runbook", "deploy-runbook",
"ingestion", "ingestion",
"data-reconciliation",
"monitoring", "monitoring",
"troubleshooting", "troubleshooting",
"---参考---", "---参考---",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment