Files
Neo-ZQYY/docs/_overview/04a-feedback/P0-1-sandbox-snapshot-design.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

20 KiB
Raw Blame History

P0-1 沙箱与参数快照机制设计调研

日期:2026-05-04 触发:Neo 在 P0-1 反馈中提出"沙箱模式下 SPI/全部参数是否每天都该有快照" 调研者:沙箱-参数快照专项子代理 状态:调研 + Patch 建议草案(不直接修改 P20 SPEC,不动代码 / 数据库) 调研边界:仅读测试库与仓库代码,产出文字结论


一、问题定义

沙箱回放历史日期(例如 sandbox_date = 2026-03-01)时,SPI 算法、绩效档位、奖金规则、助教等级定价等"门店级配置参数"应当使用哪一版?

  • A 方案(取最新):始终用今天的参数。简单,但 2026-03-01 当时跑出的真实分数无法在沙箱中复现 —— 沙箱失去"还原历史现场"的意义。
  • B 方案(取历史生效版):用 2026-03-01 当时生效的参数。准确,要求所有 cfg_* 表都有可按日期切片的版本机制,并且所有读取入口都按业务日过滤

Neo 直觉:既然沙箱机制存在,SPI 参数和全部参数都应该每天都有快照吧?

调研结论先行:Neo 的直觉方向正确,但"每天快照"是次优方案;最优方案是SCD2 区间(已有)+ 让所有读取入口都按业务日过滤(未做)。当前 P20 已经在视图层为 4 个 cfg_* 表加了业务日上界,但 ETL 层的 SPI/绩效/工资任务直接读 dws.cfg_* 裸表绕过了视图,这是 P20 的隐藏裂缝。


二、当前实现状态

2.1 cfg_index_parameters 表结构(权威 DDL db/etl_feiqiu/schemas/dws.sql:93-103)

CREATE TABLE dws.cfg_index_parameters (
    param_id        integer PK,
    index_type      varchar(50) NOT NULL,      -- WBI/NCI/RS/OS/MS/ML/SPI
    param_name      varchar(100) NOT NULL,
    param_value     numeric(14,6) NOT NULL,
    description     text,
    effective_from  date DEFAULT CURRENT_DATE NOT NULL,
    effective_to    date,                      -- 可空 = 至今仍生效
    created_at      timestamptz NOT NULL,
    updated_at      timestamptz NOT NULL
);
ALTER TABLE ... UNIQUE (index_type, param_name, effective_from);
CREATE INDEX idx_cfg_index_params_effective ON ... (effective_from, effective_to);

结论:已经具备 SCD2 区间能力。同名 (index_type, param_name) 可以有多行,通过 effective_from / effective_to 区间区分。无需新增 snapshot_date 列。

2.2 其他 cfg_* 表盘点

库.Schema 区间字段 UNIQUE 约束 RLS 视图(app schema) 视图业务日上界
cfg_index_parameters etl_feiqiu.dws effective_from / effective_to (index_type, param_name, effective_from) app.v_cfg_index_parameters 已加(20260502 迁移)
cfg_assistant_level_price etl_feiqiu.dws effective_from / effective_to (level_code, effective_from) app.v_cfg_assistant_level_price 已加
cfg_performance_tier etl_feiqiu.dws effective_from / effective_to (tier_code, effective_from) app.v_cfg_performance_tier 已加
cfg_bonus_rules etl_feiqiu.dws effective_from / effective_to (rule_type, rule_code, effective_from) app.v_cfg_bonus_rules 已加
cfg_skill_type etl_feiqiu.dws (skill_id) 未单独建视图(is_active=true)
cfg_area_category etl_feiqiu.dws (source_area_name) 未单独建视图
biz.cfg_task_generator_params zqyy_app.biz 无(只有 updated_at + updated_by) (site_id, param_key) 无 RLS,后端直读

结论:7 张配置表中,5 张算法/财务相关的(SPI/RS/OS 参数、定价、档位、奖金)已经有 SCD2 区间机制cfg_skill_type / cfg_area_category 是"枚举映射型"配置(技能 ID → 课程类型,区域名 → 分类),变更频率极低且无版本概念;biz.cfg_task_generator_params 是任务引擎运行时参数,只有最新值。

2.3 SPI 任务读参数的实际方式(关键裂缝)

apps/etl/connectors/feiqiu/tasks/dws/index/base_index_task.py:303-360:

def load_index_parameters(self, index_type=None, force_reload=False):
    ...
    sql = """
        SELECT param_name, param_value
        FROM dws.cfg_index_parameters       -- 直接读裸表,不走 app.v_*
        WHERE index_type = %s
          AND effective_from <= CURRENT_DATE          -- 用 CURRENT_DATE,不用 as_of
          AND (effective_to IS NULL OR effective_to >= CURRENT_DATE)
        ORDER BY effective_from DESC
    """
    rows = self.db.query(sql, (index_type,))
    ...

spending_power_index_task.py:178-186 同时调用:

as_of = (context.as_of_date if context and context.as_of_date else None) or _dt.now(self.tz)
db_params = self.load_index_parameters('SPI')                 # 不传 as_of
features = self._extract_spending_features(site_id, params, as_of=as_of)   # 传 as_of

问题:

  1. 数据特征(消费/充值/EWMA)的 SQL 查询用 as_of 参数化,具备回测能力;
  2. 参数加载用 CURRENT_DATE 硬编码,沙箱模式下拿到的是"今天生效的参数",不是 sandbox_date 当时生效的参数。
  3. 视图层 app.v_cfg_index_parameters 已经按 app.business_date_now() 加上界,但 ETL 任务直接读 dws.cfg_index_parameters 裸表绕过了视图,GUC 不生效。

同样问题存在于 base_dws_task.py:540-581_load_perf_tiers / _load_level_prices / _load_bonus_rules(绩效档位/工资任务用),这三个方法甚至连 effective_from WHERE 都没有,把所有历史区间的行全取出来,再让 Python 代码自己挑——一旦表里有重复 tier_code/level_code 的多区间数据,行为依赖 ORDER BY 和 Python 侧逻辑。

2.4 后端读参数的方式(已对接 P20)

后端通过 app.v_cfg_* 视图读取(如 tenant_users.py 走 v_cfg_assistant_level_price),fdw_queries._fdw_context 在事务内 SET LOCAL app.current_business_date = sandbox_date,视图自动按 business_date_now() 切片。后端侧已经是 B 方案


三、参数与沙箱关联性矩阵

参数表 是否影响沙箱回放 当前是否有快照能力 后端读路径 ETL 读路径 是否需要快照
cfg_index_parameters (SPI/RS/OS/MS/ML/WBI/NCI 算法权重) 是(分数会因参数变化而不同) 有 SCD2 走 v_* 已切片 裸表绕过 必须
cfg_assistant_level_price (助教等级定价) 是(月薪计算结果不同) 有 SCD2 走 v_* 已切片 裸表绕过 必须
cfg_performance_tier (绩效档位区间) 是(档位归属不同) 有 SCD2 走 v_* 已切片 裸表绕过 必须
cfg_bonus_rules (奖金规则) 是(奖金金额不同) 有 SCD2 走 v_* 已切片 裸表绕过 必须
cfg_skill_type (技能 → 课程类型映射) 否(枚举映射,业务定义不会因日期变) 直读 is_active=true 直读 不必
cfg_area_category (区域名 → 分类) 否(同上) 直读 直读 不必
biz.cfg_task_generator_params (任务引擎冷启动天数等) 弱影响(沙箱演示新功能时一致即可) 后端直读 可选(优先级 P3)

额外参数源:

  • AI prompt 模板版本(后端代码内常量,不在 DB)→ 沙箱演示新 prompt 时跨切换会有差异,但属于"prompt 工程版本"不属于业务参数。优先级 P3,通过 prompt_version 字段记录即可,无需快照。
  • runtime_context 自身配置(biz.site_runtime_context)→ 是状态本身,不需要快照。
  • biz.scheduled_tasks cron(全局调度)→ 设计共识保留真实时钟,不需要。

四、四个核心问题的答案

Q1:A 方案 vs B 方案

答案:B 方案(用 2026-03-01 当时生效的参数)。

理由:

  1. P20 § 1.2 沙箱设计目标是"假装今天是某个历史日期",参数是业务规则的一部分,用今天参数 = 历史现场不完整。
  2. 后端走 v_* 视图已经隐式选了 B,如果 ETL 用 A,会出现"小程序看到的分数(后端取展示分)" 与 "重跑 ETL 后实际写入的分数"不一致。
  3. SCD2 区间已经存在,迁移成本极低,只需改读取 SQL 的 WHERE 子句。

Q2:B 方案的实现路径

推荐 B-1 + 视图归一(见 § 五):利用现有 SCD2 区间,把所有 cfg_* 读取入口统一到 app.v_cfg_* 视图(由 app.business_date_now() 自动切片),禁止 ETL 任务直读 dws.cfg_* 裸表。

为什么不选 B-2(每日快照表):

  • 写入成本高(每天每门店复制全量参数行)。
  • 实际参数变更频率极低(SPI 27 个参数自上线以来基本未变;BD 手册显示 cfg_assistant_level_price 历史只有 1-2 次区间切换),99% 快照都是冗余。
  • SCD2 区间在数学上等价于"按需快照",且空间复杂度 O(变更次数) 而非 O(天数 × 门店数)。

为什么不选 B-3(变更日志重建):

  • 重建逻辑复杂,需要在每个调用点应用变更日志,容易遗漏。
  • SCD2 已经做了同样的事,无需再发明。

Q3:哪些参数需要"快照"(SCD2 切片),哪些不需要

需要切片(已具备能力,只是读取入口未对齐):

  • cfg_index_parameters(7 类指数算法参数)
  • cfg_assistant_level_price(助教等级定价)
  • cfg_performance_tier(绩效档位区间)
  • cfg_bonus_rules(奖金规则)

不需要切片:

  • cfg_skill_type(技能 → 课程类型),纯枚举映射,变更等同重命名。
  • cfg_area_category(区域 → 分类),纯枚举映射。
  • biz.cfg_task_generator_params(冷启动天数等),沙箱场景对其敏感度极低;若未来需要可补 SCD2,优先级 P3。
  • AI prompt 版本(代码内常量),通过 prompt_version 标识即可。
  • 调度 cron / runtime_context 自身 / 时间戳列(P20 § 1.3 已有设计共识)。

Q4:是否已存在类似机制

存在,且比 Neo 直觉的"每日快照"更优:

  1. SCD2 区间 effective_from / effective_to:5 张关键 cfg 表已有(2.2 矩阵)。
  2. P20 视图层已经在 app.v_cfg_*app.business_date_now() 切片(db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql:710-782)。
  3. 业务事实表(如 dws_member_spending_power_index)按 business_date / calc_time 已经天然形成"每日产出快照",这是事实层快照,不是参数快照。

漏的不是机制,是读取入口的统一


五、推荐设计方案

方案对比

方案 参数侧改造 读取入口改造 写入侧改造 工作量 推荐度
方案 1:统一视图入口 无(SCD2 已有) ETL 全部 cfg 读取改走 app.v_cfg_* 视图 0.5 天(改 2-3 处 SQL) 首选
方案 2:函数显式 as_of load_index_parameters / _load_*as_of 参数 1 天(签名改造 + 调用方 7+ 处) 备选
方案 3:每日快照表 新增 dws.cfg_*_snapshot(snapshot_date, ...) 6 张 改读 snapshot 表 新增日度生成 job 3-5 天 + 持续维护 不推荐

推荐方案 1:统一视图入口(B-1 + 视图归一)

核心思想:cfg_* 表的"按业务日切片"语义封装在 app.v_cfg_* 视图里,所有读取入口(后端 + ETL + 后端 FDW)统一走视图。GUC app.current_business_date 由 ETL Loader / 任务引擎在事务起始处下发,沙箱模式下下发 sandbox_date,live 模式下不下发(回退 CURRENT_DATE)。

实施步骤:

  1. DB 侧:确认 5 个 app.v_cfg_* 视图当前定义已经按 effective_from <= app.business_date_now() AND effective_to >= app.business_date_now() 切片(20260502 迁移已完成)。无需新增迁移。

  2. ETL Loader / 任务引擎入口:在 task_engine.py / flow_runner 跑 dws 任务前,先按当前门店读 biz.site_runtime_context.mode + sandbox_date,在事务内执行:

    SET LOCAL app.current_site_id = '<site_id>';
    SET LOCAL app.current_business_date = '<sandbox_date or CURRENT_DATE>';
    SET LOCAL app.current_runtime_mode = 'live' | 'sandbox';
    

    live 模式下也建议显式 SET LOCAL app.current_business_date = CURRENT_DATE::text,使语义对称(无侧效应,函数已 STABLE)。

  3. 代码侧改造(3 处):

    • apps/etl/connectors/feiqiu/tasks/dws/index/base_index_task.py:335-342: FROM dws.cfg_index_parametersFROM app.v_cfg_index_parameters,移除 effective_from <= CURRENT_DATE AND effective_to ... WHERE(视图已切片)。
    • apps/etl/connectors/feiqiu/tasks/dws/base_dws_task.py:543-581: _load_perf_tiers / _load_level_prices / _load_bonus_rules 改为 FROM app.v_cfg_*,删除不必要的 ORDER BY effective_from DESC(视图已只返回当前生效行)。
    • apps/etl/connectors/feiqiu/tasks/utility/seed_dws_config_task.py(种子任务):写入仍然走裸表(写入永远是真实操作),无需改。
  4. 测试:

    • 在测试库 test_etl_feiqiu 切沙箱到 sandbox_date = 2026-03-01,跑 SPI,验证读到的参数版本与裸表 WHERE effective_from <= '2026-03-01' AND effective_to >= '2026-03-01' 结果一致。
    • 切回 live,跑 SPI,验证读到的参数 = CURRENT_DATE 切片结果。
    • 同时跑两次(live → sandbox → live),三次结果中两次 live 必须一致。
  5. 审计:docs/audit/changes/2026-MM-DD__sandbox_param_view_unify.md + 更新 BD_Manual。

工作量预估:0.5 天(代码 ≤ 3 文件 / ≤ 30 行 + 1 篇审计 + 测试)。

风险:

  • 视图层 SQL 复杂度上升微乎其微(简单 WHERE 子句)。
  • 历史数据兼容性:cfg_* 表中 effective_to 应为 '9999-12-31' 表示"至今仍生效",当前 cfg_index_parameters 默认 effective_to 是 NULL,而 v_cfg_index_parameters WHERE 写的是 effective_to >= app.business_date_now()——NULL 会被过滤掉!这是一个真实存在的视图 bug,需要在执行方案 1 前先修视图(改为 (effective_to IS NULL OR effective_to >= app.business_date_now()))。

视图 NULL 兼容性补丁(前置必做)

db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql:782 当前:

WHERE effective_from <= app.business_date_now()
  AND effective_to >= app.business_date_now();   -- NULL 行被过滤

应改为:

WHERE effective_from <= app.business_date_now()
  AND (effective_to IS NULL OR effective_to >= app.business_date_now());

这条修复独立于沙箱方案,任何"effective_to 留空" 的行都需要它(cfg_index_parameters.effective_to 是 nullable)。建议作为方案 1 的前置 P0 修复。


六、P20 SPEC Patch 建议(不实际修改,仅列出"应在 § X 加入以下内容")

Patch P20-A:§ 1.3 沙箱不影响项 → 调整描述

当前 § 1.3 列出 7 项不被沙箱影响。建议新增一段说明澄清 cfg_* 配置:

### 1.4 沙箱影响项(配置参数版本)

下列配置表参与沙箱"业务日切片"语义,沙箱模式下读取 sandbox_date 当时生效的版本:

- cfg_index_parameters(指数算法权重 SPI/RS/OS/MS/ML/WBI/NCI)
- cfg_assistant_level_price(助教等级定价)
- cfg_performance_tier(绩效档位区间)
- cfg_bonus_rules(奖金规则)

下列不受沙箱影响:

- cfg_skill_type / cfg_area_category(纯枚举映射)
- biz.cfg_task_generator_params(任务引擎运行时参数)
- AI prompt 版本(代码常量)

Patch P20-B:§ 3.5 RLS 视图业务日上界 → 修复 NULL 兼容性

§ 3.5 当前列 v_cfg_* 4 个视图"effective_from 与 effective_to 双向夹住"。应补充注释:

注:effective_to IS NULL 表示"至今仍生效",视图 WHERE 必须写为
   `(effective_to IS NULL OR effective_to >= app.business_date_now())`,
   不能写成 `effective_to >= app.business_date_now()`(NULL 会被过滤)。
   当前 20260502 迁移生成的 4 个 v_cfg_* 视图存在此 bug,需在 P0 修复。

Patch P20-C:新增 § 5.6 ETL 库读取约定

P20 § 5 当前覆盖了"后端服务层 / ETL 视图层 / 小程序 / AI 提示词 / admin-web",没有覆盖 ETL 任务自身。建议新增:

### 5.6 ETL 任务读取约定

ETL 任务(task_engine 调度的 DWS 计算任务)对 cfg_* 配置表的读取必须遵守:

1. 入口必须是 `app.v_cfg_*` 视图,禁止直读 `dws.cfg_*` 裸表(参数加载场景)。
   - 例外:种子写入任务(seed_dws_config_task)写入裸表是必需的。
2. 任务事务开启后,`base_dws_task` 应在 SQL 执行前下发:
   - `SET LOCAL app.current_site_id`
   - `SET LOCAL app.current_business_date`(live 用 CURRENT_DATE,sandbox 用 sandbox_date)
   - `SET LOCAL app.current_runtime_mode`
3. 任务上下文(TaskContext)必须能从 `biz.site_runtime_context` 读到当前模式;
   live 模式下回退 live 默认行为。

当前已知未对齐(2026-05-04):
- `base_index_task.load_index_parameters` 直读 dws.cfg_index_parameters,需改走 v_*
- `base_dws_task._load_perf_tiers / _load_level_prices / _load_bonus_rules` 直读裸表,需改走 v_*

Patch P20-D:§ 8 验收标准 → 新增 AC14 / AC15

| AC14 | sandbox 模式跑 SPI,读到的 cfg_index_parameters 版本 = sandbox_date 当时生效版本 | SQL: 对比 ETL 任务日志中 params dump 与 SELECT * FROM v_cfg_index_parameters 直查结果 |
| AC15 | sandbox 模式跑工资任务,读到的 cfg_performance_tier / cfg_bonus_rules / cfg_assistant_level_price 版本 = sandbox_date 当时生效版本 | 同上 |

Patch P20-E:§ 11.2 已知 hack → 新增条目

- ETL DWS 任务直读 dws.cfg_* 裸表绕过 v_* 视图,沙箱模式下参数版本不切片 — 详见 P0-1-sandbox-snapshot-design.md
- v_cfg_index_parameters 视图 effective_to NULL 兼容性 bug — 详见 P0-1-sandbox-snapshot-design.md § 五

Patch P20-F:§ 12 任务清单 → 新增 T16 / T17

- [ ] T16:修复 4 个 v_cfg_* 视图 effective_to NULL 兼容性(P0,前置)
- [ ] T17:ETL DWS 任务参数读取统一走 v_cfg_* 视图(P1,沙箱方案 1)

七、给 Neo 的决策清单

请就以下 6 项给出 GO / NO-GO / 修订意见:

  1. B 方案确认:沙箱模式下,SPI/绩效/工资任务应使用 sandbox_date 当时生效的参数版本(而非今天的最新参数)。 → GO / NO-GO

  2. 方案 1 vs 方案 3:推荐方案 1(SCD2 + 视图归一,0.5 天),不推荐方案 3(每日快照表,3-5 天)。 → 同意 / 切方案 / 其他

  3. NULL 兼容性补丁前置:v_cfg_index_parameters 视图 NULL 行被过滤是真实 bug,建议作为 P0 前置修复(独立审计)。 → GO / 合并到 T17 一起处理

  4. biz.cfg_task_generator_params 是否要 SCD2:当前无版本机制,沙箱影响弱。建议优先级 P3,本轮不做。 → 同意 / 现在就要

  5. AI prompt 版本:沙箱演示中切换 prompt 工程版本时,跨切换的 ai_run_logs 不可比。建议增加 prompt_version 字段标识(后续单独 SPEC),不快照 prompt 全文。 → 同意 / 其他

  6. 是否直接产出 Patch SPEC:本调研只列出 P20 应改章节,不实际修改。如果决策清单 1-5 项通过,是否授权按 § 六 patch 直接修改 docs/prd/specs/P20-runtime-context-sandbox.md,并产出 0.5 天的代码+测试改造? → GO / 先讨论


八、关键文件路径(便于 Neo 复核)

  • c:\Project\NeoZQYY\db\etl_feiqiu\schemas\dws.sql:93-103 — cfg_index_parameters 表定义
  • c:\Project\NeoZQYY\db\etl_feiqiu\migrations\20260502__rls_views_business_date_upper_bound.sql:710-782 — 4 个 v_cfg_* 视图(含 NULL bug)
  • c:\Project\NeoZQYY\apps\etl\connectors\feiqiu\tasks\dws\index\base_index_task.py:303-360 — load_index_parameters 直读裸表
  • c:\Project\NeoZQYY\apps\etl\connectors\feiqiu\tasks\dws\base_dws_task.py:540-581 — 工资任务三个 load* 直读裸表
  • c:\Project\NeoZQYY\apps\etl\connectors\feiqiu\tasks\dws\index\spending_power_index_task.py:178-186 — as_of 传入数据查询但参数加载未传
  • c:\Project\NeoZQYY\docs\prd\specs\P20-runtime-context-sandbox.md — P20 SPEC 主体
  • c:\Project\NeoZQYY\docs\_overview\04a-feedback\P0-1-SPI-research.md — P0-1 step1 调研(SPI 27 参数清单)
  • c:\Project\NeoZQYY\docs\_overview\04a-feedback\P0-7-runtime-context-todos.md — 跨模块沙箱 todos

本调研产出由 Claude Opus 4.7(沙箱-参数快照专项子代理)生成于 2026-05-04, 不修改 P20 SPEC 主文,不动代码与数据库,仅产出决策建议。