Files
Neo-ZQYY/docs/_overview/04a-feedback/P0-5-matching-evolution.md
Neo 509cf43284 chore(docs): Wave 0 调研产出 + P0/P1/P2 反馈调研
建立项目级标杆文档 docs/_overview/ 作为产品全景索引,
解决"PRD 零碎、文档膨胀、跨子系统调研无入口"的问题。

主要内容:
- 00-index 总索引 + 维护协议 + 与 CLAUDE.md 关系
- 01-product-overview 产品全景脑图(6 角色 / 6 子系统 / 数据流 /
  7 业务概念 / 8+1 AI 矩阵 / 22 术语)
- 02a-miniprogram-page-matrix 小程序 21 页业务指纹
- 02b-adminweb-page-matrix admin-web 19 路由业务指纹
- 03-test-spec 测试规范 (L1-L5 分层 + 走查模板 + 75-95 case 估算)
- 04-doc-conflicts 39 条冲突索引(P0×8 / P1×13 / P2×13 + 5 子项)
- 04a/b/c-conflicts-*-detail 业务故事卡(7 字段:关联/逻辑/影响/选项/判定)
- 05-orphan-pages-cleanup admin-web 6 孤儿页面处置(1 归档 + 4 保留)
- WAVES-MASTER-PLAN.md 全 Wave 主计划(0-5,共 22-32 工作日)
- WAVE-1-KICKOFF.md Wave 1 实施 kickoff
- GLOBAL-DECISION-DASHBOARD.md 全局决策仪表板

反馈调研产物:
- 04a-feedback/ P0 两轮反馈(8+8 项决策 + D-1/2/3 + F-1/2 子代理产出)
- 04b-feedback/ P1 两轮反馈(13+1+5 项 + E-1/2/3/4 + G-1/2 子代理产出)
- 04c-feedback/ P2 反馈(13 项 + 5 子项 + H-1/2/3 子代理产出)
- NEO-DECISIONS-LOG 累积决策记录

关键追加发现 8 处 D Bug(原蓝本 0):
- P0-3 看板沙箱接入(Wave 1 W1-T1)
- P0-5 致命 1 (4 处 fdw_etl 残留, 已修 commit 17f045a)
- P0-5 致命 2 (JWT aud 缺失, 已修 commit 17f045a)
- P0-6 clearAllTasks 守卫 (Wave 3)
- P0-8 DBViewer 黑名单漏 (已修 commit 17f045a)
- P1-3 task-detail 跳转传 task_id 而非 customer_id
- P2-7 board-finance 隐式 null
- 2 个独立 Bug (page_context.created_at + ClueCategory 字典)

参考: docs/_overview/00-index.md
2026-05-04 07:38:28 +08:00

18 KiB
Raw Blame History

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

日期2026-05-04 触发Neo 在 04a-conflicts-P0-detail 反馈,倾向选项 B(补全 FDW 外部表),要求先调研当时为何偏离工程规范 关联文件:apps/backend/app/services/matching.pyapps/backend/app/services/fdw_queries.py 关联审计:2026-03-18__rns1-e2e-fdw-direct-connect-bugfix.md2026-03-20__h2-fdw-to-direct-etl-unification.md2026-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 (测试)。

# 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)

- 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)

-- 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)。