# PAC Scenarios — 4 个候选 + 算法重合度分析

> 4 个 scenario 全写完了,放一起看能不能合并 / 拆分边界。

---

## 1. 4 个 scenario 一览

| # | scenario | 中文 | 状态 | 触发数据 | 时间窗参考点 |
|---|---|---|---|---|---|
| ① | `treatment_aftercare_recall` | 疗效保障召回 | ✅ **W2 已落地** | treatment fact 含 completed | 主治疗完成时间 |
| ② | `treatment_initiation_recall` | 启治召回(漏治新链) | ⏳ 占位,W3+ | diagnosis fact 存在 | 诊断时间 |
| ③ | `treatment_chain_disruption_recall` | 链内中断召回 | ⏳ 待业务拍板 | treatment fact 含 completed + next_step | 主治疗完成时间 |
| ④ | `missed_diagnosis_recall` | 漏诊召回 | ⏳ W6+(依赖 AI 影像) | AI image_record 推断 | 影像时间 |

链接:
- ① `treatment_aftercare_recall.md`
- ② `treatment_initiation_recall.md`
- ③ `treatment_chain_disruption_recall.md`
- ④ `missed_diagnosis_recall.md`

---

## 2. 算法骨架对比

### 2.1 统一模式 — 都是 "Gap Detection"

**所有 4 个 scenario 的 SQL 模式 100% 相同**:

```
EXISTS(fact A 满足触发条件)
AND occurred_at(A) 在时间窗内
AND NOT EXISTS(fact B 满足应有条件 跟 A 关联)
AND 通用过滤(active/do_not_contact/deceased/phone)
→ 召回 patient
```

只是 A / B / 时间窗 / 关联方式 不同:

| 字段 | ① aftercare | ② initiation | ③ disruption | ④ missed_dx |
|---|---|---|---|---|
| **fact A** | 主治疗 completed | 诊断 存在 | 主治疗 completed + next_step | AI 影像 finding |
| **fact A 索引** | `treatments[].treatment_category` | `diagnoses[].category` | `treatments[].treatment_category` + `expected_next_step` | `image_record.content.ai_findings[]` |
| **应有 fact B** | 同 subject 的周期复查 encounter | 任意对应治疗 encounter | 链内下一节点 treatment | 医生补的 diagnosis |
| **关联条件** | `related_treatment_subject_id` 引用 A | patient + treatment_category 匹配 | patient + treatment_subtype 匹配 | patient + tooth_position 匹配 |
| **时间窗(典例)** | 330-540 天 | 30-180 天 | 30 天-18 月 | 30-180 天 |
| **失败逻辑** | NO 复查 | NO 治疗 | NO 下一节点 | NO 诊断 |

### 2.2 抽象通用接口

```ts
interface GapSelector {
  factAFilter: SqlFragment;    // 主索引:trigger fact + content path
  factBFilter: SqlFragment;    // 否定:应有 fact + 关联条件
  timeWindow: { minDays, maxDays }; // 中断时间窗
  scoring: { base: number; bonuses: BonusFn[] };
  scenarioKey: string;
}
```

4 个 scenario = 4 个 GapSelector 实例(同接口、不同配置)。

---

## 3. 合并 vs 独立 — 三种工程方案

### 方案 A. **4 个独立 scenario**(每个一个 scenario key)

```
enum PlanScenario:
  treatment_aftercare_recall
  treatment_initiation_recall
  treatment_chain_disruption_recall
  missed_diagnosis_recall

代码:
  4 个 SelectorService(共享 GapSelector 接口)
  4 套配置(SQL + 时间窗 + 评分)

PlanReason.scenario 标具体场景
```

**优点**:
- 业务方一眼分清(每条 reason 客服看到归属一目了然)
- 时间窗 / priorityScore 可独立微调(Host.priorityRules 按 scenario 维度配置)
- 文档独立,合规/法务 / 数据要求各自标注
- 启用 / 停用 / 灰度可独立(W6+ 才上 missed_diagnosis,其他不动)
- 客服 UI 可按 scenario 分组("您今天的:3 条疗效保障 / 2 条新单转化")

**缺点**:
- 工程代码 4 份(虽然底层 GapSelector 共享)

### 方案 B. **合成 2 个 scenario**(算法相似的合并)

```
enum PlanScenario:
  treatment_gap_recall         (合 ① + ③ — 都基于 treatment fact)
  diagnosis_gap_recall         (合 ② + ④ — 都基于 diagnosis 缺失)

PlanReason.sub_scenario 标具体子类(treatment_gap_recall.aftercare / .chain_disruption)
```

**优点**:
- 工程量减半

**缺点**:
- 业务方混淆("链已完成"和"链中断"客服话术差很多)
- 时间窗 / priorityScore 跨子类不同,合一个 scenario 内部要二分 → 实际上回到 4 个配置
- 客服 UI 分类不清

### 方案 C. **合成 1 个 scenario**(全部走通用 gap)

```
enum PlanScenario:
  treatment_gap_recall(唯一)

PlanReason.sub_scenario 4 个值
```

**优点**:
- 最少 enum

**缺点**:
- 业务方完全失去对场景的认知
- "PAC 召回所有 gap" 听起来抽象,卖不出去
- 不同子类的合规要求差异(漏诊有医生审核环节)被埋

---

## 4. 我的工程建议:**方案 A**(4 个独立 scenario)

理由,**按重要性排序**:

### 4.1 业务方需要"可见的 scenario 列表"

PAC 是 B2B 产品,业务方(诊所运营 / 集团总部)在 admin UI 上要看到的是:
> "你们集团 vs 同行 — 启用 PAC 召回场景:
>  ✓ 疗效保障(种植年度复查 + 牙周维护)
>  ✓ 新单转化(漏治新链)
>  ✗ 链内续治(根管未戴冠等)— 未启用
>  ✗ 漏诊召回(W6+)— 未启用"

这是**销售点**。合一起客户看不到 PAC 的能力清单。

### 4.2 合规边界不同

- ①③ — host 已诊断的事实,合规清晰
- ② — host 已诊断 + 客服推进,合规清晰
- ④ — **AI 推断**,法务/医师审核环节差异大

把 ④ 跟其他合一个 scenario,合规风险传染。

### 4.3 客服话术不同

- ①③ "您该来 X 了" — 直接推进
- ② "您之前看过医生说要 X,什么时候安排?" — 决策推进
- ④ "我们的检查系统提示您回来给医生再看看" — 评估推进(避免越界行医)

不同 scenario = 不同 PlanScript 模板 + 不同客服培训。

### 4.4 启用阶段不同

- ① W2 已上
- ② W3+ 数据丰富后上
- ③ 待业务拍板上 / 不上
- ④ W6+ AI 接通后上

合一起灰度复杂,拆开各自独立开关。

### 4.5 工程量边际增量小

底层抽象 GapSelector + 4 个 config / subclass = 1-2 天活,跟"4 份代码"差距很小。**省 1 天合并 vs 永久失去业务清晰度,不划算**。

---

## 5. 结论 + 当前工程动作

### 决定

**保持 4 个独立 scenario**(方案 A):
- ① `treatment_aftercare_recall` — ✅ 已在 enum,文档完整,fixture 数据就位,W3 PlanService 直接做这个
- ② `treatment_initiation_recall` — ✅ 已在 enum 占位,W3+ 实施
- ③ `treatment_chain_disruption_recall` — ⏳ **enum 暂不加**,待业务拍板
- ④ `missed_diagnosis_recall` — ⏳ **enum 暂不加**,W6+ 独立设计(含医生审核环节)

### 工程共享层

抽 `GapSelector` interface,4 个 scenario 各自一个 selector 类(共享底层 SQL 拼装 / 时间窗判断 / priorityScore 计算)。

```
PlanEngineModule
├── selectors/
│   ├── base-gap-selector.ts              (共享接口 + 工具)
│   ├── treatment-aftercare.selector.ts   (① W3 实施)
│   ├── treatment-initiation.selector.ts  (② W3+ 等数据)
│   ├── treatment-chain-disruption.selector.ts (③ 业务拍板后)
│   └── missed-diagnosis.selector.ts      (④ W6+)
```

### 业务方拍板清单(汇总)

| # | 项 | 影响 |
|---|---|---|
| 1 | ③ 链内中断要不要做?| enum 是否加 + #1 #2 子场景归属 |
| 2 | ④ 漏诊 — 合规边界 / 医生审核工作流 | W6+ 启动前置 |
| 3 | 各 scenario 子场景的具体时间窗 | priorityScore 默认值 / Host.priorityRules 模板 |
| 4 | ②/④ 数据要求 host 提供更清晰诊断 | 接入 host 时清单 |

---

## 6. 何时复看本分析

- ✅ 业务方拍板 ③ ④ 后:补 enum / 砍文档
- ✅ 真接入第一个 host 后:看实际数据形态,确认 GapSelector 抽象层是否过度
- ✅ 第一份 V2 客户使用反馈后:看 4 scenario 是否需要重新切分(可能合 / 可能拆)
