# PAC 宿主对接 — ClickHouse 视图通道

> **一句话**:宿主在自己的 ClickHouse 数仓里建一组只读视图(每类业务一张),PAC 直接 SQL 拉,**存量 + 增量同一通道**,宿主不用写任何 REST 接口。
>
> **适用对象**:数据已经在 CH 数仓里(典型 = 集团 DW 已经 ETL 过来),不想为 PAC 单独建一套接入 endpoint 的宿主。
>
> **状态**:作为 [`host-integration-v2.md`](host-integration-v2.md) §2 三种通道(Cold-import / Pull / Push)之外的**第 4 种数据通道**;两者契约和数据字典完全互通,切换零返工。

---

## 1. 跟现有通道的关系

| 通道 | 数据形态 | 增量机制 | 宿主侧工作 |
|---|---|---|---|
| Cold-import | CSV/JSON dump | ❌ 一次性 | `SELECT *` 导出 |
| Pull (REST) | HTTP JSON | 游标查询参数 | 实现 14 个 REST endpoint |
| Push | HTTP JSON | 实时事件 | 主动推 + HMAC |
| **CH 视图(本文档)** | **CH 行** | **视图内单调列 + SQL `WHERE`** | **建视图 + 给只读账号** |

**核心理念**:CH 视图 = REST Pull 的 SQL 变体——把 HTTP 协议层换成 ClickHouse Native / HTTP 协议,游标语义、字段字典、parser、sync_logs、七层防线**全部复用**。

**存量 + 增量同一条**:首次跑 `WHERE updated_at > 0` 拿全量,之后每次跑 `WHERE updated_at > <上次游标>` 拿增量,**不用再做 cold-import 一次性 dump**。

---

## 2. 宿主要做什么(4 步,工作量约 1-2 天)

### 2.1 建视图(每类业务一张,共 7 张必填 + 7 张可选)

视图名建议 `dw.v_pac_<resource>`,字段命名 / 嵌套 / 枚举编码**宿主自由**——PAC 侧 yaml 映射,跟 pull / cold-import 共用同一份[数据字典](host-data-request-checklist-v2.md#3-数据字典请配一份)。

| 视图名(建议)| 必/选 | 对应业务 |
|---|---|---|
| `dw.v_pac_patients` | 🔴 必 | 患者主档 |
| `dw.v_pac_appointments` | 🔴 必 | 预约 |
| `dw.v_pac_clinical_encounters` | 🔴 必 | 接诊 |
| `dw.v_pac_emr_documents` | 🔴 必 | 电子病历 ⭐ |
| `dw.v_pac_image_studies` | 🔴 必 | 影像 metadata ⭐ |
| `dw.v_pac_orders` | 🔴 必 | 收费单 / 医嘱 |
| `dw.v_pac_payments` | 🔴 必 | 收款 |
| `dw.v_pac_recharges` | 🟠 强烈推荐 | 充值 / 储值 |
| `dw.v_pac_complaints` | 🟠 强烈推荐 | 投诉 |
| `dw.v_pac_referrals` | 🟠 强烈推荐 | 转介绍 |
| `dw.v_pac_refunds` / `consultations` / `visit_registrations` / `visit_receptions` | 🟡 可选 | — |

### 2.2 每张视图必须满足的 4 条约束

```sql
-- 示例:dw.v_pac_clinical_encounters
CREATE VIEW dw.v_pac_clinical_encounters AS
SELECT
  enc_id,                          -- ① 业务主键(PAC 用作 subject_id 幂等)
  clinic_id,                       -- ② 诊所 id(PAC 隔离基线)
  patient_id,                      -- ③ 患者外键(回链 PAC patients.externalId)
  ...所有业务字段原样...,
  etl_updated_at                   -- ④ 单调递增列(PAC 游标基准)
FROM dw.dwd_encounter_main
WHERE clinic_id IN ('clinic_hz_dasha', 'clinic_hz_gaode', ...);  -- 试点 5 家
```

| # | 约束 | 缺失后果 |
|---|---|---|
| ① | **业务主键列**(`*_id`)| PAC 没法做幂等,重复入库 |
| ② | **clinic_id 列**(且视图已按试点诊所过滤)| 多租户隔离失守 |
| ③ | **patient 外键**(关联回主档)| 召回算法挂不上患者 |
| ④ | **单调递增列**(`etl_updated_at` / `_version` / 时间戳之类,**必须随每次行变更递增**)| 没法做增量游标,只能全表扫 |

**关于 1:N 子表**(如接诊主表 + 诊断 / 治疗子表)——两种处理都行:
- **方案 A(推荐)**:在视图里用 CH 的 `groupArray` / `arrayMap` 聚成嵌套 JSON 字段,一行 = 完整业务对象
- **方案 B**:子表也建独立视图,PAC 侧 join

### 2.3 开只读账号 + 网络打通

```sql
CREATE USER pac_reader IDENTIFIED BY '<32位强随机>';
GRANT SELECT ON dw.v_pac_* TO pac_reader;
-- 不需要任何 INSERT / ALTER / 表级权限
```

**网络可达性**(三选一,按宿主合规要求):
- VPN / 内网专线(推荐)
- CH HTTPS 端口 + IP 白名单(把 PAC 出口 IP 报给宿主)
- 跳板 / 反代

⚠️ **公网直连 CH Native 协议(9000)合规雷区**——必走加密通道。

### 2.4 提供数据字典

跟 pull / cold-import 共用同一份,见 [`host-data-request-checklist-v2.md`](host-data-request-checklist-v2.md) §3。
**枚举值含义必须列全**——`status='02'` 是"完成"还是"作废",字典没写 PAC 只能猜。

---

## 3. PAC 这边怎么消化(宿主只需了解,不用做)

### 3.1 增量游标轮询

PAC 同步引擎按 platform 配置的间隔(默认 5 分钟)轮询每个视图:

```sql
-- 每个 (platform, resource) 组合一条 cursor,落 sync_logs.cursor_after
SELECT *
FROM dw.v_pac_clinical_encounters
WHERE etl_updated_at > {cursor:DateTime64(3)}
ORDER BY etl_updated_at, enc_id     -- 双列排序防同毫秒漏数
LIMIT 200
SETTINGS max_execution_time = 30;
```

返回每行 → 一条 `patient_transactions`(`rawPayload` = 整行 JSON,`sourceEventId` = `enc_id + etl_updated_at`,`occurredAt` = 业务时间字段)→ parser 衍生 `patient_facts` 版本。

完全复用 [`host-integration-v2.md`](host-integration-v2.md) §4 的 cursor / envelope / 错误处理语义,只是承载从 HTTP 换成了 SQL。

### 3.2 单患者即时刷新

```sql
SELECT * FROM dw.v_pac_clinical_encounters
WHERE patient_id = {pid:String}
ORDER BY etl_updated_at DESC LIMIT 100;
```

对应 PAC 的 `POST /pac/v1/sync/patient/{id}/refresh` 调用路径。

### 3.3 七层防线落地

| 防线 | CH 通道下的落地 |
|---|---|
| 1. 幂等 | `sourceEventId = <主键> + <etl_updated_at>` partial UNIQUE |
| 2. 水位续传 | `sync_logs.cursor_after = MAX(etl_updated_at)` |
| 3. 原文留底 | `patient_transactions.rawPayload` 存整行 |
| 4. Reconcile 对账 | 每日跑 `SELECT count(*) FROM v_pac_*` 跟 PAC 侧 transactions 数对账 |
| 5. Quarantine | adapter 解析失败的行落应用层 error log(v1 兜底)|
| 6. Drift 监控 | 视图字段消失 / 类型变更 → adapter 启动期校验,失败告警 |
| 7. 推断 stale | `personas.eventWatermark` 不变 |

---

## 4. 跟 REST Pull / Cold-import 的取舍

| 维度 | CH 视图 | REST Pull | Cold-import |
|---|---|---|---|
| 宿主工作量 | ⭐⭐⭐ 建视图 + 开账号(1-2 d)| ⭐ 写 14 个 endpoint(3-5 d)| ⭐⭐ 一次 dump(0.5 d)|
| 实时性 | 视图刷新频率(典型小时级)| 准实时(5 min) | ❌ 不支持增量 |
| 存量 + 增量统一 | ✅ 同一条 | ❌ 需配合 cold-import | ❌ 只解决存量 |
| 字段稳定性 | ⚠️ 视图列改 PAC SQL 立刻挂(需 2 周通知)| ✅ 接口契约稳 | n/a |
| 网络 | ⚠️ 需 VPN / 内网 | ✅ HTTPS 即可 | ✅ 文件传输 |
| 5 诊所历史回灌 | ✅ SQL 一把扫 30 min | ❌ 翻页慢 | ✅ |
| 适合场景 | **DW 数仓已就位** | 业务系统直出 | 临时 / 一次性 |

**典型选择**:
- 集团 DW 已经把宿主数据 ETL 到 CH → **走 CH 视图**(最低成本)
- 宿主只有 OLTP 没数仓 → **走 REST Pull**
- 两边都搞不定先上线 → **走 Cold-import** 兜底,后面补正

---

## 5. 接入 Checklist(宿主自查)

### 必填
- [ ] 7 张必填视图建好,字段命名按宿主习惯即可
- [ ] 每张视图含**业务主键 + clinic_id + patient 外键 + 单调列**四件套
- [ ] 视图已按试点诊所(5 家)过滤
- [ ] `pac_reader` 只读账号建好,密码用强随机,plaintext 一次性交付 PAC
- [ ] 网络可达性方案确定(VPN / 白名单 / 跳板)并完成测试
- [ ] 数据字典交付(参考 [`host-data-dictionary-example.yaml`](host-data-dictionary-example.yaml))
- [ ] **视图刷新频率写明**(5 min / 1 h / T+1?)
- [ ] **字段变更 SLA**:提前 2 周通知 PAC

### 强烈推荐
- [ ] 3 张推荐视图(`recharges` / `complaints` / `referrals`)
- [ ] 1:N 子表已聚成嵌套 JSON 列(避免 PAC 侧多次查询)
- [ ] 视图查询性能验证:`WHERE etl_updated_at > now() - INTERVAL 1 HOUR LIMIT 200` 应 < 1 s

### 可选
- [ ] 4 张可选视图
- [ ] [`actionUrls`](host-integration-v2.md#7-actionurls可选deep-link-模板) 模板(deep-link 跳回宿主 UI)
- [ ] [`/users:resolve`](host-integration-v2.md#6-usersresolve宿主方提供给-pac) endpoint(召回分配查客服用,仍需 REST,跟 CH 通道独立)

---

## 6. 跟宿主对齐时的 5 个必问问题

不管前面文档读没读,直接拉对方在群里答这 5 个:

1. **视图里有没有单调递增列**(`etl_updated_at` / `_version` / 时间戳)?名字是啥?
2. **视图多久刷一次**?实时 / 5 min / 1 h / T+1?
3. **PAC 服务器怎么连你 CH 集群**?VPN / 内网专线 / 公网+白名单?哪种走得通?
4. **视图字段稳定性**——你们改表 / 改 ETL 时能提前多久通知 PAC?
5. **视图是否已经按诊所过滤**?5 个试点 clinic_id 是什么?

5 个对齐了就可以开工。

---

## 7. 不在本通道范围的事(仍需另接)

| 项 | 渠道 |
|---|---|
| `/users:resolve` 查客服 | 仍走 REST([host-integration-v2.md §6](host-integration-v2.md#6-usersresolve宿主方提供给-pac))|
| 鉴权换票(`/auth/token`)| 仍走 REST([§3](host-integration-v2.md#3-鉴权))|
| iframe 嵌入 | 仍走前端 URL([§8](host-integration-v2.md#8-iframe-嵌入客服界面接入方式))|
| actionUrls deep-link | 仍走配置([§7](host-integration-v2.md#7-actionurls可选deep-link-模板))|

**CH 视图只替换了"数据接入"那一段**,鉴权 / iframe / 客服回写 / 跳转的链路完全不变。

---

## 8. PAC 侧适配工作(本节供 PAC 团队内部 reference)

| 工作 | 工作量 |
|---|---|
| 新增 `ClickHouseSqlPullAdapter`(实现 `PullStrategy` interface)| 1 d |
| `Platform.pullConfig` 加 `chConnection` 字段(host / port / database / user / password) | 0.5 d |
| `sync_logs.direction` 维持 `pull` 不细分,通过 `pullConfig.kind` 区分实现 | 顺手 |
| AssemblerEngine yaml 复用现有,无需改 | — |
| 启动期视图字段 schema 校验(防 drift) | 0.5 d |

---

**联系**:对接细节 / 视图字段含义 / 性能问题,找 PAC 接入对接人。
