# PAC 技术架构设计方案 v2

> **文档性质:** 技术评审文档,面向开发团队,用于方案评审与落地实施。
>
> **核心定位:** **PAC(Patient Analyze Center)= 集团级"患者分析中枢"**,接收宿主侧的患者事实数据,自动评价患者画像,生成召回任务清单。
> 从 Friday SaaS 抽离的可复用领域组件,支持 Friday / 5i5ya / 400 / DW 及未来更多系统以最低对接成本接入。
>
> **v2 更新**(相对 v1 [architecture.md](architecture.md)):
> - 产品命名 `Recall Service` → `PAC`,API 路径 `/recall/v1` → `/pac/v1`
> - 数据层从 "6 镜像表"(diagnoses/treatments/appointments/charges/touchpoints/patient_fact_events)升级为"**双表模型**"(`patient_transactions` 操作账本 + `patient_facts` 事实单元版本流)
> - 多租户隔离从"`tenant_id + clinic_id` 全表三级"重构为"硬隔离 platform+tenant / 立柱业务字段 clinic_id"分级模型
> - 规则引擎从"DB 配置化"(`rulesConfig` JSON)简化为"**应用层 scenario 常量**"(`scenarios/post_treatment_recall.ts`)
> - 实施路径对齐 [W1 报告](w1-report.md) 的 6 周节奏
> - 交叉引用 [three-layer-model.md](three-layer-model.md) / [host-integration.md](host-integration.md) / [scenarios/post_treatment_recall.md](scenarios/post_treatment_recall.md)

---

## 设计初衷

架构设计要同时满足三个目标:

1. **写一遍,到处用。** PAC 的前后端逻辑只维护一份代码,Friday 和 5i5ya 乃至未来更多系统共享同一套服务。
2. **宿主接入成本极低。** 不能让对方团队觉得"接你这个模块比自己重写还麻烦"。接入应该是天级别的事,不是周级别的。
3. **功能闭环且可开放。** PAC 自己能独立运转(有自己的数据、自己的引擎、自己的前端),同时通过 API 向宿主系统开放能力,而不是寄生在某一个宿主内部。

## 架构核心原则

> **骨架按终态设计,肉按需求生长。**
>
> 每一阶段的接口契约在早期定义完成,后续阶段在确定的框架内填充实现,绝不推翻重来。

---

## PAC 的数据边界(v2 重写)

**PAC 自己拥有和管理的数据:**

| 数据 | 说明 |
|---|---|
| **patient_transactions** | 操作账本(append-only),宿主原生事件原文留底(`raw_payload`) |
| **patient_facts** | 事实单元版本流(`subject_id × version`,actual/planned 两类) |
| **personas / persona_features** | PAC 自产的患者画像(13 个候选 feature_key) |
| **followup_plans / scripts / summaries / executions** | 召回计划版本流 + 客服执行回写 |
| **agent_invocations** | AI 调用账本(token / cost / replay) |
| **三张 orchestration log** | sync_logs / persona_recompute_logs / plan_generation_logs |
| **platforms** | 接入宿主注册(鉴权 / fieldMapping / actionUrls / pullConfig) |

**数据来源于宿主、PAC 内部持有的:**

| 数据 | 说明 |
|---|---|
| **transactions.raw_payload** | 宿主原生事件 / 资源对象 100% 原文(adapter 收到啥存啥) |
| **facts.content (canonical)** | parser 把 raw_payload 翻译成 PAC canonical 形态 + 衍生 |

**核心原则:** **PAC 不持有完整 EMR**,病历原文按需通过 `agent_invocations.input_snapshot` 临时借用(受保留期管控);所有业务计算在本地完成,运行时不依赖宿主实时接口。

详见 [three-layer-model.md](three-layer-model.md)。

---

## 目录

- [一、后端部署策略选型](#一后端部署策略选型)
- [二、前端嵌入策略选型](#二前端嵌入策略选型)
- [三、鉴权体系设计](#三鉴权体系设计)
- [四、数据获取策略选型](#四数据获取策略选型)
- [五、宿主动作跳转与状态流转](#五宿主动作跳转与状态流转)
- [六、对外 API 体系](#六对外-api-体系)
- [七、Webhook 事件推送](#七webhook-事件推送)
- [八、召回算法 — Scenario 应用层常量(v2 重写)](#八召回算法--scenario-应用层常量)
- [九、多租户隔离(v2 重写)](#九多租户隔离)
- [十、SDK 与交付物策略](#十sdk-与交付物策略)
- [十一、宿主系统接入全流程](#十一宿主系统接入全流程)
- [十二、数据合规与隐私保护](#十二数据合规与隐私保护)
- [十三、分阶段实施路径(v2 对齐 W1 报告)](#十三分阶段实施路径)

---

## 一、后端部署策略选型

> 跟 v1 完全一致 — PAC 采用方案 C(完全独立服务),不复述。详见 [architecture.md §1](architecture.md#一后端部署策略选型)。

**核心决策**:方案 C 完全独立服务。前期一定的部署运维成本可控,获得物理隔离与完全解耦的长期价值,**最大程度降低宿主接入成本**(目标:1 天级)。

---

## 二、前端嵌入策略选型

> 跟 v1 完全一致 — iframe 为主,npm 组件包为备(后期按需)。详见 [architecture.md §2](architecture.md#二前端嵌入策略选型)。

**核心约束**:**零 postMessage** — iframe 作为纯渲染容器,所有数据交互走后端,前端之间彻底解耦。

```
宿主前端                        PAC 前端(iframe)
   │                               │
   │  ❌ 零通信                      │
   │  (无 postMessage)              │
   │                               │
   │                               │  只调 PAC 后端
   │                               ↓
   │                           PAC 后端
   │                            ↑    │
   ↓                            │    ↓
宿主后端 ── access_token ────▶  │   AiCall 框架 → DeepSeek
   │                            │
   │ ◀── 服务账号 token ───────┘
   │         (PAC 同步引擎拉取数据)
```

🔐 **安全提示:** HTTP Header 配置 `frame-ancestors`(CSP)防范点击劫持,iframe 增加 `sandbox` 最小权限控制。`frame-ancestors` 配置由租户接入时动态注入,新增宿主无需发版。

---

## 三、鉴权体系设计

### 3.1 双向鉴权全景

PAC 与宿主之间存在双向调用,鉴权也是双向的:

| 方向 | 场景 | 鉴权方式 | 详见 |
|---|---|---|---|
| 宿主 → PAC | 换票、iframe 加载、调 PAC API | appId + appSecret 换 access_token | 本章 3.2-3.6 |
| PAC → 宿主 | 同步引擎拉资源 / users:resolve | 宿主颁发 apiKey(推荐)或 loginUrl + 服务账号 | 第四章 4.3 |

各管各的凭证,各管各的权限,双方只需在接入时交换一次凭证即可。

### 3.2 核心原则

所有请求必须携带 **PAC 自己签发的 access_token**(JWT)。无论来自 iframe 还是宿主后端 API 调用,统一走 Token Exchange 换票机制。不接受任何外部系统的原生 token。

### 3.3 Token Exchange 换票流程

宿主后端用 `appId + appSecret` + 用户信息,调 `POST /pac/v1/auth/token` 换取短时效 JWT。

```
宿主后端                              PAC 后端
   │                                    │
   │  POST /pac/v1/auth/token           │
   │  { appId, appSecret, user }        │
   │ ─────────────────────────▶         │
   │                                    │  验 appId + appSecret
   │                                    │  用自己的密钥签 JWT
   │  ◀─────────────────────────        │
   │  { accessToken, refreshToken,      │
   │    code, expiresIn, codeExpiresIn }│
   │                                    │
   │  后续所有请求带 accessToken          │
   │  Authorization: Bearer <token>     │
   │ ─────────────────────────▶         │
   │                                    │  用自己的密钥验签
```

### 3.4 换票必传用户信息

换票时要求必传用户信息:`userId`、`role`(PAC 框架固定 `staff` / `leader` / `admin` 三个常量)、`tenantId`、`clinicIds`(用户授权的诊所 id 列表)、**可选 `dictionary`**(含 userName / clinics name 字典)。

access_token 中包含租户身份 + 用户身份 + 诊所权限范围。PAC 在数据查询和线索分配时,基于 `tenant_id` 硬隔离,基于用户的 `clinicIds` 做行级业务过滤,防止 A 诊所的客服看到 B 诊所的线索。

详见 [host-integration.md §2](host-integration.md#2-鉴权)。

> **权限时效性注意:** 宿主用户被禁用或权限变更后,需考虑强制下线机制。短时效 token(2 小时)天然提供了一层保障——权限变更后最迟 2 小时生效。如需即时生效,可通过 PAC 维护 token 黑名单或缩短 token 时效实现。

### 3.5 iframe 场景的 Token 传递——一次性 code

iframe 场景下不能通过 URL 直接传 access_token(会留在浏览器历史和日志中)。采用**一次性 code 中转**(60 秒过期 + 一次即作废)。

```
宿主后端换票获得 accessToken + code
    ↓
将 code 返回给宿主前端
    ↓
宿主前端设置 iframe src:
  https://pac.example.com/embed?code=abc123
    ↓
PAC 前端加载后,拿 code 调自己后端:
  POST /pac/v1/auth/exchange-code { code: "abc123" }
    ↓
PAC 后端验证 code(一次即作废),返回 accessToken
    ↓
PAC 前端持 token 正常工作
```

> **浏览器兼容性要求:** PAC 前端在 iframe 内拿到 access_token 后,必须将 token 存储在内存中(JavaScript 变量),每次请求通过 `Authorization: Bearer` Header 显式传递,**绝不依赖 Cookie 维持登录态**。规避 Safari ITP、Chrome 第三方 Cookie 限制。

### 3.6 Token 刷新

access_token 2 小时时效,refresh token 机制配套。详见 [host-integration.md §2.2](host-integration.md#22-换票流程host-backend--pac)。

---

## 四、数据获取策略选型

> 这是整个架构中讨论最充分的问题——"PAC 自己不持有患者数据,需要用的时候怎么拿?"

### 4.1 三种方案对比(v2 新加 Push)

| 方案 | 数据流向 | 宿主改动量 | 实时性 |
|---|---|---|---|
| A: 宿主后端主动推送(老式 webhook) | 宿主后端在业务节点加推送调用 | 侵入宿主业务代码的十几个节点 | 实时 |
| **B: PAC 同步引擎主动拉取(主流推荐)** | PAC 定时调宿主只读接口 | 零代码改动(复用现有接口) | 准实时(分钟级) |
| **C: 宿主推 transaction 事件(v2 新增,可选增强)** | 宿主主动推 transaction 报文到 PAC | 接入 push endpoint + HMAC | 秒级 |

### 4.2 方案 A 被设为非主方案的原因(宿主主动推送)

- 需要在宿主代码的十几个业务节点(写病历、完成治疗、创建预约 ...)逐一加推送调用
- 漏推某个节点就导致数据不一致,排查困难
- 宿主开发团队抵触:"我改自己的业务逻辑还要想着 PAC 的需求"

### 4.3 方案 B 详细设计——PAC 同步引擎主动拉取(主流推荐)

#### 核心思路

PAC 内部运行**同步引擎**,定时从宿主拉取**资源对象当前态**;adapter 比对前后态合成 **canonical transaction** → parser 衍生 fact 版本流。

```
┌─────────────────────────────────────────────┐
│                  PAC                         │
│                                              │
│   Sync Engine(定时增量拉取)                  │
│   ┌──────────────────────────┐               │
│   │ 拉 patients              │               │
│   │ 拉 appointments          │               │
│   │ 拉 clinical_encounters   │               │
│   │ 拉 charge_orders         │               │
│   │ 拉 payments              │               │
│   │ ... (11 种资源 endpoint)  │               │
│   └──────────┬───────────────┘               │
│              ↓ adapter 比对前后态             │
│   patient_transactions(append-only 账本)    │
│              ↓ parser 解析衍生                │
│   patient_facts(canonical 版本流)            │
│              ↓ Persona engine                 │
│   personas + persona_features                 │
│              ↓ PlanEngine                     │
│   followup_plans + scripts + summaries        │
└─────────────────────────────────────────────┘
```

#### 宿主需要提供的资源 endpoint(v2 重写 — 11 个 endpoint 对齐新 subject_type 维度)

宿主只需开放**带增量游标的只读列表接口**:

```
GET /api/patients?after=<cursor>&limit=200            # 必填
GET /api/appointments?after=<cursor>&limit=200        # 必填
GET /api/clinical_encounters?after=<cursor>&limit=200 # 必填
GET /api/charge_orders?after=<cursor>&limit=200       # 必填
GET /api/payments?after=<cursor>&limit=200            # 必填
GET /api/consultations?...                            # 推荐
GET /api/visit_registrations?...                      # 推荐
GET /api/visit_receptions?...                         # 可选
GET /api/emr_documents?...                            # 推荐
GET /api/refunds?...                                  # 推荐
GET /api/image_studies?...                            # 可选(影像 metadata,无需文件)

POST /api/users:resolve                               # 必填(Plan 自动分配查客服)
```

完整契约见 [host-integration.md §3](host-integration.md#3-pull-路径pac-主动拉资源)。

#### 宿主接口鉴权——apiKey 推荐(v2 调整)

**v2 推荐改用 apiKey** 而非服务账号 login(更简单、更稳定、易轮换):

| 方式 | 推荐度 | 适合 |
|---|---|---|
| **`apiKey`**(Bearer) | ⭐⭐⭐ 推荐 | 现代化系统(JWT / API Gateway 已有) |
| `loginUrl + username + password` | ⭐ 兜底 | 老系统(只有 session-based 登录) |

#### Platform 配置(v2 — 字段对齐新 schema)

PAC 端 `platform.pullConfig` JSONB 字段保存配置:

```jsonc
{
  "endpoints": {
    "patients":            "https://friday.xxx.com/api/patients",
    "appointments":        "https://friday.xxx.com/api/appointments",
    "clinical_encounters": "https://friday.xxx.com/api/encounters",
    "charge_orders":       "https://friday.xxx.com/api/charge-orders",
    "payments":            "https://friday.xxx.com/api/payments",
    "users":               "https://friday.xxx.com/api/users/resolve",
    "...":                 "..."
  },
  "credentials": {
    "apiKey": "<plaintext;PAC 落库 scrypt hash>"
  },
  "amountUnit": "yuan",    // 必填,host 金额数值单位
  "timezone":   "Asia/Shanghai",  // 必填,IANA 时区
  "intervalMinutes": 5
}
```

`platform.fieldMapping` 配置字段命名翻译(host 用自己命名 → PAC canonical,见 [host-integration.md §7](host-integration.md#7-field-mapping-配置))。

#### 同步引擎运行机制

- **增量拉取:** 每次同步记录上一次拉取游标(`sync_logs.cursorAfter`),下次只拉 `?after=<cursor>` 的变更。游标语义见 [host-integration.md §3.5](host-integration.md#35-增量游标契约)
- **频率可配置:** 按 platform 配置(默认 5 分钟,可调 1-1440 分钟)
- **字段映射:** 各宿主格式不同,通过 `fieldMapping` JSONB 配置 + adapter 代码处理复杂转换(单位 / 时区 / 嵌套结构),纯配置覆盖 1:1 映射;复杂转换由 adapter 代码处理
- **token 缓存:** apiKey 模式无状态;login 模式 token 缓存复用,过期自动重登
- **故障告警:** 登录失败连续 N 次触发告警,不静默失败
- **凭证管理:** apiKey / password 在 PAC 侧 scrypt 哈希存储(plaintext 永不回读),配置变更有审计日志
- **权限最小化:** 服务账号必须专用、只读,只拉召回业务必要字段

#### 对接契约约束

- 宿主关键资源(尤其 appointments / charge_orders)必须采用**逻辑删除**并更新 `updatedAt`,或额外提供已删除记录查询接口,防止物理删除产生"幻读"
- 数据接口必须支持**增量游标分页**(`?after=<cursor>&limit=200`),PAC 自动翻页拉取
- 宿主侧需为服务账号配置 **API 限流白名单**,或 PAC 自行控制请求间隔

#### 七层数据可靠性防线(v2 新加 — 跟 schema 设计对齐)

PAC 在 schema 和应用层共构建了七层防线:

| # | 防线 | 落地 |
|---|---|---|
| 1 | 幂等 | `patient_transactions.source_event_id` partial UNIQUE |
| 2 | 水位续传 | `sync_logs.cursor_before/after` |
| 3 | 原始报文留底 | `patient_transactions.raw_payload` 永久原文 |
| 4 | Reconcile 对账 | `sync_logs.triggered_by='reconcile:<date>'` |
| 5 | Quarantine 失败转储 | 应用层兜底(v1)/ 专表(W6+) |
| 6 | Drift 监控 | 应用层 metric |
| 7 | 推断 stale | `personas.event_watermark` BigInt |

### 4.4 方案 C 详细设计——宿主推 transaction 事件(v2 新增)

宿主有现代化事件总线时,可主动推 transaction 报文到 PAC,实时性秒级:

```
POST /pac/v1/push/events
Headers: X-Pac-Signature (HMAC) + X-Pac-Timestamp
Body: { events: [{action, subjectType, subjectId, rawPayload, occurredAt, ...}] }
```

`source_event_id` 幂等键由 host 提供,PAC 用 `pushSecretHashes[]` 数组验签(支持平滑密钥轮换)。详见 [host-integration.md §4](host-integration.md#4-push-路径宿主主动推事件)。

### 4.5 数据实时性评估

PAC 召回不是秒级实时场景。医生上午写的病历,客服可能下午甚至第二天才回访。5 分钟的同步延迟对业务完全没有影响。

**陈旧性容忍度划分:**
- **容忍分钟级延迟:** 召回池计算、线索列表展示、统计报表
- **需要实时拉取:** 客服点击"刷新"按钮 / 拨号前的患者状态确认 → 单患者按需 pull(`POST /pac/v1/sync/patient/{id}/refresh`)

### 4.6 按需同步机制

除定时增量拉取外,PAC 在自身关键业务节点主动触发单患者即时同步:

```
客服点击"刷新患者数据"
   ↓
POST /pac/v1/sync/patient/{patientId}/refresh
   ↓
sync engine 即时调 host endpoint 拉该患者全量当前态
   ↓ adapter + parser
新 transaction / fact 版本写入
   ↓
触发 Persona / Plan 重算
   ↓
UI 刷新到最新视图
```

并发去重:30 秒内同 patient 多次请求合并到同一个 run;60 秒冷却防滥触发。

### 4.7 历史数据冷启动

首次接入时,同步引擎将 `cursor=null` 起步,自动分页拉取最近 24 个月数据。

- 冷启期间暂停增量同步,避免游标混乱
- 使用游标分页(避免 deep offset 性能问题)
- 限流退避策略,防止压垮宿主接口
- 支持断点续传(中途失败可从上次位置恢复)

---

## 五、宿主动作跳转与状态流转

### 5.1 动作跳转——后端路由模板配置

`platform.actionUrls` JSONB 字段配置 deep-link 模板:

```jsonc
{
  "VIEW_PATIENT":       "https://friday.xxx.com/patient/{patientId}/profile",
  "VIEW_EMR":           "https://friday.xxx.com/emr?patientId={patientId}",
  "CREATE_APPOINTMENT": "https://friday.xxx.com/appointment/new?patientId={patientId}",
  "VIEW_XRAY":          null   // null = 不支持,前端按钮不渲染
}
```

PAC 前端从自己后端拿到该 platform 的路由配置:
- 有 URL 的渲染对应按钮,拼好参数
- `null` 的不渲染
- 按钮使用 `<a href="..." target="_top">` 原生跳转,零 postMessage

详见 [host-integration.md §8](host-integration.md#8-action-urls)。

### 5.2 状态自动流转

线索状态的更新不依赖宿主主动回写。同步引擎定时拉取后,PAC 重新执行 selector + state machine:
- 患者新增了预约 → 对应 plan 自动跳过(`active appointment_plan` fact 存在时不召回)
- 患者完成了治疗 → 对应 plan 标记 completed(下次重算时不再生成)

`plan_executions` 反馈驱动 Plan 状态机(active → assigned → completed / abandoned),**v1 不回写 fact 层**(架构债待评估),保持数据流单向。

---

## 六、对外 API 体系

### 6.1 API 分类

**业务操作类 `P0`:** PAC 前端(或宿主自写前端)调用——获取召回名单、分配/回收/执行回写、单患者刷新、查询 patient timeline / persona / plan 详情等。

**Plan assets 生成:** PlanScript / PlanSummary 采用**异步 BullMQ** 队列驱动,通过 AiCall 框架直连 DeepSeek V4 Pro。前端展示 `status='pending'` 占位,生成完后 SSE 推送或前端轮询(`GET /plans/{id}`)拉到最新。

**鉴权类 `P0`:** 宿主后端调用——Token Exchange 换票(`/auth/token`)、一次性 code 兑换(`/auth/exchange-code`)。

**数据查询类 `P2`:** 召回转化率、客服绩效、线索漏斗、AI 调用成本等统计数据。

### 6.2 鉴权统一

所有 API 统一使用 `Authorization: Bearer <accessToken>`。

### 6.3 响应信封统一

HTTP 状态码统一 `200`(除真崩溃 5xx),业务异常通过 envelope `code` 区分:

```jsonc
{ "code": 0, "msg": "ok", "data": { /* payload */ } }
```

Code 编号规则:5 位 `A-BB-CC`(A 错误来源 / BB 模块 / CC 顺序)。完整列表见 [`packages/types/src/schemas/wrap.ts`](../packages/types/src/schemas/wrap.ts) `ApiCode` 常量。

### 6.4 AI 调用配额与限流

按 **用户 / 诊所 / 租户** 三个维度设限,防止恶意刷新话术导致 Token 费用失控。配额配置在应用层(不进 schema)。

---

## 七、Webhook 事件推送(PAC → 宿主)`将来 P2`

### 7.1 适用场景

当 PAC 内部发生关键事件(新 plan 产生、自动回收、自动 abandoned、AI 服务降级)时,主动推送通知给宿主系统。

### 7.2 触发实施的信号

- 宿主对实时性有明确要求(如新线索产生需即时通知客服)
- 接入方超过两个,轮询 API 产生性能压力
- 当前阶段宿主可通过轮询 `GET /plans?status=...&updatedAfter=...` API 替代

### 7.3 设计要点(预留)

| 要素 | 设计 |
|---|---|
| 事件类型 | `plan.created` / `plan.assigned` / `plan.recycled` / `plan.completed` / `plan.abandoned` / `system.ai_degraded` |
| 注册方式 | 宿主提供 callbackUrl + 签名密钥 + 订阅事件列表(W6+ 加入 platform schema 字段) |
| 可靠性 | 幂等 eventId、HMAC-SHA256 签名、指数退避重试(5s → 30s → 2min → 10min,最多 5 次) |
| 死信队列 | 重试 5 次仍失败的关键事件入库,提供 API 手动补偿 |

详见 [host-integration.md §9](host-integration.md#9-pac-outbound-webhook可选--嵌-iframe-时考虑)。

---

## 八、召回算法 — Scenario 应用层常量(v2 重写)

### 8.1 v1 → v2 设计演进

| 维度 | v1(`rulesConfig` JSONB DB 配置) | v2(应用层常量) |
|---|---|---|
| 配置位置 | `platform.rulesConfig` JSONB 字段 | `scenarios/post_treatment_recall.ts` 代码常量 |
| 灵活度 | 每 platform 可不同 | 集团统一规则 |
| 维护方 | 运维改 DB | PAC 团队改代码 |
| 验证 | 改完不可知 | 代码 review + CI |

**v2 重大决定:** 删除 `Platform.rulesConfig` 字段,scenario 规则改为**应用层常量** — 集团统一运营,不需要 per-platform 差异化。

### 8.2 v1 召回 scenario(post_treatment_recall)

PAC 一期主战场,**4 个子场景**同属一个 scenario:

| 子场景 | 业务定义 | 时间窗 |
|---|---|---|
| **#1 根管未戴冠** | 根管完成但没及时加冠保护 | 30 天-18 个月 |
| **#2 正畸中断 / 保持器期** | 正畸完成 2 年内,长期未回诊 | 6-24 个月 |
| **#3 种植年度复查** | 种植完成超过 11 个月,没有年度复查 | 330-540 天 |
| **#4 牙周维护期** | 牙周治疗后超过 5 个月,未做维护 | 150-360 天 |

完整规则、selector 伪 SQL、priorityScore 公式、阈值参数集中表 — 见 [scenarios/post_treatment_recall.md](scenarios/post_treatment_recall.md)。

### 8.3 后续 scenario 扩展(W5+ 评估)

| 候选 scenario | 业务定义 | 何时做 |
|---|---|---|
| `incomplete_treatment_recall` | 漏治召回(已诊断未治疗) | W5+ 评估业务策略后 |
| `image_ai_missed_diagnosis` | 漏诊召回(影像 AI > 医生诊断) | W5+ 接朝厚影像 AI 后 |
| `sleeping_customer_activation` | 沉睡客户激活 | W6+ 视试点反馈 |
| `complaint_followup` | 投诉后跟进 | W5+ 接入投诉数据源后 |
| `vip_followup` | 高价值客户跟进 | 通过 post_treatment_recall 的 VIP 加权解决 |

### 8.4 阈值参数集中(v0 默认,试点后调)

| 参数 | 值 | 位置 |
|---|---|---|
| 根管时间窗 | 30 天-18 个月 | `scenarios/post_treatment_recall.ts:WINDOWS.endodontic_crown` |
| 正畸时间窗 | 6-24 个月 | 同上 `.ortho_retention` |
| 种植时间窗 | 330-540 天 | 同上 `.implant_annual_review` |
| 牙周时间窗 | 150-360 天 | 同上 `.perio_maintenance` |
| VIP 累计消费门槛 | ¥20,000 | `personas/value-feature.ts` |

---

## 九、多租户隔离(v2 重写)`P0`

### 9.1 v1 → v2 设计演进

| 维度 | v1(`tenant_id + clinic_id` 全表三级) | v2(分级模型) |
|---|---|---|
| 隔离粒度 | 所有表都带 `tenant_id + clinic_id` | 按表语义分级 |
| `clinic_id` 角色 | 硬隔离字段 | 业务字段(立柱有索引,非硬边界) |
| 跨诊所可见性 | DB 层禁止 | 应用层 RBAC 决定 |

### 9.2 隔离分级模型(v2)

```
硬隔离 (platform + tenant)
   — 所有业务表的强制基线,跨 tenant 不允许越界(应用层 + 查询拦截器)

立柱 clinic_id (业务字段)
   — patient_transactions(操作发生地)
   — plan_executions(执行发生地)
   — 同 tenant 内跨 clinic 的可见性由应用层 RBAC 决定,不是 DB 硬边界

立柱 target_clinic_id (业务字段)
   — followup_plans(业务归属诊所,nullable = 集团统一客服池)

不立 clinic_id
   — 患者身份(patients)
   — 推断层(personas / followup_plans 的 clinic 维度走 target_clinic_id)
   — 跨任务日志(sync_logs / persona_recompute_logs)
   — AI 调用账本(agent_invocations)
```

### 9.3 跨诊所查询

| 业务问题 | 实现 |
|---|---|
| "该患者去过哪些诊所" | `patient_transactions.clinic_id` 反查 |
| "某诊所的召回池" | `followup_plans.target_clinic_id` 业务过滤 |
| "Staff 看自己负责的" | `assignee_user_id = <userId>` 过滤 |
| "Leader 看本团队" | 应用层 RBAC,过滤 `target_clinic_id IN <user.clinicIds>` |

### 9.4 Merge 穿透查询(v2 新加)

`patient.status='merged'` 时,历史 transactions / facts 仍挂在被合并的旧 patient_id 上(保审计原貌);应用层读历史时用**递归 CTE** 穿透 root + mergedFrom 子树。详见 [`apps/pac-service/prisma/schema.prisma`](../apps/pac-service/prisma/schema.prisma) Patient model 注释。

### 9.5 单诊所场景兼容

如果某 platform 只有一个诊所(或不区分诊所概念),接入时统一分配一个默认 `clinic_id`(如 `default`),业务逻辑跟多诊所一致。

### 9.6 AI 用量隔离

`agent_invocations` 立 `platform_id + tenant_id`(无 clinic 维度),用量统计 + 配额管理走 platform / tenant 粒度。

---

## 十、SDK 与交付物策略

> 跟 v1 完全一致 — iframe + API 文档为主,后端 SDK 接入方 > 3 时评估。详见 [architecture.md §10](architecture.md#十sdk-与交付物策略)。

**文档是契约,SDK 是契约的便捷实现。** 先有契约,后有封装。

---

## 十一、宿主系统接入全流程(v2 简化)

### 11.1 接入双方交换的信息

```
PAC → 宿主:
  ① appId + appSecret(一次性 plaintext)
  ② pushSecret(可选,若启用 push 接入)
  ③ API 文档(Swagger / OpenAPI / Redoc HTML)
  ④ iframe 嵌入地址

宿主 → PAC(配置清单一次性提交,详见 host-integration.md §10.2):
  ① 资源 endpoint URL(11 种)
  ② 鉴权方式(apiKey ⭐ 或 loginUrl)
  ③ amountUnit + timezone(必填)
  ④ fieldMapping yaml(host 字段命名清单)
  ⑤ actionUrls 模板(deep-link 跳回 host UI)
  ⑥ 试点 clinic 字典
  ⑦ 联系人 + oncall 渠道
```

### 11.2 宿主开发工作量清单

| 工作项 | 前端/后端 | 工作内容 | 预估工时 |
|---|---|---|---|
| 创建服务账号 + 颁发 apiKey | 后台配置 | 操作宿主管理后台 | 10 分钟 |
| 实现 11 个资源 endpoint 中至少 5 个必填 | 后端 | 暴露 REST API + 增量游标 | 1-3 天 |
| 实现 `/users:resolve` 端点 | 后端 | byIds + list 双模式 | 半天 |
| 提供 fieldMapping yaml | 后端梳理 | 字段命名对照 | 半天 |
| 放置 iframe 标签 + 实现换票 | 前/后端 | iframe 标签 + 换票调用 + 一次性 code 传前端 | 2 小时 |
| 配置 actionUrls 模板 | 后端 | URL 模板 | 10 分钟 |
| (可选)实现 push 接收 + HMAC | 后端 | webhook 接收 + 验签 | 1 天 |
| **合计** | | | **3-5 天** |

对比 v1(推模式十几个节点 + 联调 1-2 周),v2 拉模式将宿主对接成本压缩到 **3-5 天**。

详细 checklist 见 [host-integration.md §10.4](host-integration.md#104-验证-checklist给-host-自查)。

### 11.3 故障降级与 SLA

- **iframe 加载失败:** 提供友好降级页面,不能让宿主页面出现空白或 500 错误
- **PAC 后端 SLA:** ≥ 99.5% / month
- **同步延迟超阈值:** 工作台顶部展示"数据可能不是最新"告示

---

## 十二、数据合规与隐私保护

> 跟 v1 完全一致,医疗数据高敏区,本章核心点不变。

- **患者数据合规边界:** PIPL / GDPR 等法规要求评估,明确数据处理的法律依据
- **PIPL 删除权处理:** PAC 走"匿名化"(name=null, phone=null, externalId 保留)— **不物理删除**,审计链完整
- **数据保留期限:** `agent_invocations.input_snapshot`(可能含 EMR 临时借用片段)需要保留期清理策略 — 成功调用 N 天后清,失败的保留更久供 debug
- **最小必要原则:** 服务账号只拉召回业务必要字段
- **跨地域合规:** 如 PAC 与宿主跨地域部署,评估数据跨境传输合规

---

## 十三、分阶段实施路径(v2 对齐 [W1 报告](w1-report.md))

### W1 ✅ 框架定稿 + 评审 closure

- ✅ PAC 三层业务模型确立(详见 [three-layer-model.md](three-layer-model.md))
- ✅ 数据库结构 60+ 评审决议项 closure(详见 [db-design-v2.md](db-design-v2.md))
- ✅ PAC 自身业务边界明确
- ✅ 工程骨架搭建 + 本地服务跑通

### W2 🟡 数据骨架 + Mock + 文档定型

- 🎯 [`host-integration.md`](host-integration.md) v0 发出 → DW / 5i5ya 团队评估
- 🎯 Mock 数据集启动(脱敏样本 + 兜底典型病例)
- 🎯 [`scenarios/post_treatment_recall.md`](scenarios/post_treatment_recall.md) 业务方评审
- 🎯 packages/types 重写完成(已经 W1 末完成)
- 🎯 业务方拍板召回算法策略(🔴 P0 风险)

### W3 🎯 数据摄入跑通 + 患者画像

- 数据摄入跑通(Mock 数据 → patient_transactions → parser → patient_facts)
- 患者画像出来 — 4 个起步特征(value / treatment_chain_status / recall_risk / do_not_contact_status)
- 画像页面 v1(100+ mock 患者画像卡片)

### W4 ⭐ 召回名单 + AI 话术 + 摘要(评估节点 1)

- 召回名单生成 + 优先级排序解释
- 异步 PlanScript / PlanSummary 生成(BullMQ + AiCall 框架 → DeepSeek)
- Mock 召回页面 v2(完整业务流可点击)
- **管委会评估节点 1**

### W5 🎯 客服工作台扩展 + 数据源真实接入启动

- 客服工作台 v3(完整工作台)
- 第一个真实 platform 接入(可能是 mock 升级为真实 host)
- 数据源真实接入启动(DW 排期对接)

### W6 🎯 演示版 + 1 家试点最小数据接入

- 演示版(完整业务流 + 真实数据)
- 1 家试点门诊最小数据接入

### W7+ ⏳ 试点跑通(需 DW + 客服培训配合)

- 全 5 家试点门诊接入
- 客服培训实施
- 实测 KPI 收集 → v1.x 微调权重

### Phase 4 — 能力增强(W6+ 按需迭代)

- ⬜ 漏诊召回 scenario(选项 B-2,接朝厚影像 AI)
- ⬜ npm 前端组件包(更深度 UI 集成)
- ⬜ 后端薄 SDK(接入方 > 3 时)
- ⬜ Webhook 事件推送 + 死信队列
- ⬜ 数据查询类 API(报表 / Dashboard)
- ⬜ 管理控制台(可视化规则编辑器、同步监控、租户管理)

---

## 附录:整体架构拓扑图(v2)

```
宿主系统(Friday / 5i5ya / 400 / DW)
├── 前端
│   └── iframe 嵌入 PAC 前端(URL 带一次性 code)
│   └── 或 host 自有前端直调 PAC 开放 API
│
├── 后端
│   ├── 提供 apiKey(PAC 调宿主资源 API 鉴权) ────────────┐
│   ├── 实现 11 种资源 endpoint(增量游标 + envelope)    │
│   ├── 实现 /users:resolve(PAC 分配客服查用户)         │
│   ├── 调 /pac/v1/auth/token 换票                      │
│   └── (可选)实现 push 接收 + HMAC 验签                │
│                                                       │
│         ↕ accessToken                                  │
│                                                       │
PAC(独立部署)                                            │
├── Token Exchange 鉴权                                  │
├── Sync Engine(11 种资源拉取 + 单患者按需 + reconcile) ◀┘
│   └── adapter(fieldMapping 翻译)+ parser(canonical 化)
├── patient_transactions(append-only 账本)
├── patient_facts(canonical 版本流)
├── Persona Engine(eventWatermark 水位 + 4-13 features)
├── PlanEngine(scenario 应用层常量 + selector + 优先级算法)
├── followup_plans / plan_scripts / plan_summaries
├── plan_executions(接口 2 客服回写 → Plan 状态机)
├── 多租户隔离(硬隔离 platform+tenant / 立柱业务字段 clinic_id)
│         ↓
├── AiCall 框架 → DeepSeek V4 Pro(scripts/summaries 异步 BullMQ)
└── 对外 API(/pac/v1/*)+ Swagger / OpenAPI / Redoc 文档
```

详细分层视图见 [three-layer-model.md](three-layer-model.md)。
