Commit b707b1c4 by luoqi

docs(recall): 补「宿主跟进闸」契约 —— 宿主正在跟进的患者不进召回池

宿主自己已建复杂病例、客服/咨询师在推进的患者,PAC 不该再发起召回:
两边同时联系同一个人,患者体验差,也显得两个系统各说各话。

【落点】patient_profiles.host_follow_up_active(canonical: hostFollowUpActive),
与 do_not_contact / deceased 并列在②合规硬过滤,但性质不同:
  合规闸  = 法务/风险,永久,需人工解除
  跟进闸  = 协作分工,临时,宿主停止跟进即回池

判定用 `IS NOT TRUE` 而非 `= false` —— 该列可空,NULL 表示宿主未提供该信号
(如 jvs-dw 暂未接入),不能因 NULL != false 把这些患者全挡在池外。

【FRIDAY 侧口径】customer_basic_info 新增 inline 列 active_complex_case_count =
该患者未删除且 case_stage ∈ {待跟进,已咨询,已预约,诊疗中} 的 complex_case_info 条数;
已成单(需求已闭环)/ 暂停跟进(宿主已停手)不计,交还 PAC 召回。

为什么由宿主算而非整表推 complex_case_info:
  1. case_stage 的码→中文映射不在库里(column_comment 只写「病例阶段:」值为空),PAC 无从翻译
  2. 一患者可有多条病例,PAC 要的是患者粒度聚合结论
  3. 与既有 inline 纪律一致(同 contacts_tel / std_code / class_name)

️ 两处待 FRIDAY 确认,确认前不启用该闸:
  ① 列名 active_complex_case_count 是 PAC 建议名(源表无此列,属新定义);
     宿主若另有习惯叫法以宿主为准 —— 契约纪律是列名随宿主
  ② complex_case_info.is_del 注释写「未删除1/已删除0」与惯例相反,实际数据 0/1 都有,
     取数前须确认哪个值代表未删除,否则计数整体反掉

顺手修一处已与线上代码矛盾的文档:customer_referee_circle.referee_relationship
的方向说明还是旧的「本行 customer 是 referee 的 X」,而 a38f1b60 已按源库年龄实测
改成同向直译。文档同步为「本行 referee 是 customer 的 X」。

本次仅文档,无代码/schema 改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent dd6795c0
...@@ -52,6 +52,7 @@ icon: Filter ...@@ -52,6 +52,7 @@ icon: Filter
``` ```
① 隔离 host_id + tenant_id ① 隔离 host_id + tenant_id
② 合规硬过滤 patients.active=true AND do_not_contact=false AND deceased=false ② 合规硬过滤 patients.active=true AND do_not_contact=false AND deceased=false
AND host_follow_up_active IS NOT TRUE ← 宿主正在跟进的不召回(见下「宿主跟进闸」)
③ 触发信号 fact.status='active' AND type ∈ {diagnosis_record, recommendation_record} ③ 触发信号 fact.status='active' AND type ∈ {diagnosis_record, recommendation_record}
AND content->>'code' = ANY(allCodes) AND content->>'code' = ANY(allCodes)
④ 冷静期下界 COALESCE(occurred_at, planned_for) <= now - cooldownDays ④ 冷静期下界 COALESCE(occurred_at, planned_for) <= now - cooldownDays
...@@ -70,6 +71,30 @@ icon: Filter ...@@ -70,6 +71,30 @@ icon: Filter
--- ---
## 宿主跟进闸(`host_follow_up_active`)
**要解决的问题**:宿主自己已经在跟进的患者,PAC 不该再发起召回 —— 否则两边同时联系同一个人,
患者体验差,也显得两个系统各说各话。
**位置**:`patient_profiles.host_follow_up_active`,与 `do_not_contact` / `deceased` 并列在②合规硬过滤,
但**性质不同**:
| | 合规闸(`do_not_contact`/`deceased`)| 宿主跟进闸(`host_follow_up_active`)|
|---|---|---|
| 性质 | 法务 / 风险,**永久** | 协作分工,**临时** |
| 解除 | 需人工介入 | 宿主停止跟进 → 下次推主档即回池 |
| 来源 | 患者意愿 / 事实 | 宿主业务系统的跟进状态 |
**判定用 `IS NOT TRUE` 而非 `= false`** —— 该列可空,`NULL` 表示**宿主未提供该信号**
(如 jvs-dw 暂未接入),此时不启用该闸,不能因为 `NULL != false` 把这些患者全挡在池外。
**信号由宿主给,PAC 不猜**:各宿主"正在跟进"的定义不同(FRIDAY 是复杂病例阶段,
其他宿主可能是工单/任务),且宿主的私有枚举 PAC 无从翻译。宿主在推患者主档时
inline 一列自己的跟进凭据,PAC 归一成本字段。FRIDAY 侧的具体口径见
[FRIDAY 推送载荷 · customer_basic_info](/docs/integration/friday-push-payload)。
---
## 「已启动 / 无需召」的牙位级判定(resolved) ## 「已启动 / 无需召」的牙位级判定(resolved)
排除不是简单"做过同类治疗就不召",而是**逐牙算"剩余未治牙位"**:`remaining = 信号牙位 − resolved`,`remaining` 非空才召(全口病退化到 patient/category 级)。判定逻辑在 `clinical-gap/potential-treatment-gap.sql`,**召回与「潜在治疗」画像标签共用同一份**,口径逐字节一致(圈到的人群 = 召回候选)。 排除不是简单"做过同类治疗就不召",而是**逐牙算"剩余未治牙位"**:`remaining = 信号牙位 − resolved`,`remaining` 非空才召(全口病退化到 patient/category 级)。判定逻辑在 `clinical-gap/potential-treatment-gap.sql`,**召回与「潜在治疗」画像标签共用同一份**,口径逐字节一致(圈到的人群 = 召回候选)。
......
...@@ -16,7 +16,7 @@ icon: Database ...@@ -16,7 +16,7 @@ icon: Database
|---|---|---|---| |---|---|---|---|
| `hosts` | 顶层 | upsert(admin) | 接入宿主注册:换票凭据 + 出站 `pull_config` + push 验签密钥 + deep-link 模板。子表 `host_id` FK 到此 | | `hosts` | 顶层 | upsert(admin) | 接入宿主注册:换票凭据 + 出站 `pull_config` + push 验签密钥 + deep-link 模板。子表 `host_id` FK 到此 |
| `patients` | L1 | upsert | 患者主档,**只装身份**(name/phone/dob/病历号);跨诊所共享一行 | | `patients` | L1 | upsert | 患者主档,**只装身份**(name/phone/dob/病历号);跨诊所共享一行 |
| `patient_profiles` | L1 | upsert(1:1) | 患者非事实属性副表:合规标记(`do_not_contact`/`deceased`)+ 标签/备注 + 获客渠道 + 转介绍聚合 | | `patient_profiles` | L1 | upsert(1:1) | 患者非事实属性副表:合规标记(`do_not_contact`/`deceased`)+ 宿主跟进闸(`host_follow_up_active`)+ 标签/备注 + 获客渠道 + 转介绍聚合 |
| `patient_relations` | L1 | upsert(边) | 患者—患者关系边(联系人/亲属/转介绍人);关系人本身也是 patient | | `patient_relations` | L1 | upsert(边) | 患者—患者关系边(联系人/亲属/转介绍人);关系人本身也是 patient |
| `patient_return_visits` | L1 | upsert | 诊所回访任务记录(展示用)。**非临床 fact,不进召回信号** | | `patient_return_visits` | L1 | upsert | 诊所回访任务记录(展示用)。**非临床 fact,不进召回信号** |
| `patient_transactions` | L1 | append-only | 操作账本:谁在哪个诊所对哪个主体做了什么。原文留底 | | `patient_transactions` | L1 | append-only | 操作账本:谁在哪个诊所对哪个主体做了什么。原文留底 |
...@@ -102,9 +102,9 @@ patient_facts.transactionIds → patient_transactions.id → rawPayload ...@@ -102,9 +102,9 @@ patient_facts.transactionIds → patient_transactions.id → rawPayload
**`patients`** — 患者主档,**只装身份**。唯一键 `(host_id, tenant_id, source_unit, external_id)`。`active` 布尔(true=在召回池,false=宿主软删/归档);`name`/`phone` stub 期可空(push 首见只拿到 ref,后续 pull 补全;`phone IS NULL` 自动排除召回池);`medical_record_number` 可读病历号。**不支持物理删除**(PIPL 删除权走匿名化或 `active=false`),子表 `onDelete=Restrict` 兜底防误删。 **`patients`** — 患者主档,**只装身份**。唯一键 `(host_id, tenant_id, source_unit, external_id)`。`active` 布尔(true=在召回池,false=宿主软删/归档);`name`/`phone` stub 期可空(push 首见只拿到 ref,后续 pull 补全;`phone IS NULL` 自动排除召回池);`medical_record_number` 可读病历号。**不支持物理删除**(PIPL 删除权走匿名化或 `active=false`),子表 `onDelete=Restrict` 兜底防误删。
> 召回池硬过滤:`active=true` AND `phone IS NOT NULL` AND(`patient_profiles`)`do_not_contact=false` AND `deceased=false`。 > 召回池硬过滤:`active=true` AND `phone IS NOT NULL` AND(`patient_profiles`)`do_not_contact=false` AND `deceased=false` AND `host_follow_up_active IS NOT TRUE`
**`patient_profiles`** — 患者非事实属性副表(1:1,PK=`patient_id`,Cascade)。合规标记 `do_not_contact`(+原因/时间)、`deceased`;产品标签 `tags[]` + 自由 `notes`;获客渠道 `acquisition_channel`(立柱标准枚举,host 值经 enum_mapping 归一)+ `acquisition_sub`;转介绍聚合 `referral_count` / `referral_amount_cents`。拆出副表让主表身份查询更轻,合规过滤走 join。 **`patient_profiles`** — 患者非事实属性副表(1:1,PK=`patient_id`,Cascade)。合规标记 `do_not_contact`(+原因/时间)、`deceased`;宿主跟进闸 `host_follow_up_active`(宿主侧正在跟进 → 不召回,与合规闸并列但语义不同:合规是**永久/法务**,跟进闸是**临时/协作**,宿主停止跟进即回池);产品标签 `tags[]` + 自由 `notes`;获客渠道 `acquisition_channel`(立柱标准枚举,host 值经 enum_mapping 归一)+ `acquisition_sub`;转介绍聚合 `referral_count` / `referral_amount_cents`。拆出副表让主表身份查询更轻,合规过滤走 join。
**`patient_relations`** — 患者—患者关系边(联系人/亲属/转介绍人)。自关联:关系人本身也是 patient,姓名/电话从对端现取、零冗余。唯一键 `(patient_id, related_external_id, relationship)`;`related_external_id` 始终保留(晚绑定),`related_patient_id` 仅当关系人也是 active patient 时填。 **`patient_relations`** — 患者—患者关系边(联系人/亲属/转介绍人)。自关联:关系人本身也是 patient,姓名/电话从对端现取、零冗余。唯一键 `(patient_id, related_external_id, relationship)`;`related_external_id` 始终保留(晚绑定),`related_patient_id` 仅当关系人也是 active patient 时填。
......
...@@ -39,6 +39,7 @@ icon: Table2 ...@@ -39,6 +39,7 @@ icon: Table2
| `medicalRecordNumber` | 病历号(客服沟通用)| | `medicalRecordNumber` | 病历号(客服沟通用)|
| `status` | `active` / `archived`(默认 active)| | `status` | `active` / `archived`(默认 active)|
| `doNotContact` / `doNotContactReason` / `deceased` | 免打扰 / 离世标记(合规)| | `doNotContact` / `doNotContactReason` / `deceased` | 免打扰 / 离世标记(合规)|
| `hostFollowUpActive` | 宿主侧正在跟进该患者(true → 召回排除闸)。宿主推自己的「跟进中」凭据,PAC 不猜 |
| `tags` / `notes` | 标签数组 / 备注 | | `tags` / `notes` | 标签数组 / 备注 |
| `acquisitionChannel` / `acquisitionSub` | 获客渠道 / 二级渠道 | | `acquisitionChannel` / `acquisitionSub` | 获客渠道 / 二级渠道 |
| `referralCount` / `referralAmount` | 转介绍人数 / 带来金额 | | `referralCount` / `referralAmount` | 转介绍人数 / 带来金额 |
......
...@@ -64,8 +64,44 @@ icon: FileJson ...@@ -64,8 +64,44 @@ icon: FileJson
| `organization_id` | string | | 建档诊所 | | `organization_id` | string | | 建档诊所 |
| `contacts_tel` | string | | **默认联系号(宿主 inline)**——按上述规则挑出的有效号码(= customer_contacts 原列名) | | `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') | | `relationship` | string/number | | **该号归属(宿主 inline,= customer_contacts 原列名)**——`PhoneRelationshipEnum`:`1`本人 `2`爸爸 `3`妈妈 `4`爷爷 `5`奶奶 `6`朋友 `7`配偶 `8`子女 `9`其他(PAC 存 `isSelf`=码=='1') |
| `active_complex_case_count` | number | | **进行中的复杂病例数(宿主 inline,新增)**——宿主侧仍在主动跟进该患者的凭据,PAC 用作召回排除闸。见下方「跟进中患者不进召回池」 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | 建档/更新时间 | | `created_gmt_at` / `updated_gmt_at` | string(datetime) | ✅ | 建档/更新时间 |
#### 跟进中患者不进召回池 —— `active_complex_case_count`(待宿主确认)
**要解决的问题**:宿主自己正在跟进(已建复杂病例、客服/咨询师在推进)的患者,PAC 不该再发起召回 ——
否则两边同时联系同一个人,患者体验差,也显得两个系统各说各话。
**取值口径**:该患者名下**未删除**且 `case_stage` 处于以下 4 档的 `complex_case_info` 条数:
| 阶段 | 是否算"进行中" | 理由 |
|---|---|---|
| 待跟进 | ✅ 计入 | 已建档待联系,宿主即将接触 |
| 已咨询 | ✅ 计入 | 沟通已开始 |
| 已预约 | ✅ 计入 | 已约到店 |
| 诊疗中 | ✅ 计入 | 治疗未结束 |
| 已成单 | ❌ 不计 | 该需求已转化闭环,患者可被其他理由召回 |
| 暂停跟进 | ❌ 不计 | 宿主已停止跟进 → 交还给 PAC 召回 |
无进行中病例填 `0`(**不要留空** —— 留空 PAC 按"宿主未提供该信号"处理,不启用该闸)。
**为什么由宿主算、而不是把 `complex_case_info` 整表推给 PAC**
1. `case_stage` 的**码→中文映射不在库里**(`column_comment` 只写了「病例阶段:」,值为空),
PAC 无从翻译;宿主认识自己的枚举。
2. 一个患者可有多条复杂病例(不同阶段),PAC 需要的是**患者粒度的聚合结论**,不是逐条明细。
3. 与既有 inline 纪律一致 —— 私有字典码 / 头-行聚合由宿主 within-host join 后 inline
(同 `contacts_tel`、`std_code`、`class_name`)。
> ⚠️ **两处待宿主确认,确认前 PAC 不启用该闸**
> ① **列名**:`active_complex_case_count` 是 PAC 侧的建议名(取自宿主表名 `complex_case_info`)。
> 该列宿主源表尚不存在,属新定义 —— 若宿主内部另有习惯叫法(如模块在日志里也称「潜在治疗」),
> 以宿主的为准,PAC 改映射即可,**契约纪律是列名随宿主**。
> ② **`is_del` 语义**:`complex_case_info.is_del` 的注释写「未删除1/已删除0」,与常规惯例相反,
> 而实际数据里 `0` 和 `1` 都有。取数前必须确认哪个值代表"未删除",否则计数会整体反掉。
>
> 也接受宿主只推布尔(`0`/`1`)而非计数;计数的额外价值是 PAC 将来可据此排优先级,不影响本闸。
### `customer_referee_circle` — 转介绍圈(患者-患者关系) ### `customer_referee_circle` — 转介绍圈(患者-患者关系)
| 字段 | 类型 | 必填 | 说明 | | 字段 | 类型 | 必填 | 说明 |
...@@ -75,7 +111,7 @@ icon: FileJson ...@@ -75,7 +111,7 @@ icon: FileJson
| `organization_id` | string | | 诊所 | | `organization_id` | string | | 诊所 |
| `customer_id` | string | ✅ | 本人患者 id | | `customer_id` | string | ✅ | 本人患者 id |
| `referee_patient_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**) | | `referee_relationship` | string/number | | 关系码(官方枚举 `RecommendRelationshipEnum`:`1`配偶 `2`爸爸 `3`子女 `4`兄弟 `5`爷爷 `6`孙子女 `7`朋友 `8`其他亲属 `9`姐妹 `10`妈妈 `11`奶奶 `12`姐弟 `13`兄妹 `14`其他 `15`外公 `16`外婆 `17`外孙子女;**语义=本行 referee 是 customer 的 X**,与 PAC 契约同向,PAC 直译不取逆) |
| `type` | string/number | | 关系类型标记 | | `type` | string/number | | 关系类型标记 |
| `created_gmt_at` / `updated_gmt_at` | string(datetime) | | | | `created_gmt_at` / `updated_gmt_at` | string(datetime) | | |
......
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