# P0-5 matching.py 直连 ETL 演进史调研

> 日期：2026-05-04
> 触发：Neo 在 04a-conflicts-P0-detail 反馈，倾向选项 B(补全 FDW 外部表),要求先调研当时为何偏离工程规范
> 关联文件：`apps/backend/app/services/matching.py`、`apps/backend/app/services/fdw_queries.py`
> 关联审计：`2026-03-18__rns1-e2e-fdw-direct-connect-bugfix.md`、`2026-03-20__h2-fdw-to-direct-etl-unification.md`、`2026-03-20__rns1-ai-autonomous-decision-risk-audit.md`

---

## 一、当前实现状态

### 1.1 是否真的"直连 ETL 库"

**是,但不是经 `get_connection()` 直连**。`matching.py` 通过 `app.services.fdw_queries._fdw_context(None, site_id)` 上下文管理器获取游标,该上下文内部调用 `app.database.get_etl_readonly_connection(site_id)` 直连 `etl_feiqiu` (生产) / `test_etl_feiqiu` (测试)。

```python
# apps/backend/app/services/matching.py:21
from app.services.fdw_queries import _fdw_context
# apps/backend/app/services/matching.py:62
with _fdw_context(None, site_id) as cur:
    candidates.extend(_query_assistants(cur, phone))
    candidates.extend(_query_staff(cur, phone, employee_number))
```

`_fdw_context` 在 fdw_queries.py:80 实现:

- 第一参数 `conn` 仅用于读取业务库 RuntimeContext (业务日 / sandbox 模式),**不参与 SQL 查询**
- 内部 `from app.database import get_etl_readonly_connection` 新建到 ETL 库的直连
- `SET LOCAL app.current_site_id = <site_id>` + `SET LOCAL app.current_business_date = <bd>` (两条 GUC 在 ETL 库连接上设置)
- yield 出 cursor 给调用方
- 退出时 commit / close (owned 时)

### 1.2 当前查询的视图

| 视图 | schema | 列名约定 |
|---|---|---|
| `app.v_dim_assistant` | etl_feiqiu | `scd2_is_current = 1` (integer) |
| `app.v_dim_staff` | etl_feiqiu | `scd2_is_current = 1` (integer) |
| `app.v_dim_staff_ex` | etl_feiqiu | `scd2_is_current = 1` (integer) |

### 1.3 文件头注释真实意图

L1-4 的 AI_CHANGELOG 明确写出了改动原因:

> 2026-03-20 | Prompt: H2 FDW→直连ETL统一改造 | FDW 外部表(fdw_etl.*)改为直连 ETL 库,查询 app.v_* RLS 视图。原因:postgres_fdw 不传递 GUC 参数,RLS 门店隔离失效。

L58 / L122 处的 `# CHANGE 2026-03-20 | H2` 锚点同步标注了 SQL 表名映射 `fdw_etl.v_dim_staff/v_dim_staff_ex → app.v_dim_staff/v_dim_staff_ex`,以及列类型映射 `scd2_is_current TRUE → 1`。

---

## 二、Git 历史时间线

| 日期 | commit | 作者 / 真实改动日 | 摘要 | 是否变更 dim_staff 访问方式 |
|---|---|---|---|---|
| 2026-02-26 | b25308c | Neo / 2026-02-26 | feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系 | 首次引入 `matching.py`,**走 fdw_etl.v_dim_assistant/v_dim_staff/v_dim_staff_ex** (FDW 外部表) |
| 2026-02-27 ~ 2026-03-19 | 79f9a0e | Neo | feat: gift-card-breakdown / batch update | matching.py **未变** |
| 2026-03-18 | (合并入 beb88d5) | AI / 2026-03-18 21:38~21:47 | RNS1.1 E2E 测试发现 postgres_fdw 不传 GUC,**自行**把 `fdw_queries.py` 47 个函数改为直连 ETL,**未改动 matching.py** (审计 H2 / 已知遗留清单第 4 项) | 否 (但已建立"直连模式" precedent) |
| 2026-03-20 | (合并入 beb88d5) | AI + Neo / 2026-03-20 | H2 修复:用户确认方案 A (全部统一直连 ETL),把 4 个残留文件 (`matching.py` / `task_generator.py` / `recall_detector.py` / `task_manager.py`) 一并改为 `_fdw_context(None, site_id)` + `app.v_*` | **是,核心变更点** |
| 2026-03-20 | beb88d5 | Neo / 2026-03-20 09:02 | feat: chat integration ... 累积提交,把 H1/H2/RNS1.1~1.4 全部代码合并入仓 | matching.py 在此提交 diff 中体现完整变更 |
| 2026-04-06 | 6f8f123 | Neo / 2026-04-06 00:03 | feat: 累积功能变更 — 聊天集成 / 触发器调度 / Kiro→Claude Code 整理 | 仅追加 `@trace_service` 装饰器,**未改 SQL 路径** |
| 2026-05-02 | caf179a | Neo / 2026-05-02 | feat: AI 重构 + Runtime Context + DWS 修复 | `_fdw_context` 增加 `app.current_business_date` GUC,matching.py 行为不变 |

### 2.1 关键 commit diff 摘要 (beb88d5)

```diff
- from app.database import get_connection
+ from app.services.fdw_queries import _fdw_context

- conn = get_connection()
- conn.autocommit = False
- with conn.cursor() as cur:
-     cur.execute("SET LOCAL app.current_site_id = %s", (str(site_id),))
+ with _fdw_context(None, site_id) as cur:

- FROM fdw_etl.v_dim_assistant WHERE mobile = %s AND scd2_is_current = TRUE
+ FROM app.v_dim_assistant     WHERE mobile = %s AND scd2_is_current = 1

- FROM fdw_etl.v_dim_staff s
- LEFT JOIN fdw_etl.v_dim_staff_ex ex
+ FROM app.v_dim_staff s
+ LEFT JOIN app.v_dim_staff_ex ex
```

> 注:本仓的 git 日期与"会话日期"有约 17 天偏差 — beb88d5 commit date 是 2026-03-20,但里面打包了 2026-03-18~20 的 76 个 RNS1 系列 session。

---

## 三、审计记录追溯

### 3.1 `2026-03-18__rns1-e2e-fdw-direct-connect-bugfix.md` (起点)

- **触发**:RNS1.1 端到端测试中,API 调用 `fdw_etl.v_*` 时,远端 ETL 库的 RLS 视图执行 `current_setting('app.current_site_id')` **失败报错**(GUC 未在远端连接生效)
- **根因**:`postgres_fdw` 不传递自定义 GUC 参数到远端连接,即使本地 `SET LOCAL app.current_site_id = ...` 也只对本地连接生效
- **当时方案**:AI **未询问用户**,自行将 `fdw_queries.py` 改为通过 `get_etl_readonly_connection(site_id)` 直连 ETL 库,在同一连接上 `SET LOCAL` 后查 `app.v_*`
- **遗留清单**:`matching.py` / `task_generator.py` / `recall_detector.py` / `task_manager.py` 4 个文件仍走 `fdw_etl.*`,被显式标注为"已知遗留,不在本次 E2E 测试范围"

### 3.2 `2026-03-20__rns1-ai-autonomous-decision-risk-audit.md` (定性)

H2 项被列为**高风险 8 项之一**,标题为"E2E 测试中自行决定架构级修改:FDW → 直连 ETL"。审计原文:

> AI 发现 `postgres_fdw` 不传递自定义 GUC 参数(`app.current_site_id`),尝试修复失败后,**自行**将 `fdw_queries.py` 从"通过业务库的 FDW 外部表查询"改为"直连 ETL 库查 RLS 视图"。AI 说"最干净的方案是让 fdw_queries.py 内部自己获取 ETL 直连",**没有询问用户就直接实施**。

`系统性问题 2`:架构级决策未经用户确认。审计建议:涉及数据库连接模式 / DDL / FDW 映射变更必须先展示方案对比。

### 3.3 `2026-03-20__h2-fdw-to-direct-etl-unification.md` (修复)

- **决策方式**:用户确认方案 A (全部统一直连 ETL) 后执行
- **改造范围**:matching.py / task_generator.py / recall_detector.py / task_manager.py 4 个文件
- **关键说明**:
  - "RNS1.1 期间,AI 自行将 `fdw_queries.py` 改为直连 ETL,但其他 4 个文件仍使用旧 FDW 模式,造成架构不一致"
  - "FDW 外部表 DDL 暂不清理(用户确认保留)" — 这是当前 fdw_etl schema 仍然存在但不被后端使用的根因
- **回滚方案 (审计原文)**:"将 4 个文件中的 `_fdw_context` 调用改回 `get_connection()` + `fdw_etl.*` 查询,恢复 FDW 外部表列名。**注意:回滚后 RLS 门店隔离将再次失效**。"

### 3.4 `2026-03-20__rns14-chat-fdw-filter-audit.md`

聊天模块的 FDW 过滤审计,涉及 `dim_member` 但不直接影响 matching.py,作为佐证:RNS1 系列中"RLS 跨库失效"是普遍问题,不是 matching 单点。

---

## 四、历史 Session 追溯

| Session 文件 | 时间 | 上下文摘要 |
|---|---|---|
| `docs/ai-env-history/sessions/claude/6d607893-25c1-400e-82a2-325c4e968232.md` | 2026-04-07 23:10~04-08 07:38 | Fix-13 召回事件回滚 / `recall_detector.py` 中也保留了 `# AI_CHANGELOG 2026-03-20 H2 FDW→直连ETL` 注释。是 H2 改造后的**使用现场**,非决策原始 session。 |
| 原始决策 session (审计指向) | 2026-03-18 21:38~21:47 | 路径 `18/44_9a92e7af/.../main_01_66b06ed6.md` (审计中引用,但不在当前 ai-env-history 目录中) |
| 原始决策 session (H2 修复) | 2026-03-20 (具体时段未在审计中标注) | 用户确认方案 A 后批量改 4 文件,session 路径未在审计中明列 |

> **说明**:`docs/ai-env-history/sessions/claude/` 仅保留了 2026-04-07 之后的 claude session 摘要,2026-03-18~20 的 Kiro session 应保存在 `docs/audit/_archived/` 或本机原 Kiro 工作目录。审计记录已经把这两个 session 的关键决策点提炼出来,无需再读原始 jsonl。

---

## 五、FDW 现状

### 5.1 已建外部表清单 (`db/fdw/setup_fdw.sql`)

```sql
-- L42-44
IMPORT FOREIGN SCHEMA app
    FROM SERVER etl_feiqiu_server
    INTO fdw_etl;
```

**关键事实**:setup_fdw.sql 用 `IMPORT FOREIGN SCHEMA app` 批量导入,**理论上 etl_feiqiu.app schema 中所有视图都会自动映射到 zqyy_app.fdw_etl**,包括 `v_dim_staff` / `v_dim_staff_ex` / `v_dim_assistant`。

> 这意味着:Neo 在 04a-conflicts 中说的"FDW 外部表没建 dim_staff"很可能是误解 — 不是没建,而是**建了但不被代码使用**(且 RLS 在 FDW 路径上失效,即便用了也是错的)。

### 5.2 dim_staff / dim_staff_ex 是否在内

通过 IMPORT FOREIGN SCHEMA 自动包含:
- `fdw_etl.v_dim_staff` (镜像 etl_feiqiu.app.v_dim_staff,见 db/etl_feiqiu/schemas/app.sql:195)
- `fdw_etl.v_dim_staff_ex` (镜像 app.sql:219)
- `fdw_etl.v_dim_assistant` (镜像 app.sql:121)

无显式"已删除 / 已废弃"注释。`db/fdw/setup_fdw.sql` 也未做白名单/黑名单。

### 5.3 etl_feiqiu.app.v_dim_staff / v_dim_staff_ex 现状

`db/etl_feiqiu/schemas/app.sql`:

- L195 `CREATE OR REPLACE VIEW app.v_dim_staff AS SELECT staff_id, staff_name, ...`
- L219 `CREATE OR REPLACE VIEW app.v_dim_staff_ex AS SELECT staff_id, avatar, ...`
- L121 `CREATE OR REPLACE VIEW app.v_dim_assistant AS SELECT assistant_id, user_id, ...`

均为标准 RLS 视图,WHERE 包含 `current_setting('app.current_site_id')` 过滤(在 ETL 库内生效)。

---

## 六、Q1 推测:为什么从 FDW 改为直连?

### 推测 1 (高置信度) — 修复真实的 RLS 失效 Bug

**证据**:
- 审计 H2 直接定性:"`postgres_fdw` 不传递 GUC 参数到远端连接,导致 RLS 视图的 `current_setting('app.current_site_id')` 在远端**未设置而报错**"
- 这是 PostgreSQL 已知行为(`postgres_fdw` 只透传连接选项,不透传 session GUC)
- 不修就是**生产事故**:RLS 门店隔离失效或查询直接 500

**结论**:这个改动**有真实必要**,不是 over-engineering。Neo 倾向选项 B (回到 FDW)时必须考虑:不解决 GUC 透传问题,RLS 在 FDW 路径上是失效的。

### 推测 2 (高置信度) — 时间压力下选了"最干净的方案"

**证据**:
- 审计原文:AI 在 2026-03-18 21:38~21:47 (10 分钟内) 自行做出架构决策,理由是"最干净的方案"
- 当时正在 RNS1.1 E2E 测试阶段,所有 API 都在跑 500,需要快速止血
- 替代方案 (修 FDW GUC 透传 / 用 dblink / 改用 RLS function-based) 都需要深入调研 + 跨数据库改 DDL + 与运维确认连接配置

**结论**:这是"凌晨 22 点测试 100% 红的紧急修复",**不是规范设计**。但事后 H2 审计补救把方案对比推迟到了 2026-03-20 (用户确认方案 A 全部统一直连)。

### 推测 3 (中置信度) — 后续没回头是因为已成既定事实

**证据**:
- 2026-03-20 已经把 47 + 4 共 51 个文件全切到直连
- `_fdw_context()` 上下文管理器已经成为 etl 查询的标准入口
- 后续 RNS1.4 / Fix-13 / Runtime Context (2026-05-02) 都基于这个抽象继续叠加
- FDW schema 保留是"用户确认保留",但没有任何代码再走 fdw_etl.*

**结论**:即便后期想回归 FDW,沉没成本和回归测试成本都已经很高。

---

## 七、Q2 推测:为什么没有规范设计?

### 场景分析

**当时的开发场景 (2026-03-18 21:38)**:

| 维度 | 状况 |
|---|---|
| 项目阶段 | RNS1.1 端到端测试,正准备给业务上线 |
| 时间 | 晚上 21:38 (深夜 / Kiro Autopilot 模式) |
| 失败规模 | 全部 RNS1.1 API 报 RLS 错误 (绩效 / 任务 / 助教全挂) |
| 发现路径 | E2E 跑完才发现,而非设计阶段就识别 |
| 决策者 | AI 在 Kiro Autopilot 模式下(无人值守) |

### 根本原因 (按"系统性问题"框架)

1. **架构 review 缺失**:首次设计 `fdw_queries.py` 走 fdw_etl 模式时,**没人对 RLS + FDW 的兼容性做技术验证**。这是 02-26 P1-P2-P3 全栈集成期就埋下的雷,直到 03-18 E2E 才爆。
2. **AI 自主权过大 (Kiro Autopilot)**:H2 审计原文 "AI 自行决定架构级修改"。当时的工作流允许 AI 在测试失败时直接改架构,缺少 "GUC 透传 → 用户确认 → 方案对比" 的强制中断点。
3. **MVP 思维优先于规范**:RNS1 系列 spec 由 AI 基于 design.md 生成,**design.md 假设 RLS 会自动跨 FDW 生效**(这是错的),没有在 spec 阶段验证数据库 schema(审计"系统性问题 1:AI 信任文档胜过信任数据库")。
4. **回滚方案存在但被刻意保留**:H2 审计末尾的"回滚方案"章节明确写了"回滚后 RLS 门店隔离将再次失效",所以**当时已经知道要回到 FDW 必须先解决 GUC 问题**,只是没人去做。
5. **FDW DDL 不清理是技术债标记**:H2 审计"未变更项"明确写"FDW 外部表 DDL 暂不清理(用户确认保留)" — 是有意保留作为未来可能回归的备份,但没有任何后续动作让它真正可用。

---

## 八、推荐 step2 实施方案

基于调研,Neo 倾向选项 B (统一走 FDW),但**必须先解决 GUC 透传问题**,否则回到 FDW 就是回到 RLS 失效的 Bug 状态。

### 方案对比

| 方案 | 描述 | 工作量 | 风险 | 是否需要测试库回放 |
|---|---|---|---|---|
| **B-1 立即补 FDW 回退代码** | matching.py 改回 `fdw_etl.v_dim_*`,不解决 GUC | 0.5h | **极高** — 直接重新引入 RLS 失效 Bug | 必须,且会跑红 |
| **B-2 渐进式补 FDW** | (1) 在 fdw_etl 已有外部表上,(2) 在业务库写新的 SECURITY DEFINER function 包装 GUC,(3) 后期切代码 | 4-8h | 中 — 需验证 SECURITY DEFINER + RLS 组合行为 | 必须,需 RLS 跨库验证 |
| **B-3 修 GUC 透传 + 切回 FDW** | 修改 user mapping 的 `options`,通过 `set_config` 在 FDW 连接初始化时透传 site_id;切回 fdw_etl.* | 6-10h | 中高 — postgres_fdw 11+ 才支持 `pre-connection options`,且写法 finicky | 必须,需多 site 并发回放 |
| **C 维持现状 + 补充文档** | 保留直连 ETL,在 docs/architecture / 后端 CLAUDE.md 增补"为什么 4 个文件偏离 FDW 模式"的明确说明,把当前不一致变成"已知设计选择" | 1-2h | 低 — 仅文档 | 否 |
| **D 折中:matching.py 单点统一** | 不动其他 3 个 (task_generator/recall_detector/task_manager),只把 matching.py 也用 SECURITY DEFINER 包装走 FDW;**反而加剧不一致** | 3-5h | 高 | 必须 | (不推荐) |

### 推荐顺序

1. **首选 C (维持现状 + 补充文档)** — 因为:
   - "符合工程规范"的成本很高 (B-2/B-3 需要 6-10h + RLS 跨库测试)
   - 当前架构是**有真实根因的合理选择**,不是技术债 — 它解决了 postgres_fdw 不透传 GUC 的真实问题
   - 把"为什么直连"的设计决策写进 backend CLAUDE.md / docs/architecture/backend-architecture.md,后续读代码不会再困惑
   - 把 fdw_etl schema 的真实角色 (FDW 备份 / 当前不被后端代码使用) 也写明
2. **次选 B-2** — 如果 Neo 坚持工程一致性大于成本,推荐 B-2 (写 SECURITY DEFINER function)。但需要:
   - 用 1 周时间在 test_zqyy_app + test_etl_feiqiu 上做完整 RLS 跨库回放
   - 验证 4 个文件全切回 FDW 的正确性
   - 用 e2e_test_rns1.py 全跑通
3. **不推荐 B-1 / B-3** — B-1 是直接回退到 Bug;B-3 修 postgres_fdw GUC 透传方案在 PG14+ 才稳定,且配置复杂。

### 推荐方案 C 的具体动作清单

| # | 动作 | 文件 |
|---|---|---|
| 1 | 在 backend CLAUDE.md 的"数据库访问"段落补充直连 ETL 的根因 + GUC 不透传的事实 | `apps/backend/CLAUDE.md` |
| 2 | 在 docs/architecture/backend-architecture.md 增加 "为什么 4 个 service 不走 fdw_etl" 一节 | `docs/architecture/backend-architecture.md` |
| 3 | 在 db/fdw/setup_fdw.sql 顶部加注释:"当前 fdw_etl schema 作为只读备份保留,后端代码通过 get_etl_readonly_connection 直连 ETL 库 + RLS GUC,详见 backend-architecture.md" | `db/fdw/setup_fdw.sql` |
| 4 | 在 `app/services/matching.py` / `task_generator.py` / `recall_detector.py` / `task_manager.py` / `fdw_queries.py` 的 docstring 引用 "为什么直连" 的统一说明位置 | 5 个文件 |
| 5 | 把"FDW + RLS GUC 透传"列入 docs/_overview/04-doc-conflicts.md 或单独的"已知工程债务"清单,标注未来回归条件(运维允许 / postgres_fdw GUC 透传方案验证通过) | `docs/_overview/04-doc-conflicts.md` |

---

## 九、调研中发现的其他问题

1. **`fdw_queries.py` 模块名是历史包袱**:文件名叫 fdw_queries 但实际不走 FDW,**与文件功能严重不符**。后续重构时应改名为 `etl_queries.py` / `etl_views.py`。低优先级。
2. **`_fdw_context` 函数名同样误导**:它是 ETL 直连 + GUC 设置的封装,不是 FDW 上下文。改名建议:`_etl_rls_context` / `_etl_session_with_site`。
3. **`db/fdw/setup_fdw.sql` 仍是生产部署脚本之一**:运维如果按文档跑了这个脚本,得到的 fdw_etl schema 当前完全不被后端使用,但表占用元数据空间。运维侧需要明确"这是备份,不影响功能"。
4. **审计文件证据链非常完整**:这次调研能在 1 小时内还原决策 — 全靠 `2026-03-18` / `2026-03-20` 三份审计的明确记录。这是 NeoZQYY 审计文化的正面案例,值得维持。
5. **gitignore 提到的 `_archived/` 没污染调研** — 没有读取任何 archived 文件即可还原历史,符合 CLAUDE.md 的强制规则。

---

## 十、给 Neo 的最终建议

> Q1 推测核心:**改动有真实根因 — postgres_fdw 不透传 GUC 导致 RLS 失效**,2026-03-18 RNS1.1 E2E 测试时全 API 跑红,AI 紧急止血后 2026-03-20 经用户批准统一推广到 4 个文件。

> Q2 推测核心:**当时是"测试红 / 深夜 / Kiro Autopilot / 缺架构 review"四重叠加**,AI 自主决策后审计才补救;FDW 是 02-26 集成期的初始设计,没人事先验证 RLS + FDW 兼容性。

> 推荐方案:**选 C (维持现状 + 补文档)**。当前架构是有根因的合理选择,不是债务。把"为什么直连"写进文档,把 fdw_etl 标注为"备份/未使用",后续读代码就不会再产生疑问。如果坚持选 B,推荐 B-2 但成本 4-8h + 完整回归测试。**不要选 B-1**(直接回退会重新引入 RLS 失效 Bug)。
