建立项目级标杆文档 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
20 KiB
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
问题:
- 数据特征(消费/充值/EWMA)的 SQL 查询用
as_of参数化,具备回测能力; - 参数加载用
CURRENT_DATE硬编码,沙箱模式下拿到的是"今天生效的参数",不是 sandbox_date 当时生效的参数。 - 视图层
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_taskscron(全局调度)→ 设计共识保留真实时钟,不需要。
四、四个核心问题的答案
Q1:A 方案 vs B 方案
答案:B 方案(用 2026-03-01 当时生效的参数)。
理由:
- P20 § 1.2 沙箱设计目标是"假装今天是某个历史日期",参数是业务规则的一部分,用今天参数 = 历史现场不完整。
- 后端走 v_* 视图已经隐式选了 B,如果 ETL 用 A,会出现"小程序看到的分数(后端取展示分)" 与 "重跑 ETL 后实际写入的分数"不一致。
- 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 直觉的"每日快照"更优:
- SCD2 区间
effective_from / effective_to:5 张关键 cfg 表已有(2.2 矩阵)。 - P20 视图层已经在
app.v_cfg_*用app.business_date_now()切片(db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql:710-782)。 - 业务事实表(如
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)。
实施步骤:
-
DB 侧:确认 5 个
app.v_cfg_*视图当前定义已经按effective_from <= app.business_date_now() AND effective_to >= app.business_date_now()切片(20260502 迁移已完成)。无需新增迁移。 -
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 处):
apps/etl/connectors/feiqiu/tasks/dws/index/base_index_task.py:335-342:FROM dws.cfg_index_parameters→FROM 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(种子任务):写入仍然走裸表(写入永远是真实操作),无需改。
-
测试:
- 在测试库
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 必须一致。
- 在测试库
-
审计:
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 / 修订意见:
-
B 方案确认:沙箱模式下,SPI/绩效/工资任务应使用 sandbox_date 当时生效的参数版本(而非今天的最新参数)。 → GO / NO-GO
-
方案 1 vs 方案 3:推荐方案 1(SCD2 + 视图归一,0.5 天),不推荐方案 3(每日快照表,3-5 天)。 → 同意 / 切方案 / 其他
-
NULL 兼容性补丁前置:
v_cfg_index_parameters视图 NULL 行被过滤是真实 bug,建议作为 P0 前置修复(独立审计)。 → GO / 合并到 T17 一起处理 -
biz.cfg_task_generator_params 是否要 SCD2:当前无版本机制,沙箱影响弱。建议优先级 P3,本轮不做。 → 同意 / 现在就要
-
AI prompt 版本:沙箱演示中切换 prompt 工程版本时,跨切换的 ai_run_logs 不可比。建议增加
prompt_version字段标识(后续单独 SPEC),不快照 prompt 全文。 → 同意 / 其他 -
是否直接产出 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 主文,不动代码与数据库,仅产出决策建议。