建立项目级标杆文档 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 残留, 已修 commit17f045a) - P0-5 致命 2 (JWT aud 缺失, 已修 commit17f045a) - P0-6 clearAllTasks 守卫 (Wave 3) - P0-8 DBViewer 黑名单漏 (已修 commit17f045a) - 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
27 KiB
P1-1 维客线索 schema 迁移(public → biz)风险评估
日期:2026-05-04 触发:Neo 在
04b-conflicts-P1-detail.md§ P1-1 反馈选 A(迁移到 biz),保证项目工程的规范性,要求单独起任务调研风险评估 调研范围:测试库test_zqyy_app+ 仓库内全部代码 / DDL / FDW / SPEC / 后端 / 小程序 产出形态:调研 + 实施方案推荐,不动任何代码或 DB
一、依赖盘点结果
1.1 总览
按 grep 结果(member_retention_clue / retention_clue)统计涉及面:仓库内 85 个文件命中。其中会因为 schema 迁移真正受影响的"代码 + DDL + FDW"清单如下(文档 SPEC 仅需更新文字,不阻塞迁移)。
1.2 业务库 DDL(db/zqyy_app/)
| 文件 | 关键内容 | 改动类型 |
|---|---|---|
db/zqyy_app/schemas/public.sql |
序列 public.member_retention_clue_id_seq、表 public.member_retention_clue、3 个索引、1 个 PK |
删除 |
db/zqyy_app/schemas/biz.sql |
当前不含此表 | 新增表 + 序列 + 索引 |
db/zqyy_app/migrations/ |
当前空(v1 已归档) | 新增迁移脚本 |
db/_archived/migrations_v1_merged/zqyy_app/2026-03-20__ns4_member_clue_is_hidden.sql |
历史迁移(已归档),不动 | 不改(仅文档说明) |
1.3 FDW 反向映射(db/fdw/)
| 文件 | 关键内容 | 改动类型 |
|---|---|---|
db/fdw/setup_fdw_reverse.sql |
CREATE FOREIGN TABLE fdw_app.member_retention_clue ... OPTIONS (schema_name 'public', table_name 'member_retention_clue') |
改 schema_name 'biz' |
db/fdw/setup_fdw_reverse_test.sql |
同上(指向 test_zqyy_app) |
改 schema_name 'biz' |
docs/database/ddl/fdw_reverse.sql |
自动生成的 DDL 镜像 | 重新生成 |
docs/database/BD_Manual_fdw_reverse_retention_clue.md |
FDW 文档 | 全文更新 schema 引用 |
注:FDW 外部表名 fdw_app.member_retention_clue 不需改名,只需把 OPTIONS.schema_name 由 'public' 改成 'biz',对 ETL 侧调用无破坏(外部表名不变)。
1.4 后端代码(apps/backend/app/)— 共 6 处 SQL 直引
| 文件 | 行 | SQL 引用形态 | 改动 |
|---|---|---|---|
routers/member_retention_clue.py |
29 / 68 / 87 | 无 schema 前缀(依赖 search_path) | 加 biz. 显式前缀 |
routers/tenant_clues.py |
67 / 197 / 235 / 267 / 298 | public.member_retention_clue 硬编码 |
全改 biz.member_retention_clue |
services/customer_service.py |
271 / 278 | public.member_retention_clue 硬编码 |
改 biz. |
services/task_manager.py |
1125 | public.member_retention_clue 硬编码 |
改 biz. |
ai/data_fetchers/page_context.py |
241 | 无 schema 前缀,且字段名错为 created_at(应为 recorded_at) |
加 biz. 前缀,附带修 created_at → recorded_at Bug |
ai/dispatcher.py |
574 / 588 | App8 写入:无 schema 前缀(DELETE + INSERT) | 加 biz. 前缀 |
总计后端硬编码点:6 文件 / 11 处 SQL(与冲突卡估计的 5-8 处一致,略多)。
1.5 后端 Pydantic / Schema 层
| 文件 | 关注点 |
|---|---|
apps/backend/app/schemas/member_retention_clue.py |
无 schema 名硬编码;但枚举 ClueCategory 仍使用旧值 客户基础信息,与 BD 手册 2026-03-08 对齐后的 客户基础 不一致(独立 Bug,登记不在本任务) |
apps/backend/app/schemas/tenant_clues.py |
grep 命中文档字符串,无 schema 引用 |
apps/backend/app/schemas/xcx_tasks.py / xcx_customers.py |
grep 命中文档字符串,无 schema 引用 |
1.6 小程序前端(apps/miniprogram/)
仅命中 retentionClues / retention_clues 字段名,与 schema 无关:
typings/api.d.ts(L112 / L209):API 响应类型pages/customer-detail/customer-detail.ts:122、pages/task-detail/task-detail.{ts,wxml}:页面渲染utils/mock-data.ts:57:MOCK 数据
结论:小程序零改动。
1.7 租户管理后台前端(apps/tenant-admin/)
grep 命中 0 处。零改动。
1.8 ETL Connector(apps/etl/connectors/feiqiu/)
grep 命中 0 处。ETL 侧零改动(FDW 外部表名 fdw_app.member_retention_clue 由 FDW 文件维护)。
1.9 文档 / SPEC(仅文字同步,不阻塞迁移)
| 文件 | 内容 |
|---|---|
docs/database/BD_Manual_member_retention_clue.md |
9 处 public → biz |
docs/database/BD_Manual_fdw_reverse_retention_clue.md |
5 处 schema 引用 + 数据流向图 |
docs/database/BD_Manual_biz_tables.md |
public.member_retention_clue → biz.member_retention_clue |
docs/database/ddl/zqyy_app__public.sql / ..__biz.sql |
重新生成(tools/db/gen_consolidated_ddl.py) |
docs/database/README.md |
schema 索引 |
docs/specs/tenant-admin-web/{requirements,design,tasks}.md |
7 处 |
docs/specs/rns1-task-performance-api/*.md |
4 处 |
docs/specs/rns1-customer-coach-api/*.md |
4 处 |
docs/specs/05-miniapp-ai-integration/design.md |
1 处 |
docs/prd/specs/00-数据依赖矩阵.md |
3 处 |
docs/prd/specs/P10-tenant-admin-web.md |
1 处 |
docs/prd/specs/P14-ai-dashscope-migration.md |
1 处 |
docs/prd/specs/board-detail-gap-analysis.md |
1 处 |
docs/prd/Neo_Specs/RNS1-split-plan.md、NS1、NS2、NS4、review-audit/ |
多处 |
docs/_overview/01-product-overview.md |
§ 八冲突登记 |
docs/_overview/04b-conflicts-P1-detail.md |
冲突卡 |
docs/contracts/openapi/backend-api.json |
注释字段 |
docs/architecture/backend-architecture.md L118 |
1 处 |
docs/ai/ai_apps_feature_acceptance_spec.md |
4 处(含 G20 RLS 缺失提醒) |
二、表结构 / 索引 / RLS 现状
2.1 列定义(11 列)
| 列名 | 类型 | 默认值 | NOT NULL | 备注 |
|---|---|---|---|---|
| id | bigint | nextval('public.member_retention_clue_id_seq'::regclass) |
YES | PK |
| member_id | bigint | — | YES | 会员 ID |
| category | varchar(20) | — | YES | CHECK 6 值 |
| summary | varchar(200) | — | YES | 摘要 |
| detail | text | — | NO | 详情 |
| recorded_by_assistant_id | bigint | — | NO | 助教 ID |
| recorded_by_name | varchar(50) | — | NO | 助教姓名 |
| recorded_at | timestamptz | now() |
YES | 录入时间 |
| site_id | bigint | — | YES | 多门店隔离 |
| source | varchar(20) | 'manual' |
YES | manual / ai_consumption / ai_note |
| is_hidden | boolean | false |
YES | 隐藏控制(NS4) |
2.2 约束
- PK:
member_retention_clue_pkey(id) - CHECK:
chk_retention_clue_category(6 值枚举) - NOT NULL:8 列
- FK:无(既不引用其他表,也无表 FK 引用本表 — 经
information_schema.constraint_column_usage查证)
2.3 索引(4 个,含 PK)
member_retention_clue_pkey UNIQUE (id)
idx_retention_clue_member (member_id)
idx_retention_clue_site (site_id)
idx_retention_clue_category (member_id, category)
2.4 RLS 策略
relrowsecurity = false/relforcerowsecurity = falsepg_policies查询返回 0 行- 结论:当前未启用 RLS
注:
ai_apps_feature_acceptance_spec.mdG20 项已登记"全部 AI 表未启用 RLS"为已知差异,本次迁移不引入新 RLS,保持现状(与现行后端隔离方式一致:tenant_clues 路由通过site_filter_clause强制 site_id 条件,retention_clue 路由通过参数site_id限制)。
2.5 触发器
无非内部触发器。
2.6 序列
public.member_retention_clue_id_seq(bigint),需迁移到 biz。
三、数据量评估
测试库 test_zqyy_app(2026-05-04 实查):
总行数: 44
门店分布: site_id=2790685415443269 → 44 行(单店)
来源分布: ai_consumption → 44 行(全部 AI 写入,无 manual)
分类分布: 消费习惯 23 / 玩法偏好 12 / 客户基础 9
时间范围: 2026-04-21 00:20 ~ 2026-05-01 01:53
biz schema: 存在
biz.member_retention_clue: 不存在
生产库未直连查询,估算依据:本表为 AI 写入主导(App8 强幂等替换),单门店 10 天 44 行,按 4 门店、180 天线性外推约 3000-5000 行。即使生产库 10 倍于测试库,整表数据量在万行量级,迁移过程一次性 INSERT 即可完成(< 1 秒)。
四、风险等级矩阵
| # | 风险维度 | 等级 | 简述 | 缓解措施 |
|---|---|---|---|---|
| R1 | 数据丢失 | 低 | 行数小(< 万),无并发写压力,可在事务内 INSERT INTO biz... SELECT FROM public... | 事务 + 完成后 COUNT(*) 一致性校验;保留 public 表 7 天后再 DROP |
| R2 | FK 破坏 | 无 | 无任何 FK 入/出 | — |
| R3 | FDW 外部表破坏 | 中 | fdw_app.member_retention_clue OPTIONS 写死 schema='public',不改会读到旧表(DROP 后报错) |
迁移完成后立即 ALTER FOREIGN TABLE ... OPTIONS (SET schema_name 'biz') |
| R4 | RLS policy 重建 | 无 | 当前未启用 RLS,迁移不引入 | — |
| R5 | 后端代码硬编码 schema | 高 | 11 处 SQL 直引,遗漏一处即在生产报"relation does not exist" | 全量替换 + grep 自动校验 + 后端单元测试 + staging 全链路验证 |
| R6 | 后端 search_path 隐式依赖 | 中 | routers/member_retention_clue.py、dispatcher.py、page_context.py 共 5 处不带 schema 前缀,依赖 search_path 默认含 public |
迁移后这 5 处必须显式加 biz. 前缀,否则会找到 public 旧表(如未删)或报错(已删) |
| R7 | 前端 mock / 类型 | 无 | 小程序仅引用字段名 retentionClues,与 schema 无关 |
— |
| R8 | App8 写入路径 | 高 | dispatcher.py L574/L588 是核心幂等写入,DELETE + INSERT 在事务内运行,写错 schema 会让幂等失效 | 改 schema 同时保持 SQL 结构不变;测试库 dry-run 跑一次 App8 |
| R9 | AI prompt 硬编码 SQL | 低 | App8 prompt 仅做 JSON 拼接,不含 SQL;只在注释提及表名 | 注释同步改即可 |
| R10 | 回滚成本 | 低 | 备份 public 表(保留 7 天)+ 代码 git revert + FDW OPTIONS 回退 | 完整 rollback runbook 见 § 五-A 第 7 步 |
| R11 | 文档 / SPEC 不一致 | 中 | 25+ 个文档需要同步替换 public.member_retention_clue → biz.member_retention_clue |
批量 grep + sed 风格替换;先做 DDL/代码,文档收尾 |
| R12 | 序列权限 | 低 | public.member_retention_clue_id_seq 需迁到 biz,应用 INSERT 时使用 nextval() |
DDL 层 ALTER SEQUENCE ... SET SCHEMA biz 一并搬迁 |
| R13 | 测试数据库与生产环境差异 | 中 | 测试库已观测无 RLS、无 FK;生产库可能有手动加的索引或 grant | 迁移前对生产库执行同样的 prescan SQL(见 § 七验证锚点) |
| R14 | 多门店数据混入 | 低 | 测试库仅 1 site,生产库未知;INSERT INTO ... SELECT 不会造成混入 | site_id 列原样复制 |
风险最高的 3 项:R5(11 处 SQL 直引漏改)、R8(App8 幂等链路)、R3(FDW OPTIONS)。
五、推荐方案(三套对比)
方案 A — 一次性迁移(短停机 bigbang)
适用条件:本任务 100% 满足
- 数据量小(< 万行)
- 无 FK 出入
- 无 RLS 不需重建
- 无生产小程序高并发写入(写入只来自 tenant-admin 手动 + App8 离线触发)
- 单一 monorepo,可同时部署后端代码 + DB DDL + FDW
步骤清单
- 预检(5 min):在生产库执行 § 七 验证 SQL,确认结构与测试库一致
- 公告 / 短停机(10 min 内):通知所有 tenant-admin 用户,暂停 ai_trigger_jobs 调度
- DDL 迁移(事务内执行,1 min):
BEGIN; -- 5.1 先把 schema 从 public 搬到 biz(含序列) ALTER TABLE public.member_retention_clue SET SCHEMA biz; ALTER SEQUENCE public.member_retention_clue_id_seq SET SCHEMA biz; -- 5.2 PostgreSQL 12+ 会自动改 default 表达式中的序列引用,但稳妥起见显式重置: ALTER TABLE biz.member_retention_clue ALTER COLUMN id SET DEFAULT nextval('biz.member_retention_clue_id_seq'::regclass); -- 5.3 重命名约束(保持 chk_retention_clue_category 命名)— ALTER SCHEMA 已自动处理 -- 5.4 索引随表迁移(PostgreSQL 自动) COMMIT; - FDW OPTIONS 切换(在 etl_feiqiu 中执行,10 sec):
测试库同样在 test_etl_feiqiu 执行。
ALTER FOREIGN TABLE fdw_app.member_retention_clue OPTIONS (SET schema_name 'biz'); - 后端代码部署(已经过 staging 全链路通过):
- 6 文件 11 处 SQL 全改为
biz.member_retention_clue - 顺手修
page_context.py:243created_at → recorded_atBug
- 6 文件 11 处 SQL 全改为
- 冒烟测试(10 min):
- retention_clue CRUD 路由(POST/GET/DELETE)
- tenant_clues 5 个端点
- customer_service 客户详情接口(CUST-1 retentionClues 块)
- task_manager 任务详情接口
- 触发一次 App8(dispatch app8_consolidation),确认幂等 DELETE + INSERT 落入 biz
- page_context.build_page_text 维客线索拼接
- 回滚预案:
- DDL 层:
ALTER TABLE biz.member_retention_clue SET SCHEMA public; ALTER SEQUENCE biz.member_retention_clue_id_seq SET SCHEMA public; - FDW:
ALTER FOREIGN TABLE fdw_app.member_retention_clue OPTIONS (SET schema_name 'public'); - 后端:
git revert单个 commit - 数据:原表整表搬迁未发生数据复制,零丢失
- DDL 层:
- 观察期(7 天):保留 7 天观察告警,无异常后归档迁移脚本到
db/zqyy_app/migrations/
工作量预估
| 阶段 | 人时 |
|---|---|
| 代码改动(6 文件 + 顺修 created_at Bug) | 2.0 |
| DDL + FDW 脚本编写 + 测试库验证 | 1.5 |
| 测试库全链路冒烟(含 App8 dry-run) | 1.5 |
| 文档 / SPEC 批量同步(25+ 文件) | 2.0 |
| 审计记录 + DB 文档同步 | 1.0 |
| 生产部署 + 观察 | 1.0 |
| 合计 | 9 人时 |
风险等级 — 低-中
主要风险落在 R5(漏改 SQL)和 R8(App8 链路),通过 staging 全链路验证可消除。
方案 B — 渐进迁移(双写 / 视图过渡)
思路:建 biz.member_retention_clue 实表 + 数据复制 → 在 public.member_retention_clue 替换为 biz 同名视图 → 后端代码逐文件切到 biz. 前缀 → 全部切完后删 public 视图。
步骤清单
- 在 biz 中
CREATE TABLE biz.member_retention_clue (LIKE public.member_retention_clue INCLUDING ALL)+ 序列 + 索引 INSERT INTO biz.member_retention_clue SELECT * FROM public.member_retention_clue- 重置
biz.member_retention_clue_id_seq到 max(id) + 1 - 事务内:
DROP TABLE public.member_retention_clue→CREATE VIEW public.member_retention_clue AS SELECT * FROM biz.member_retention_clue+ 创建对应 INSTEAD OF 触发器(处理 INSERT/UPDATE/DELETE) - 后端代码逐文件 PR 切到 biz.(11 处分多个 PR)
- 全部切完后:删
public.member_retention_clue视图 + 触发器 - FDW OPTIONS 切到 biz
工作量预估
约 20-25 人时(视图 + INSTEAD OF 触发器 + 多次部署)。
风险等级 — 中
- 优点:后端代码可灰度切换,无停机
- 缺点:双写期间复杂度高,触发器路径出 Bug 排查难;本任务无并发写压力,收益不匹配成本
方案 C — 暂缓 + 文档说明
思路:保留 public.member_retention_clue,在 db/CLAUDE.md 和 01-product-overview.md § 八明确登记"维客线索为 public 例外,因 [理由]"。
何时选择
- 当迁移工作量 > 收益时
- Neo 已否决此选项(反馈选 A,明确要"保证项目工程的规范性")
故方案 C 不再展开。
六、关键风险点详细分析
R5 — 后端代码 11 处 SQL 直引(高)
tenant_clues.py 是受影响最大的文件(5 处显式 public. 前缀),其中 _get_clue_with_site_check 内嵌 SQL 同时带 site_filter_clause 拼装,改动需保持参数顺序和 f-string 拼接的占位符一致。
具体替换清单(文件:行 → 旧 → 新):
routers/member_retention_clue.py:29 "INSERT INTO member_retention_clue" → "INSERT INTO biz.member_retention_clue"
routers/member_retention_clue.py:68 "FROM member_retention_clue" → "FROM biz.member_retention_clue"
routers/member_retention_clue.py:87 "DELETE FROM member_retention_clue WHERE..." → "DELETE FROM biz.member_retention_clue WHERE..."
routers/tenant_clues.py:67 "FROM public.member_retention_clue" → "FROM biz.member_retention_clue"
routers/tenant_clues.py:197 "FROM public.member_retention_clue" → "FROM biz.member_retention_clue"
routers/tenant_clues.py:235 "UPDATE public.member_retention_clue" → "UPDATE biz.member_retention_clue"
routers/tenant_clues.py:267 "DELETE FROM public.member_retention_clue" → "DELETE FROM biz.member_retention_clue"
routers/tenant_clues.py:298 "UPDATE public.member_retention_clue" → "UPDATE biz.member_retention_clue"
services/customer_service.py:278 "FROM public.member_retention_clue" → "FROM biz.member_retention_clue"
services/task_manager.py:1125 "FROM public.member_retention_clue" → "FROM biz.member_retention_clue"
ai/data_fetchers/page_context.py:241 "SELECT summary FROM member_retention_clue" → "SELECT summary FROM biz.member_retention_clue" ⚠ 同时修复 created_at → recorded_at
ai/dispatcher.py:574 "DELETE FROM member_retention_clue" → "DELETE FROM biz.member_retention_clue"
ai/dispatcher.py:588 "INSERT INTO member_retention_clue" → "INSERT INTO biz.member_retention_clue"
校验做法:迁移完成后,跑一次 Grep "FROM member_retention_clue|INTO member_retention_clue|UPDATE member_retention_clue|DELETE.+member_retention_clue|public\.member_retention_clue" --path apps/backend,应当 0 命中。
R8 — App8 幂等写入链路(高)
dispatcher.py:_write_retention_clue 是 App8 的强幂等核心:DELETE 同源旧记录 + INSERT 新批,事务级别保证当天替换原子性。迁移后必须验证:
- 触发一次 App8(target_id = 测试 member),确认
biz.member_retention_clue中该 (member_id, site_id, source='ai_consumption') 的记录被 DELETE 后 INSERT - 检查事务回滚路径(人为制造 INSERT 失败):DELETE 也要回滚
R3 — FDW OPTIONS 切换(中)
FDW 外部表 fdw_app.member_retention_clue 是独立对象,名字不变;只需把 OPTIONS 中 schema_name 从 'public' 改成 'biz'。遗漏后果:ETL 库读到"对端 public 表已不存在",但当前没有 ETL 任务消费此外部表(BD_Manual_fdw_reverse_retention_clue.md § 4 已注明 "当前无 DWS 任务直接消费"),故影响面其实更小,但仍需更新以避免未来踩坑。
R6 — search_path 隐式依赖(中)
5 处不带 schema 前缀的 SQL(member_retention_clue.py 3 处、page_context.py 1 处、dispatcher.py 2 处)依赖 search_path 默认包含 public。迁移后 public 表被删,这些 SQL 会立刻报错;反向风险:如果 public 表保留过渡(如方案 B),SQL 会写到 public 的视图(间接到 biz),但 grep 看不出来,调试更难。结论:方案 A 在删 public 表的瞬间即暴露所有遗漏,更"硬"也更"快"。
R11 — 文档 / SPEC 不一致(中)
25+ 文档需同步。建议批量执行 sed -i 's/public\.member_retention_clue/biz.member_retention_clue/g'(PowerShell 等价 (Get-Content ... ) -replace ...),但要排除 _archived/ 目录和审计历史记录(保留历史原文)。
R13 — 测试库 vs 生产库结构差异(中)
测试库观测:无 RLS、无 FK、无触发器。但生产库可能存在:
- 手动加的额外索引(DBA 可能为查询优化加)
- 表级 GRANT(
app_reader/etl_user/app_user) - COMMENT
生产部署前必须执行:
-- 在生产 zqyy_app 中
\d+ public.member_retention_clue
SELECT grantee, privilege_type FROM information_schema.role_table_grants
WHERE table_schema = 'public' AND table_name = 'member_retention_clue';
SELECT obj_description('public.member_retention_clue'::regclass);
所有发现的额外属性(GRANT / COMMENT / 额外索引),需在 ALTER SCHEMA 后逐项确认是否随表带过去(PostgreSQL 12+:表级权限和注释会保留,索引随表迁移)。
七、推荐实施顺序(最小风险路径,方案 A)
阶段 1 — 预检(30 min)
-- 在生产 zqyy_app 中执行
-- A1. 当前结构快照
\d+ public.member_retention_clue
-- A2. 数据量
SELECT COUNT(*) AS rows, COUNT(DISTINCT site_id) AS sites,
MIN(recorded_at), MAX(recorded_at)
FROM public.member_retention_clue;
-- A3. GRANT 现状
SELECT grantee, privilege_type FROM information_schema.role_table_grants
WHERE table_schema = 'public' AND table_name = 'member_retention_clue';
-- A4. 是否有意外 RLS / 触发器(理论应为 0)
SELECT relrowsecurity FROM pg_class WHERE oid = 'public.member_retention_clue'::regclass;
SELECT tgname FROM pg_trigger WHERE tgrelid = 'public.member_retention_clue'::regclass AND NOT tgisinternal;
阶段 2 — 测试库迁移演练(2 h)
- 在 test_zqyy_app 执行
ALTER TABLE ... SET SCHEMA biz+ 序列同步 - 在 test_etl_feiqiu 执行
ALTER FOREIGN TABLE ... OPTIONS (SET schema_name 'biz') - 部署后端代码改动(11 处 SQL + page_context.py 顺修 Bug)到本地或 staging
- 跑一遍后端单元测试 + 集成测试
- 触发一次 App8 dispatch,确认 biz 表 DELETE + INSERT 正常
- 对照测试库写一份"产出报告":行数前后一致 / 序列衔接正常 / FDW 可读
阶段 3 — 文档批量同步(1 h)
按 § 一-1.9 文档清单,批量替换 public.member_retention_clue → biz.member_retention_clue,排除 _archived/ 和 docs/audit/changes/ 历史审计。
阶段 4 — 生产部署(30 min 窗口)
按方案 A 步骤 1-7 执行;持续 7 天观察告警。
阶段 5 — 收尾审计(30 min)
- 写
docs/audit/changes/2026-05-XX__schema-migrate-retention-clue-public-to-biz.md - 跑
python scripts/audit/gen_audit_dashboard.py - 把迁移 DDL 脚本归档到
db/zqyy_app/migrations/2026-05-XX__retention_clue_to_biz.sql
总工作量:~9 人时(测试库演练 4h + 生产部署 1h + 文档 + 审计 4h)
八、与其他 Wave 的关系
| Wave | 关系 | 建议 |
|---|---|---|
| Wave 0(已完成) | 04b-conflicts-P1-detail.md 第 P1-1 项即本任务的源 | 本评估即对 P1-1 反馈的响应 |
| Wave 1(小程序对齐) | 小程序零改动(仅引用字段名 retentionClues) | 不阻塞 |
| Wave 2(后端 API 对齐) | 后端 6 文件 11 处 SQL 改动属于 Wave 2 范围 | 建议合入 Wave 2 一起做,避免后端分两次部署 |
| Wave 3(数据库 / Schema 治理) | 本迁移属于"业务表归 biz"治理大方向 | 建议作为 Wave 3 的首发任务,先于其他迁移做(数据量最小、依赖最少,风险最低,可作为模板) |
| Wave 4(AI / ETL) | App8 dispatcher 受影响,但本任务自带验证 | 在 Wave 4 启动前完成本迁移可以让 App8 路径在新 schema 下稳定 1-2 周 |
| Wave 5(tenant-admin 完整化) | tenant_clues 路由 5 处改动属本任务范围 | 同 Wave 2 |
合并建议:把本任务和 Wave 2 后端 API 对齐合并为一个工作流(同一个分支同一次 PR),避免后端 SQL 改动分两次。Wave 3 治理的其他业务表迁移(如果有)以本任务为模板。
九、给 Neo 的决策清单
请逐项确认(每项打 √ 或 ✗ + 备注):
| # | 决策项 | 默认 | 备注 |
|---|---|---|---|
| D1 | 选择方案 A(一次性迁移)? | √ | 数据量小、无 FK、无 RLS、无小程序写入压力,方案 A 性价比最高 |
| D2 | 接受 9 人时工作量预估? | √ | 含测试库演练 + 生产部署 + 文档 + 审计 |
| D3 | 把本任务与 Wave 2 后端对齐合并到同一 PR? | √ | 避免后端代码分两次部署 |
| D4 | 顺手修复 page_context.py:243 的 created_at → recorded_at Bug? |
√ | 同文件同 SQL,零额外成本 |
| D5 | 顺手修复 schemas/member_retention_clue.py 的 ClueCategory.BASIC_INFO = "客户基础信息" 与 BD 手册 2026-03-08 对齐到 客户基础 不一致? |
⚠ | 登记但不在本任务范围,建议另起 task。若一并修需评估对历史数据的兼容(44 行测试数据是否有"客户基础信息"值需要迁移) |
| D6 | 接受迁移后 public.member_retention_clue 立即删除(非保留视图过渡)? |
√ | 与方案 A 的"硬切"一致,所有遗漏 SQL 立即暴露 |
| D7 | 迁移后保留旧表数据备份多久? | 7 天 | pg_dump -t public.member_retention_clue 单表备份,存到生产备份目录 |
| D8 | 是否在迁移同步引入 RLS(site_id 过滤)? | ✗ | 本任务不引入新 RLS,保持 G20 已知差异;RLS 治理另起任务(涉及全部 AI 表统一规划) |
| D9 | 是否需要先做"预检"小任务确认生产库无意外结构? | √ | 提交执行 § 七 阶段 1 SQL 后再决定动迁移 |
| D10 | 生产部署窗口选择? | 工作日 09:00 前 / 22:00 后 | App8 调度、tenant-admin 用户访问最少时段 |
十、附录:grep 校验脚本
迁移完成后用以下命令自检遗漏:
# 后端代码:应为 0 命中
grep -rEn "(public\.member_retention_clue|FROM member_retention_clue|INTO member_retention_clue|UPDATE member_retention_clue|DELETE.+member_retention_clue)" \
apps/backend/app
# 文档(排除归档和审计历史):应为 0 命中
grep -rEn "public\.member_retention_clue" docs/ \
--exclude-dir=_archived --exclude-dir=audit
# DDL(应仅出现在 biz.sql 中)
grep -rEn "member_retention_clue" db/zqyy_app/schemas/
# FDW(应仅 schema_name 'biz')
grep -En "schema_name" db/fdw/setup_fdw_reverse*.sql
十一、附:本评估自身的限制说明
- 未访问生产库:仅基于测试库
test_zqyy_app推断;生产库结构差异通过 § 七 阶段 1 预检 SQL 排查 - 未跑 staging 全链路:本评估为只读调研,方案 A 阶段 2 必须在 staging 跑通后才能进入生产
- 未量化 App8 触发频率:dispatcher.py 写入路径风险等级标"高"是基于"幂等性是核心特性"的保守判断;如果 App8 实际触发频率 < 1 次/天,迁移期间踩坑概率极低
- 未触及 RLS 引入:本评估遵循 Neo 反馈"保证规范性"的范围,仅做 schema 迁移,不并入 RLS 治理(RLS 治理建议另起任务统筹 G20)
结论:迁移 public.member_retention_clue → biz.member_retention_clue 工程上完全可行,推荐方案 A(一次性迁移)+ 与 Wave 2 后端对齐合并 PR。最高风险落在后端 11 处 SQL 直引,通过 grep 自检 + staging 全链路验证可控。预估 9 人时。