# Recall Service 技术架构设计方案

> **文档性质：** 技术评审文档，面向开发团队，用于方案评审与落地实施。
> 
> **核心定位：** 将客户召回模块从 Friday SaaS 中抽离为独立的**可复用领域组件**，支持 Friday、5i5y 及未来更多系统以最低对接成本接入。

---

### 设计初衷

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

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

### 架构核心原则

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

---

### 召回服务的数据边界

**召回服务自己拥有和管理的数据：**

|   |   |
|---|---|
|数据|说明|
|召回池|经引擎计算后产生的漏诊线索池|
|线索状态|线索的完整生命周期（待分配/已分配/已回访/已预约/已拒绝/休眠）|
|任务记录|每次回访的执行记录、话术快照|
|租户配置|各宿主的同步引擎配置、召回规则、路由模板、凭证信息|
|AI 调用记录|话术生成的 Token 用量、响应状态、成本统计|

**数据来源于宿主系统、冗余存储在本地的数据：**

|   |   |
|---|---|
|数据|说明|
|患者档案|姓名、年龄、联系方式、就诊偏好等|
|诊断记录|召回池计算的核心输入|
|治疗记录|用于扣减已完成的治疗项|
|预约记录|用于扣减已有预约的线索|
|排班信息|话术生成时推荐复诊时间|

**核心原则：数据来源于宿主，但通过同步引擎冗余存储在本地。所有业务计算在本地完成，运行时不依赖宿主的实时接口。**

---

## 目录

- 一、后端部署策略选型
    
- 二、前端嵌入策略选型
    
- 三、鉴权体系设计
    
- 四、数据获取策略选型
    
- 五、宿主动作跳转与状态流转
    
- 六、对外 API 体系
    
- 七、Webhook 事件推送
    
- 八、召回规则引擎配置化
    
- 九、多租户隔离
    
- 十、SDK 与交付物策略
    
- 十一、宿主系统接入全流程
    
- 十二、数据合规与隐私保护
    
- 十三、分阶段实施路径
    

---

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

### 1.1 三种方案对比

|   |   |   |   |
|---|---|---|---|
|维度|方案 A：嵌入 Friday|方案 B：模块化隔离|方案 C：完全独立服务|
|代码位置|直接写在 Friday 业务代码中|代码在 Friday 内，独立 package / 路由 / 表|独立代码仓库、独立部署|
|部署方式|随 Friday 一起部署|随 Friday 一起部署（共享进程）|独立 CI/CD、独立扩缩容|
|5i5y 接入|调 Friday 的常规 API|调 Friday 暴露的 /recall/v1/* 路由|直接调独立的 Recall Service|
|数据隔离|共库共表，耦合风险高|共库独立表（统一前缀），逻辑隔离|独立数据库，物理隔离|
|发版耦合|完全耦合|部分耦合，代码隔离降低影响|完全解耦，独立迭代|
|运维成本|零额外成本|零额外成本|需建立服务监控、网络配置|
|演进成本|将来拆分代价高|拆分成本低，剪切即独立|已是终态|
|适用阶段|仅 Friday 自用|过渡期：2 系统复用|明确目标：一步到位|

### 1.2 决策结论：采用方案 C——完全独立服务

方案 A 和方案 B 本质上都是走向方案 C 的过渡阶段，且它们在实际运行中均无法根本避免以下痛点：

- **上下级依赖：** 导致原本平级的系统产生主从调用关系。
    
- **数据安全与组织协作：** 数据跨系统流转边界模糊，责任与权限难以厘清。
    
- **发版影响：** 召回业务迭代必须与宿主系统发版节奏强绑定，互相牵制。
    

相比之下，方案 C 仅在前期需要一定的部署和运维初始化（如构建 CI/CD、独立数据库等），这些工作目前实施起来相对容易且成本完全可控。因此，本方案以终为始，直接采用方案 C，一步到位实现物理隔离与完全解耦。

### 1.3 关于"过度设计"的坦诚讨论

**召回模块的本质定位——"可嵌入的领域组件"，不是独立系统。** 它没有自己的用户体系、没有自己的患者数据、离开宿主系统什么也干不了。与积分商城、直播教学这类自带用户和数据闭环、可独立商业变现的系统级产品有本质区别。

**业务****耦合****的代价必须正视。** 召回模块与宿主系统业务耦合度较高（患者档案、诊断记录、排班预约、治疗状态等多模块频繁交互）。独立后，原本通过数据库关联或内部函数调用即可完成的需求，将变为跨系统通信，会增加开发复杂度并限制部分业务编排的灵活性。这是复用性与灵活性之间的固有取舍，需在具体需求评审时逐案权衡。

**为什么不走大厂微服务体系？** 大厂微服务能跑起来靠的是一整套基建（服务注册发现、配置中心、链路追踪、统一日志、API 网关、熔断限流、分布式事务），每一项都需要团队维护。我们当前的体量不需要这套重武器。**学大厂的思路，不学大厂的规模**——数据归属清晰（召回自己的表自己管）、关键字段冗余（减少跨系统调用）、通信接口标准化（API 契约定好）。RPC 框架、消息队列这些等真到了规模再上不迟。

**值不值得做，取决于用多大力气做。** 本方案的核心设计目标是**最大程度降低宿主系统的对接成本**，让"给我创建个服务账号、提供几个只读接口地址就行"成为现实。

**总结结论：** 本次设计的核心思路是"借鉴与取舍"。我们借鉴了独立系统的思想，以及大厂微服务解耦、边界清晰、契约先行的优秀实践，并在后续各项设计中根据实际业务体量和场景做了针对性的适应性分析与取舍：我们取舍掉了沉重复杂的底层基建成本；我们追求领域组件独立复用的长远价值，但也必须取舍并坦然接受跨系统通信带来的短期复杂度。一切技术折中的最终标尺，都落在如何以最小代价实现"宿主极简接入"这一核心目标上。

---

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

### 2.1 四种方案全景对比

|   |   |   |   |   |
|---|---|---|---|---|
|维度|iframe|npm 组件包|微前端|独立页面（浏览器 tab）|
|对宿主要求|几乎为零，放一个 iframe 标签|宿主必须是兼容的前端框架（如 React）|宿主必须支持微前端框架|零，打开一个 URL|
|通信方式|后端 API（零 postMessage 方案）|props / 回调函数（原生、自然）|共享状态 / props 注入|URL 传参 + 后端 API|
|数据获取|后端同步引擎冗余存储|宿主传入回调函数|宿主注入数据源|后端对后端|
|样式隔离|天然隔离|需额外处理（CSS Modules / Tailwind prefix）|框架提供沙箱|天然隔离|
|体验|好，但有 iframe 固有限制（弹窗裁切、高度自适应）|最佳，完全无缝|最佳，完全无缝|有页面跳转感|
|宿主前端开发量|放一个 iframe 标签，完事|安装包 + 写几个回调|前端架构改造 + 接入框架|零|
|技术栈绑定|无|绑定（如 React）|部分绑定|无|
|版本更新|召回服务更新即时生效|各宿主单独升级 npm 包|取决于框架加载策略|即时生效|

### 2.2 微前端方案被排除的原因

微前端（qiankun / Module Federation / single-spa）要求宿主系统本身具备微前端架构，需改造入口文件、路由系统、加载机制——这是对宿主前端架构的侵入性改造。如果 Friday 和 5i5y 本身没有微前端体系，为嵌一个召回模块让两边都改造，代价过大。

### 2.3 独立页面（浏览器 tab）方案

在零 postMessage 的架构下，独立页面与 iframe 在技术本质上一致——召回前端只与自己的后端交互，不与宿主前端有任何通信，因此不存在通信通道断裂的问题。独立页面可视为 iframe 的补充形态，适用于宿主系统希望以独立 tab 而非内嵌面板展示召回工作台的场景。两种方式共享同一套后端架构（code 换票、API 鉴权、同步引擎），前端代码也是同一份，只是承载容器不同。

### 2.4 npm 组件包方案的定位

npm 组件包是体验最好的方案——通过 props 和回调函数通信，零 postMessage 开销，宿主开发量最小。但**前期不提供**，原因：

- 需要额外的前端工程投入（组件封装、样式隔离、版本管理）。
    
- 与宿主存在技术栈绑定（React 版本兼容性）和样式冲突风险。
    
- 当前阶段应优先验证后端架构和业务流程的完整性。
    

**作为后期锦上添花的备选方案。** 当 iframe 方案跑通、业务验证完成、且有宿主明确需要更深度 UI 集成时，再投入开发。同时也可弥补不想使用 iframe 默认页面样式的宿主需求——通过组件包自行控制渲染和样式，融入宿主自身的 UI 体系。

### 2.5 决策结论

**iframe 为主，npm 组件包为备（后期按需提供）。**

两种方案的后端交互完全一致，前端嵌入方式的差异不影响后端架构。

### 2.6 iframe 方案详细设计——零 postMessage

**设计目标：iframe 作为纯渲染容器，所有数据交互走后端，前端之间彻底解耦，宿主前端开发量降到接近零。**

```Plain
宿主前端                        召回前端（iframe）
   │                               │
   │  ❌ 零通信                      │
   │  (无 postMessage)              │
   │                               │
   │                               │  只调自己的后端
   │                               ↓
   │                           召回后端
   │                            ↑    │
   ↓                            │    ↓
宿主后端 ── recall_token ───▶  │   AI Gateway → Dify
   │                            │
   │ ◀── 服务账号 token ─────┘
   │         (同步引擎拉取数据)
```

**要点：两个前端之间没有任何通信，所有数据交互收敛在两个后端之间。**

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

---

## 三、鉴权体系设计

### 3.1 双向鉴权全景

召回服务与宿主系统之间存在双向调用，因此鉴权也是双向的：

|   |   |   |   |
|---|---|---|---|
|方向|场景|鉴权方式|详见|
|宿主 → 召回|换票、iframe 加载、调用召回 API|appId + appSecret 换取 recall_token|本章 3.2 - 3.6|
|召回 → 宿主|同步引擎拉取患者/诊断/预约等数据|宿主提供的服务账号走现有登录接口|第四章 4.3|

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

### 3.2 核心原则

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

### 3.3 Token Exchange 换票流程

宿主后端用 `appId + appSecret` + 用户信息，调用 `POST /recall/v1/auth/token` 换取短时效 JWT。`appSecret` 仅在这一次后端对后端的请求中出现，换完票后永远不再传输。recall_token 由召回服务自己的密钥签发和验签，不依赖任何外部系统，鉴权完全自包含。

```Plain
宿主后端                              召回后端
   │                                    │
   │  POST /recall/v1/auth/token        │
   │  { appId, appSecret, user }        │
   │ ──────────────────▶    │
   │                                    │  验证 appId + appSecret
   │                                    │  用自己的密钥签发 JWT
   │  ◀─────────────────    │
   │  { recall_token, expiresIn }       │
   │                                    │
   │  后续所有请求带 recall_token        │
   │  Authorization: Bearer <token>     │
   │ ──────────────────▶    │
   │                                    │  用自己的密钥验签
```

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

换票时要求必传用户信息：`userId`、`recallRole`、`clinicIds`（该用户有权限管辖的诊所 ID 列表）。recall_token 中包含租户身份 + 用户身份 + 诊所权限范围。召回服务在数据查询和线索分配时，不仅基于 `tenant_id`，还强制基于 `clinic_id` 做行级过滤，防止 A 诊所的客服看到 B 诊所的线索。

好处是：

- 召回服务可以做精细的用户级权限控制（客服只看自己的线索，主管看全部）。
    
- 所有操作都能追溯到具体的人，审计日志完整。
    
- 不需要在认证中间件里区分两种 token 类型，逻辑更简单。
    

宿主系统在调换票接口时一定处于某个用户的登录态下（要么是客服打开了召回工作台，要么是后端代某个用户执行操作），用户信息随手就能传，没有额外成本。

用户角色使用召回服务预设的角色标识（`staff` / `leader` / `admin`），由宿主在换票时将自身角色体系映射为召回角色。召回服务内部将角色展开为细粒度权限列表写入 token，前端根据权限控制 UI 渲染，后端在 API 层做权限校验，前后端双重控制。将来如有宿主需要自定义权限组合，换票接口支持直接传权限列表覆盖角色预设，向后兼容。

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

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

iframe 场景下不能通过 URL 直接传 recall_token（会留在浏览器历史和日志中）。采用**一次性 code 中转**：

**code 生成机制：** 换票接口同时返回 recall_token 和一次性 code。召回后端在签发 token 的同时生成随机字符串作为 code（如 UUID），以 code 为 key、recall_token 为 value 存入 Redis（Redis 需 HA 部署保障可用性），设置 60 秒过期。宿主后端自己留着 recall_token 供后端 API 调用，把 code 传给前端拼 iframe src。

```Plain
宿主后端换票获得 recall_token + code
    ↓
将 code 返回给宿主前端
    ↓
宿主前端设置 iframe src:
  https://recall.xxx.com/workbench?code=abc123&tenantId=friday_001
    ↓
召回前端加载后，拿 code 调自己后端:
  POST /recall/v1/auth/exchange-code { code: "abc123" }
    ↓
召回后端验证 code（一次即作废），返回 recall_token
    ↓
召回前端持 token 正常工作
```

code 用一次即作废，无法重放，安全性有保障。appSecret 保护了 token 的签发，code 保护了 token 的传递，各管一段。

> **浏览器兼容性要求：** 召回前端在 iframe 内拿到 recall_token 后，必须将 token 存储在内存中（JavaScript 变量），每次请求通过 `Authorization: Bearer` Header 显式传递，**绝不依赖 Cookie 维持登录态**。这可以彻底规避 Safari ITP、Chrome 第三方 Cookie 限制等跨域安全策略。页面刷新后 token 丢失，重新走一次性 code 流程即可。

### 3.6 Token 刷新

recall_token 建议设 2 小时时效，引入 refresh token 机制。

- **iframe 场景：** 召回前端检测 token 临近过期时，使用 refresh token 向召回后端请求刷新。如刷新失败，触发 iframe 重新加载（宿主后端重新换票生成新 code）。
    
- **API 场景：** 宿主后端自行管理刷新，token 过期前重新调用换票接口。
    

---

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

> 这是整个架构中讨论最充分的问题——"召回服务自己不持有患者数据，需要用的时候怎么拿？"

### 4.1 两种方案对比

|   |   |   |   |
|---|---|---|---|
|方案|数据流向|宿主改动量|实时性|
|A: 宿主后端主动推送|宿主后端在业务节点加推送调用|侵入宿主业务代码的十几个节点|实时|
|B: 召回同步引擎主动拉取（推荐）|召回同步引擎定时调宿主只读接口|零代码改动（复用现有接口）|准实时（分钟级）|

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

- 需要在宿主代码的十几个业务节点（写病历、完成治疗、创建预约、取消预约、改手机号、影像报告……）逐一加推送调用。
    
- 漏推某个节点就导致数据不一致，排查困难。
    
- 宿主开发团队抵触："我改自己的业务逻辑还要想着召回服务的需求"。
    

### 4.3 方案 B 详细设计——召回同步引擎主动拉取（推荐）

#### 核心思路

召回服务内部运行一个**同步引擎**，定时从宿主系统拉取增量数据，存入自己的本地库。所有召回业务计算（召回池运算、线索管理、话术生成）都在本地数据上完成，不依赖宿主的实时接口。

```Plain
┌─────────────────────────────────────────────┐
│              Recall Service                  │
│                                              │
│   Sync Engine (定时增量拉取)                   │
│   ┌──────────────────────────┐               │
│   │ 拉取患者数据   → 本地患者表  │               │
│   │ 拉取诊断记录   → 本地诊断表  │               │
│   │ 拉取治疗记录   → 本地治疗表  │               │
│   │ 拉取预约记录   → 本地预约表  │               │
│   └──────────┬───────────────┘               │
│              ↓                               │
│   召回引擎运算（纯本地计算）                     │
│   诊断去重 → 扣减已治疗 → 扣减已预约 → 召回池     │
│              ↓                               │
│   线索管理 → AI话术生成 → 工作台展示              │
└─────────────────────────────────────────────┘
```

#### 宿主需要提供的接口

宿主只需开放几个**带时间过滤的只读列表接口**，大部分系统已有现成的：

```Plain
GET /api/patients?updatedAfter=2026-04-27T10:00:00Z
GET /api/diagnoses?updatedAfter=2026-04-27T10:00:00Z
GET /api/treatments?updatedAfter=2026-04-27T10:00:00Z
GET /api/appointments?updatedAfter=2026-04-27T10:00:00Z
```

#### 宿主接口鉴权——服务账号登录

在宿主系统后台创建一个专用的服务账号（如 `recall-bot`），分配只读角色权限。召回服务的同步引擎使用这个账号走宿主**现有的登录接口**获取 token，后续用该 token 调宿主的常规 API。**宿主零代码改动。**

**曾考虑的其他鉴权方案：**

|   |   |   |
|---|---|---|
|方案|宿主改动|说明|
|宿主签发专用 API Key（推荐升级方案）|需新增 Key 签发和校验逻辑|更稳定的 B2B 通信方式，作为将来升级方向|
|OAuth Client Credentials|需新增 OAuth 端点|增加宿主开发量且复杂|
|服务账号登录（当前采用）|零代码改动|对接成本最低的兜底方案|

**宿主的全部工作量：** ① 创建 recall-bot 账号 ② 分配只读角色 ③ 把账号密码给召回服务。

#### 同步引擎租户配置

```JSON
{
  "tenantId": "friday_001",
  "sync": {
    "auth": {
      "loginUrl": "https://friday.xxx.com/api/auth/login",
      "username": "recall-bot",
      "password": "encrypted_xxx"
    },
    "endpoints": {
      "patients": "https://friday.xxx.com/api/patients",
      "diagnoses": "https://friday.xxx.com/api/diagnoses",
      "treatments": "https://friday.xxx.com/api/treatments",
      "appointments": "https://friday.xxx.com/api/appointments"
    },
    "fieldMapping": {
      "patients": {
        "patientId": "id",
        "patientName": "name",
        "phone": "mobile"
      }
    },
    "intervalMinutes": 5
  }
}
```

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

- **增量拉取：** 每次同步记录上一次拉取时间戳，下次只拉 `updatedAfter` 之后的变更数据。增量游标使用 `(update_time, id)` 复合游标，写入采用幂等 upsert，避免边界丢失或重复。以宿主时间为准。
    
- **频率可配置：** 按租户配置——实时性要求高的 5 分钟一次，要求不高的 30 分钟一次。
    
- **字段映射：** 各宿主系统返回格式不同，通过 adapter 类做字段适配，每个宿主一个 adapter（包含字段拆分、计算、字典翻译、嵌套结构提取等）。纯配置的 `fieldMapping` 只覆盖最简单的 1:1 映射场景，复杂转换由 adapter 代码处理，预留扩展点。
    
- **token 缓存：** 登录获取的宿主 token 缓存复用，过期后自动重新登录。
    
- **故障告警：** 登录失败连续 N 次触发告警通知运维，不静默失败。
    
- **密码管理：** 宿主的服务账号密码在召回服务侧加密存储（密钥通过 KMS / 配置中心 / 环境变量管理），配置变更有审计日志，需明确密钥轮换流程。
    
- **权限最小化：** 服务账号必须专用、只读，禁止使用管理员账号。只拉召回业务必要的字段，不做全表拉取。
    

#### 对接契约约束

- 宿主关键表（尤其是预约、治疗记录）必须采用**逻辑删除**并更新 `update_time`，或额外提供已删除记录查询接口，防止同步引擎因物理删除产生"幻读"。对于无法做逻辑删除的宿主，启用**每日全量主键对账机制**（拉主键集合做 diff），T+1 内识别物理删除。
    
- 数据接口必须支持**游标分页**（`cursor + limit`），同步引擎具备自动翻页拉取能力，防止全量初始化或突发大批量数据时 OOM 或超时。
    
- 宿主侧需为 recall-bot 服务账号配置 **API 限流白名单**，或同步引擎自行控制请求间隔，避免触发宿主网关限流。
    

> **风险提示：** 服务账号模拟登录方式存在脆弱性——若宿主升级登录安全策略（如强制验证码、MFA、定期改密码），同步引擎将瘫痪。预防措施：① 对接时明确要求宿主将 recall-bot 账号标记为"服务账号"，豁免交互式安全策略；② 同步引擎登录失败时立即告警，不静默失败。若宿主安全体系不支持豁免，则升级为宿主提供长效 API Key 方案（需宿主少量开发）。

#### 将来升级路径

如宿主安全要求提升，可将服务账号登录升级为 OAuth Client Credentials 或专用 API Key。同步引擎只需更换 Auth 模块，数据拉取逻辑不变。

### 4.4 数据实时性评估

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

**陈旧性容忍度划分：**

- **容忍分钟级延迟的场景：** 线索池计算、线索列表展示、统计报表。
    
- **需要实时拉取的场景：** 拨号前的患者状态确认、生成话术时的最新档案、分配前的线索查重。
    

### 4.5 按需同步机制

除定时增量拉取外，召回服务在自身关键业务节点主动触发针对性的即时同步，确保数据新鲜。例如客服点击"执行回访"时，召回后端自动拉取该患者的最新档案和预约状态，刷新本地数据后再生成话术。这个过程由召回服务内部驱动，宿主无感知、零改动。

作为补充，也支持宿主在关键业务节点主动通知召回服务来拉取：

```Plain
POST /recall/v1/sync/trigger
{ tenantId: "friday_001", scope: "appointments" }
```

但这不是必选项——召回服务已具备自触发能力，宿主只在确有需要时才加。

### 4.6 历史数据冷启动

首次接入时，同步引擎将 `updatedAfter` 设为空或极早时间，自动分页拉取全量历史数据完成初始化。大部分诊所的数据量（万级）在数小时内可完成。

**冷启动注意事项：**

- 全量拉取期间暂停增量同步，避免游标混乱。
    
- 使用游标分页（避免 deep offset 性能问题）。
    
- 实施限流退避策略，防止压垮宿主接口。
    
- 支持断点续传机制，中途失败可从上次位置恢复。
    

若数据量特别大或宿主要求快速上线，后续可提供批量导入接口作为补充通道。

---

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

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

宿主在接入时通过租户配置提供自己的路由模板：

```JSON
{
  "tenantId": "friday_001",
  "actionUrls": {
    "CREATE_APPOINTMENT": "https://friday.xxx.com/appointment/new?patientId={patientId}&doctorId={doctorId}",
    "VIEW_PATIENT": "https://friday.xxx.com/patient/{patientId}/profile",
    "VIEW_XRAY": null
  }
}
```

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

### 5.2 状态自动流转

线索状态的更新不依赖宿主主动回写。同步引擎定时拉取预约和治疗数据后，召回引擎重新执行扣减计算——患者新增了预约，对应线索自动标记为"已预约"；患者完成了治疗，对应线索自动标记为"已完成"。整个状态流转由召回服务内部闭环，宿主零感知、零开发量。

> **状态设计注意：** 考虑将线索状态从单状态改为多标签模式——"已分配 + 已联系 + 已预约"可以共存。同时明确客服的"跟进中"标记会暂时屏蔽自动流转，等客服结束跟进后再做扣减，避免客服正在沟通时线索状态突然被自动改变。

---

## 六、对外 API 体系

### 6.1 API 分类

**业务操作类** **`P0`****：** 召回前端（或宿主系统自写前端）调用——获取线索池列表、分配/回收线索、触发话术生成、更新任务状态、获取租户路由模板配置。

话术生成接口采用 **SSE（Server-Sent Events）流式输出**，既避免长连接超时问题，又让客服看到"逐字生成"的打字机效果，缓解等待焦虑。如客户端不支持 SSE，降级为异步任务模式（发起生成 → 返回任务 ID → 前端轮询状态）。

> iframe 跨域场景下 EventSource 需配置 CORS（含 credentials），具体配置随宿主域名动态注入。

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

**数据查询类** **`P2`****：** 召回转化率、客服绩效、线索漏斗、AI 调用成本等统计数据。供宿主在自己的管理后台展示。

### 6.2 鉴权统一

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

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

按**用户 / 诊所 / 租户**三个维度设限，防止恶意刷新话术导致 Token 费用失控。

---

## 七、Webhook 事件推送（召回 → 宿主）`将来 P2`

### 7.1 适用场景

当召回服务内部发生关键事件时（新线索产生、线索回收、任务状态变更、话术生成完成、AI 服务降级），主动推送通知给宿主系统。

### 7.2 触发实施的信号

- 宿主对实时性有明确要求（如新线索产生需即时通知客服）。
    
- 接入方超过两个，轮询 API 产生性能压力。
    
- 当前阶段宿主可通过轮询数据查询类 API 替代。
    

### 7.3 设计要点（预留）

|   |   |
|---|---|
|要素|设计|
|事件类型|lead.created / lead.recycled / task.status_changed / task.script_ready / system.ai_degraded|
|注册方式|宿主提供 callbackUrl + 签名密钥 + 订阅事件列表|
|可靠性|幂等 eventId、SHA256 签名、指数退避重试（5s → 30s → 2min → 10min，最多 5 次）|
|死信队列|重试 5 次仍失败的关键事件入库，提供 API 手动补偿|

Webhook 鉴权采用签名验证机制（HMAC-SHA256），不走宿主的用户登录体系。宿主注册时提供签名密钥，召回服务每次推送时签 `timestamp + payload`，宿主验签确认来源可信，接收方拒绝超过 5 分钟的请求（防重放）。这是 Webhook 的业界标准做法（GitHub、Stripe 等均采用此方式）。

---

## 八、召回规则引擎配置化

### 8.1 背景

如果 Friday 和 5i5y 对"什么算漏诊"的定义不同（如 Friday 认为 6 个月未复诊算漏诊，5i5y 认为 3 个月），硬编码需要写 if-else 或 fork 代码。

### 8.2 设计

将召回规则做成按租户可配置：

```JSON
{
  "tenantId": "friday_001",
  "rules": {
    "missedThresholdDays": 180,
    "excludeCompletedWithinDays": 30,
    "priorityWeights": {
      "implant": 100,
      "rootCanal": 80,
      "filling": 40
    },
    "recycleTimeoutHours": 72,
    "maxContactAttempts": 3
  }
}
```

|   |   |
|---|---|
|配置项|含义|
|missedThresholdDays|多久未复诊算漏诊|
|excludeCompletedWithinDays|近 N 天完成的治疗不召回|
|priorityWeights|不同漏诊类型的优先级权重|
|recycleTimeoutHours|线索超时回收时间|
|maxContactAttempts|最大联系次数（防打扰熔断）|

不同宿主接入时不改代码，调配置即可。将来可做成管理后台的可视化规则编辑器。

> 对于复杂组合规则（如不同治疗类型的差异化阈值），规则引擎预留**表达式 DSL 接口**，避免接口签名被数值锁死。

---

## 九、多租户隔离 `P0`

每个接入系统作为一个租户（通过 `appId` 标识），租户下按诊所（`clinic_id`）做二级隔离。前期采用共享数据库 + `tenant_id` 强制过滤（通过 ORM 拦截器 / SQL 审核保证，所有 SQL 必须携带 `tenant_id`），预留按租户分库的扩展能力。

- **数据隔离：** 所有表统一携带 `tenant_id` + `clinic_id` 双字段，查询层强制追加双重过滤条件。租户之间数据绝不串访，同一租户下不同诊所的数据也互相隔离。
    
- **单诊所场景兼容：** 如果某个租户只有一个诊所（或不区分诊所概念），接入时统一分配一个默认的 `clinic_id`（如 `default`），对该租户透明——换票时不传 `clinicIds`，系统自动绑定默认值，业务逻辑和多诊所完全一致，无需特殊分支处理。
    
- **同步引擎隔离：** 各租户独立的同步配置、拉取周期、字段映射、服务账号。
    
- **规则隔离：** 各租户独立的召回规则配置。规则可进一步按诊所维度差异化配置（如 A 诊所漏诊阈值 90 天，B 诊所 180 天），也可全租户统一。
    
- **AI 用量隔离：** Token 消耗量绑定租户 + 诊所维度，为将来差异化计费打基础。
    

---

## 十、SDK 与交付物策略

### 10.1 交付物清单

|   |   |   |
|---|---|---|
|交付物|形式|当前阶段|
|召回服务后端|独立部署的 Java 服务|✅ Phase 1|
|召回服务前端|独立部署的 Web 应用（供 iframe 加载）|✅ Phase 1|
|API 文档 + 各语言调用示例|curl / Java HttpClient|✅ Phase 1|
|npm 前端组件包|@your-org/recall-ui（React）|⬜ 后期按需|
|后端薄 SDK|各语言的 API Client + 自动换票刷新|⬜ 接入方 > 3 个时评估|

### 10.2 后端 SDK 不急着做的原因

- 后端 SDK 需分语言维护（当前宿主有 Java），同一套逻辑写两份。
    
- 换票逻辑就一个 POST 请求，各语言自行封装几十行代码的事。
    
- 等接入方数量上来，从文档里沉淀出 SDK 是自然的过程。
    

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

---

## 十一、宿主系统接入全流程

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

```Plain
召回服务 → 宿主：
  ① appId + appSecret（宿主调召回 API 换票用）
  ② API 文档 + 调用示例
  ③ iframe 嵌入地址

宿主 → 召回服务：
  ④ 服务账号用户名密码（召回同步引擎登录用）
  ⑤ 数据接口地址（patients / diagnoses / treatments / appointments）
  ⑥ 字段映射关系（如返回格式与标准契约不一致）
  ⑦ 动作跳转路由模板（如新建预约的 URL 模板）
  ⑧ 召回规则配置（漏诊阈值、回收超时等，可后续调整）
```

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

|   |   |   |   |
|---|---|---|---|
|工作项|前端/后端|工作内容|预估工时|
|创建 recall-bot 服务账号 + 分配只读权限|后台配置|操作宿主系统后台|10 分钟|
|确认数据接口地址 + 字段映射|后端|梳理现有 API，提供地址和字段对照|半天|
|放置 iframe 标签|前端|在宿主页面中加一个 iframe|10 分钟|
|实现 Token Exchange 换票调用|后端|调召回服务换票接口 + 生成一次性 code 传给前端|2 小时|
|配置动作跳转路由模板|后端|提供 URL 模板|10 分钟|
|合计|||约 1 天|

对比推模式（宿主需在十几个业务节点加推送调用、联调 1-2 周），拉模式将宿主对接成本压缩到了 **1 天**。宿主无需侵入自身业务代码，所有工作均为配置操作或一次性对接。

### 11.3 故障降级与 SLA

- **iframe 加载失败：** 提供友好降级页面（显示"服务暂时不可用，请稍后重试"），不能让宿主页面出现空白或 500 错误。
    
- **召回后端 SLA 承诺：** 需明确可用性目标（如 99.9%），定义故障响应与恢复时间。
    
- **同步延迟超阈值：** 工作台顶部展示"数据可能不是最新"告示，提醒客服注意。
    

---

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

> 医疗数据是合规高敏区，本章缺失对评审通过是大风险点。

- **患者数据冗余存储的合规边界：** 需评估冗余存储患者个人信息是否符合 PIPL（个人信息保护法）等法规要求，明确数据处理的法律依据。
    
- **患者行使删除权时的级联机制：** 宿主侧删除患者数据后，需通过同步引擎识别删除事件，触发召回服务侧的级联清除。
    
- **数据保留期限：** 明确召回服务本地冗余数据的保留策略，过期数据定期清理。
    
- **最小必要原则：** 服务账号只拉取召回业务必要的字段，不做全表全字段拉取。
    
- **跨地域部署的合规约束：** 如宿主与召回服务部署在不同地域，需评估数据跨境传输的合规要求。
    

---

## 十三、分阶段实施路径

### Phase 1 — Recall Service 核心搭建（6 周）

**Phase 1a（3 周）：骨架 + 鉴权 + 同步引擎 + CI/CD**

- ✅ 独立服务后端搭建（独立仓库、CI/CD、数据库）
    
- ✅ Token Exchange 鉴权体系（含一次性 code 机制）
    
- ✅ 同步引擎 + 服务账号登录机制
    
- ✅ 多租户隔离（tenant_id + clinic_id 全表覆盖）
    

**Phase 1b（3 周）：召回引擎 + AI 集成 + 前端 + 文档**

- ✅ 召回池运算引擎（本地数据计算）
    
- ✅ 线索管理核心 API（分配/回收/状态流转）
    
- ✅ AI 话术生成（对接 AI Gateway，SSE 流式输出）
    
- ✅ 租户路由模板配置（动作跳转）
    
- ✅ 召回前端开发（供 iframe 加载）
    
- ✅ API 文档 + 各语言调用示例
    

### Phase 2 — Friday 接入验证（3 周）

- ✅ Friday 作为第一个宿主接入（验证全流程）
    
- ✅ Friday 创建服务账号 + 配置同步引擎
    
- ✅ Friday 前端嵌入 iframe
    
- ✅ Friday 后端实现换票
    
- ✅ 召回规则配置化
    

**Friday 存量切换方案：**

- 明确新旧系统并行还是直接切换策略。
    
- 规划历史数据迁移路径（线索池、任务记录、历史话术）。
    
- 制定灰度策略（按诊所 / 按客服灰度上线）。
    
- 准备回滚预案。
    

### Phase 3 — 5i5y 接入（1-2 周）

- ✅ 5i5y 创建服务账号 + 配置同步引擎（字段映射）
    
- ✅ 5i5y 前端嵌入 iframe
    
- ✅ 5i5y 后端实现换票
    
- ✅ 验证多租户数据隔离
    
- ✅ 按 5i5y 业务差异调整召回规则配置
    

### Phase 4 — 能力增强（按需迭代）

- ⬜ npm 前端组件包（有宿主需要更深度 UI 集成时）
    
- ⬜ 后端薄 SDK（接入方 > 3 个时）
    
- ⬜ Webhook 事件推送 + 死信队列
    
- ⬜ 数据查询类 API（报表 / Dashboard）
    
- ⬜ 管理控制台（可视化规则编辑器、同步监控、租户管理）
    

---

## 附录：整体架构拓扑图

```Plain
宿主系统（Friday / 5i5y / 其他）
├── 前端
│   └── iframe 嵌入召回前端页面（URL 带一次性 code）
│
├── 后端
│   ├── 提供服务账号（recall-bot 只读）─────────────────────┐
│   ├── 提供数据接口（现有 API，零改动）                      │
│   ├── 调 /recall/v1/auth/token 换票                      │
│   └── 生成一次性 code 传给前端                             │
│                                                          │
│         ↕ recall_token                                    │
│                                                          │
Recall Service（独立部署）                                    │
├── Token Exchange 鉴权                                     │
├── 同步引擎（定时增量拉取宿主数据） ◀──── 服务账号 token ─────┘
│   └── 字段映射 / Adapter 配置化
├── 召回引擎（规则配置化，纯本地计算）
├── 线索管理（分配/回收/熔断/生命周期）
├── 租户路由模板（动作跳转配置）
├── 多租户隔离（tenant_id + clinic_id）
│         ↓
├── AI Gateway → Dify（话术生成，SSE 流式输出）
└── 对外 API（/recall/v1/*）
```