feat(db,docs): F1-5b MP-2 prep + 沙箱时光机模块 spec (W1)

MP-2 经 4 轮调研 + Neo 反馈,采纳方案 C(推迟到 F1-6 沙箱时光机阶段 B):
- 第 1 轮原方案 D(双口径) → 第 2 轮 D'(单口径)
- 第 3 轮 Neo 架构纠正:不读 DWD,走 Core/DWS/app
- 第 4 轮 DWS 视图靠谱性审计:dws_assistant_daily_detail 是计费明细
  (ledger_amount),不是助教工资(gross_salary 需等级时薪 + 抽成
  + 罚分),且缺 effective_hours / work_days
- 结论:MP-2 真正实施需要新建 dws_assistant_daily_salary 表(ETL
  改造),跟其他 14 个 P1 指标一起做更高效 → 推迟到 F1-6

本次 Wave B 只做 prep:DB schema + 模块 spec + tasks.md 状态调整。

DB 迁移(zqyy_app):
- db/zqyy_app/migrations/20260505__add_effective_date_for_excel_adjustments.sql
- 3 张 Excel 暂存表(全空,Neo 确认尚无 Excel 上传)ADD COLUMN
  effective_date DATE NOT NULL(无 DEFAULT,强制未来 Excel 上传必须带):
  * biz.salary_adjustments(助教薪资扣款/奖励)
  * biz.stg_finance_expense(月度支出)
  * biz.stg_platform_income(平台结算收入)
- 3 个复合索引 (site_id, effective_date) 支持后续 daily 截断查询
- biz.stg_recharge_commission 已有 recharge_date,无需改造

测试库执行 + 5/5 校验 PASS:
- 字段存在(NOT NULL DATE 无 default)
- 复合索引存在 + 列序正确
- 字段注释含 'F1-5b MP-2 prep'
- INSERT 不带 effective_date 触发 NotNullViolation

docs/database/ 同步:
- docs/database/changes/2026-05-05__add_effective_date_for_excel_adjustments.md
  完整变更说明 + 兼容性 + 回滚 + 5 条校验 SQL + 正式库执行说明

沙箱时光机模块 spec(主干任务排期登记):
- docs/_overview/sandbox-replay-engine-spec.md
- 22 个相关指标分 P1/P2/P3 优先级:
  * P1 14 项(daily 视图已有,后端切换)
  * P2 5 项(算法重算,含 MP-2 完整 daily salary)
  * P3 3 项(状态算法 + sandbox_audit_log 用户行为)
- 4 阶段实施路径:
  * 阶段 0(本次 prep)
  * 阶段 A(F1-5a/b 已完成)
  * 阶段 B(F1-6,2-3 周)— MP-2 真正实施在此
  * 阶段 C(F1-7+,1-2 周)
- sandbox_replay 模块结构 + runtime_aware decorator 接口契约
- 性能 + 测试 + 前置依赖清单

F1-5b-tasks.md 状态调整:
- §4.3 顺序 15:MP-2 从"待开始/C4" → "延期 F1-6"
- §6 进度表 MP-2 行同步标"延期 F1-6 + 改方向说明"
- 关联到 mp2_prep.md 审计

业务影响:
- board-coach sandbox 行为暂遗留(F1-6 解决)
- 旧 Excel 模板上传将因 NOT NULL 失败,需 F1-6 同期 ETL UI 改造 +
  操作员培训
- 跨页面已 audit:board-finance / customer-records / coach-service-records
  / customer-service-records 等已合规(F1-5b A1/A3 + MP-1/3/5 收益)

审计:docs/audit/changes/2026-05-05__wave1_f1_5b_mp2_prep.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-05 22:12:22 +08:00
parent 16c6fb0d3b
commit 1e803e23cd
5 changed files with 536 additions and 2 deletions

View File

@@ -0,0 +1,168 @@
# 沙箱时光机引擎 (Sandbox Replay Engine) — 模块 Spec
> 版本:v1.0 · 创建日期:2026-05-05
> 决策来源:F1-5b Wave B MP-2 调研收尾(Neo 同意分阶段实现,加入主干任务排期)
> 主要落地依赖:F1-5a runtime_context 框架 + F1-5b 业务日上界裁剪基础设施
>
> **状态**:Spec 阶段(未启动实施)
## 一、设计意图
P20 沙箱(`runtime_mode='sandbox' + sandbox_business_date`)的核心承诺是 **"设定某个历史日期 X 后,系统所有数据/统计/AI 生成内容呈现的状态都是 X 那天的样子"**,即"时光机"语义。
当前 F1-5a/F1-5b 已建立基础设施,但**业务读取层只覆盖了"daily 累计型"指标**(财务流水 / 服务记录),**未覆盖"月度结算型 / 状态算法型 / Excel 修正型"指标**。沙箱设到 4-20 时,部分页面仍能"看见未来"或显示已知最终结果,违反时光机语义。
本模块旨在**统一所有业务读取按 sandbox_business_date 重算**,达成完整时光机体验。
## 二、当前已就位的基础设施(继承)
| 层 | 已做 | Wave / Commit |
|----|------|--------------|
| **RuntimeContext 框架** | site_runtime_context 表 + apply_runtime_session_vars() | F1-5a 421e193 |
| **数据写入隔离** | ai_run_logs / coach_tasks / 各业务写入表带 runtime_mode + sandbox_instance_id | F1-5a + F1-5b A1 af02446 |
| **app 视图业务日上界裁剪** | 39 个 app.v_* 视图加 `WHERE stat_date <= business_date_now()`(2026-05-02 迁移) | 2026-05-02 ETL 迁移 |
| **后端读取**(daily 累计型指标) | board-finance / customer-detail / customer-records / coach-service-records 等 | F1-5b MP-1/3/5 |
| **后端读取**(权限路由) | manager 角色权限隔离 | F1-5b BE-1 |
| **admin-web sandbox 透出** | UI-1/2/3/4/5 全套 runtime 字段 + 提示条 + 全局徽章 | F1-5b Wave A/B |
| **Excel 修正表 schema 准备** | salary_adjustments / stg_finance_expense / stg_platform_income 加 effective_date NOT NULL | F1-5b MP-2 prep(本次) |
## 三、缺失能力清单(本模块覆盖范围)
22 个指标按复杂度 + 优先级分:
### P1 — Daily 视图已有,后端 service 切换即可(14 项,S 复杂度)
| # | 指标 | 当前数据源 | 目标 daily 视图 | 重算逻辑 |
|---|------|-----------|----------------|---------|
| 1 | 会员储值卡余额 | dws_member_balance_snapshot(状态) | dws_member_balance_change daily 累计 | SUM(收入-支出) WHERE date<=B |
| 2 | 60 天消费 | dws_member_consumption_summary | dws_member_consumption_daily | SUM 60 day window 终点为 B |
| 3 | 累计消费总额 | 同上 | 同上 | SUM all WHERE date<=B |
| 4 | 距上次到店天数 | 状态字段 | dwd 消费记录 | B - MAX(visit_date WHERE<=B) |
| 5 | 累计服务客户数(助教) | dws_assistant_customer_stats | daily 累计 | COUNT DISTINCT member WHERE date<=B |
| 6 | 助教等级 | dws_assistant_daily_detail.assistant_level_code | 同表 | SELECT WHERE stat_date=B |
| 7 | 月度课时(助教) | dws_assistant_daily_detail.base_hours 等 | 同表月度聚合 | SUM WHERE month_of(B) AND stat_date<=B |
| 8 | 月度计费金额(助教) | dws_assistant_daily_detail.total_ledger_amount | 同上 | SUM WHERE month_of(B) AND stat_date<=B |
| 9 | 门店月度财务 | dws_finance_daily_summary | 同表(已实现 MP-1) | ✓ 已完成 |
| 10 | 月度新增会员 | dws_member_*_summary | dws_member_daily | COUNT WHERE join_date<=B AND month_of(B) |
| 11 | 月度流失会员 | 同上 | 同上 | COUNT WHERE last_visit<B-30 AND month_of(B) |
| 12 | 累计交易笔数 | dws_order_summary | daily | COUNT WHERE date<=B |
| 13 | 累计 GMV | dws_finance_daily_summary.gross_amount | 同上 | SUM WHERE date<=B |
| 14 | AI 缓存命中率 | sandbox_instance_id 隔离 | F1-5a 已覆盖 | ✓ 已完成 |
### P2 — 算法重算(5 项,M 复杂度)
| # | 指标 | 复杂度 | 难点 |
|---|------|------|------|
| 15 | 关系指数 RS | M | RS 涉及窗口期 + 衰减函数,daily 重算需要重做累计逻辑 |
| 16 | 客户黏性指数 | M | 类似 RS,有时间衰减 |
| 17 | 助教月薪(完整 daily salary 含罚分) | M-L | **MP-2 真正实施在此**,需要新建 dws_assistant_daily_salary 表 |
| 18 | 任务完成率(coach_tasks 累计) | M | 需 coach_tasks 加 dws 层 daily 聚合视图 |
| 19 | Excel 修正(扣款/奖励/支出/收入) | M | 依赖 effective_date(F1-5b prep 已做),后端 SQL 加截断 |
### P3 — 状态算法依赖累计(3 项,L 复杂度)
| # | 指标 | 难点 |
|---|------|------|
| 20 | 门店等级评级 | 评级算法依赖累计 KPI 反推,需要重新建模 |
| 21 | 助教星级 | 类似门店等级,涉及多月 KPI 综合 |
| 22 | 用户操作行为日志 | **完全未覆盖**(F1-5a 仅写入隔离,没有"用户行为审计表");需新建 sandbox_audit_log |
## 四、实施分阶段(主干任务排期)
| 阶段 | 内容 | 工作量 | 时机 |
|------|------|------|------|
| **阶段 0(F1-5b prep,本次)** | Excel 修正表加 effective_date schema + 沙箱时光机 spec 文档 | 1.5h | **本次 F1-5b Wave B 完成** |
| **阶段 A(F1-5b 已完成)** | RuntimeContext 框架 + app 视图 business_date 上界 + daily 累计型指标 9 项 | — | F1-5a + F1-5b 已完成 |
| **阶段 B(F1-6)** | 14 个 P1 指标 service 层切换 daily 累计 + 5 个 P2 指标(含 MP-2 完整 daily salary) | 2-3 周 | F1-6 |
| **阶段 C(F1-7+)** | 3 个 P3 指标 + sandbox_audit_log 用户行为审计 | 1-2 周 | F1-7 长期 |
## 五、阶段 B 实施模式建议
### 5.1 sandbox_replay 模块结构
```
apps/backend/app/services/sandbox_replay/
├── __init__.py # runtime_aware decorator
├── balance_replay.py # 会员余额(P1-1)
├── consumption_replay.py # 消费累计(P1-2/3/4/12/13)
├── assistant_metrics_replay.py # 助教课时/收入/客户(P1-5/6/7/8)
├── member_lifecycle_replay.py # 月度新增/流失(P1-10/11)
├── salary_replay.py # MP-2 完整 daily salary(P2-17,需新 dws 表)
├── adjustments_replay.py # Excel 修正截断(P2-19)
├── tasks_replay.py # 任务完成率(P2-18)
├── rs_replay.py # RS 算法重算(P2-15)
└── intimacy_replay.py # 客户黏性(P2-16)
```
### 5.2 接口契约(runtime_aware decorator)
```python
@runtime_aware(metric='member_balance')
def get_member_balance(site_id: int, member_id: int) -> Decimal:
"""根据 RuntimeContext 自动选 live / sandbox 路径。"""
# 实现内会判断:
# - if runtime_ctx.is_sandbox: 调 balance_replay.partial(business_date)
# - else: 调 dws_query.live_balance()
pass
```
### 5.3 测试模式
每个 replay 模块配套 unit test(BE-3 / T3 模式):
- mock get_runtime_context 返回 live / sandbox 两路
- 断言 SQL 包含 daily 累计 + business_date 截断
- 部分 integration test 用真实测试库验证数据一致性
### 5.4 性能考虑
- daily 累计 SQL 比 monthly snapshot 慢(SUM 多行 vs 单行查)
- 建议:sandbox 模式下接受性能折衷;live 模式仍走原 dws 月度路径
- 必要时可做 in-memory cache(business_date 不变时缓存命中)
## 六、阶段 B 前置依赖清单
| 依赖 | 来源 | 状态 |
|------|------|------|
| RuntimeContext + business_date | F1-5a | ✓ 已完成 |
| app 视图 daily 上界裁剪 | 2026-05-02 迁移 | ✓ 已完成 |
| Excel 修正表 effective_date 字段 | F1-5b MP-2 prep(本次) | ✓ 本次完成 |
| ETL Excel 上传 UI 支持 effective_date 列 | F1-6 阶段 B 内 | ⏳ 待做 |
| dws_assistant_daily_salary 表/视图 | F1-6 阶段 B 内(MP-2 真正实施) | ⏳ 待做 |
| dws_member_daily 系列视图(部分缺失) | F1-6 阶段 B 内 | ⏳ 待做 |
## 七、阶段 C 远期目标
### sandbox_audit_log(用户行为审计)
**业务目标**:沙箱模式下用户的所有操作都记录,切回 live 后可追溯,支持"沙箱演练复盘"场景。
**表设计草案**:
```sql
CREATE TABLE biz.sandbox_audit_log (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
sandbox_instance_id VARCHAR(100) NOT NULL,
user_id BIGINT NOT NULL,
action_type VARCHAR(50) NOT NULL, -- 'view' / 'trigger_ai' / 'modify' / ...
page_path VARCHAR(200),
payload JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
```
**写入入口**:统一 middleware 或 decorator 拦截 sandbox 模式请求。
## 八、关联
- F1-5a 主体审计:`docs/audit/changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md`
- F1-5a 走查报告:`docs/audit/changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md`
- F1-5b 任务清单:`docs/_overview/wave1-findings/F1-5b-tasks.md`
- F1-5b MP-2 prep 审计:`docs/audit/changes/2026-05-05__wave1_f1_5b_mp2_prep.md`
- P20 SPEC:`docs/prd/specs/P20-runtime-context-sandbox.md`
- 业务日上界 ETL 迁移:`db/etl_feiqiu/migrations/20260502__rls_views_business_date_upper_bound.sql`
## 九、决策路径(Owner Approval)
- **2026-05-05 Neo 决策**:
> "沙箱'全数据时光机'模块可行性 — 同意,分阶段实现,往主干任务排期中增加。"
- **本 spec 状态**:已 commit,等待 F1-6 启动。F1-6 启动时本 spec 作为阶段 B 实施依据。

View File

@@ -230,7 +230,7 @@ D1 ──> D2 ──> D3 ──> D4 (本阶段全部完成后)
|---|---|---|---|---|---|
| 13 | UI-3 AIDashboard sandbox 提示条 + runtime 分组 | follow-up | M | A1 | C3 |
| 14 | UI-5 AITriggerJobs runtime 列 | follow-up | XS | 无 | C3 |
| 15 | MP-2 board-coach 月度面板双口径 | follow-up | L | A2 | C4 |
| 15 | MP-2 board-coach 月度面板双口径 | **延期 F1-6** | L | A2 + dws_assistant_daily_salary 视图(F1-6 内建) | F1-6 |
| 16 | MP-4 coach-detail data.coachId 修复 | follow-up | S | 无 | C4 |
| 17 | T3 unit dispatcher runtime | 测试 | M | A3 | C5 |
| 18 | BE-3 ai_run_logs runtime 写入回归 | follow-up | S | T2 | C5 |
@@ -334,7 +334,7 @@ def reset_runtime_context_to_live():
| — | **Wave A mid-wave checkpoint** | — | — | — | — | — | — | Neo 复审 |
| UI-3 | AIDashboard sandbox 提示+分组 | 待开始 | — | C3 | — | ▢ | ▢ | 双线分组显示 |
| UI-5 | AITriggerJobs runtime 列 | 待开始 | — | C3 | — | ▢ | ▢ | 列正确显示 |
| MP-2 | board-coach 月度面板双口径 | 待开始 | — | C4 | — | | | partial vs settled |
| MP-2 | board-coach 月度面板双口径 | **延期 F1-6** | 2026-05-05 | C10(prep) | mp2_prep.md | - | - | **改方向**:走 D 方案需新建 dws_assistant_daily_salary,本次只做 schema prep(3 stg 表加 effective_date)+ spec 文档,完整实施在 F1-6 沙箱时光机阶段 B |
| MP-4 | coach-detail data.coachId 修复 | 待开始 | — | C4 | — | ▢ | - | 预存 bug 与 sandbox 无关 |
| T3 | dispatcher unit | 待开始 | — | C5 | — | ▢ | - | mock |
| BE-3 | ai_run_logs runtime 回归 | 待开始 | — | C5 | — | ▢ | ▢ | runtime 字段写入 |

View File

@@ -0,0 +1,121 @@
# 2026-05-05 · F1-5b MP-2 准备工作 + 沙箱时光机模块 spec
> Wave 1 / F1-5b Wave B 第 3 项任务调整(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2 顺序 15)
>
> 工作量评估 L / 4-6h(原方案)→ **本次仅 prep 1.5h**,完整实施延期到 F1-6
>
> **MP-2 决策路径**:经 4 轮调研 + Neo 反馈后,采纳方案 C(推迟到 F1-6 沙箱时光机阶段 B)
## 决策路径回顾
### 第 1 轮:原方案 D(双口径)
推荐 partial(daily 累计) + settled(monthly 全量)双字段并存。
### 第 2 轮:Neo 反馈 — 助教薪资业务流程澄清
- 系统先算工资,Excel 导入再修正(扣款/奖励对应到某一天)
- 任务完成率应按沙箱日截至
- 支出(房租)应按支出日期 + 沙箱日处理
- "信任危机"业务侧担心是伪命题(沙箱本就是产品功能)
- → 改方案 D''(单一口径,按 business_date 实时累计;DB 加 effective_date 字段)
### 第 3 轮:Neo 架构纠正 — 不要直接读 DWD
- DWD 是连接器层(飞球/未来其他),后续会做 DWD 处理 + 更新 Core
- Core 跨连接器保持表/字段一致(标准化)
- DWS / app 视图 / RLS 都基于 Core
- 后端应该读 Core / DWS / app,不读 DWD
- → 改方案 D'''(走 app.v_dws_assistant_daily_detail 已有 daily 视图)
### 第 4 轮:DWS 视图靠谱性审计 — 发现 daily_detail 不能替代 salary_calc
- `dws_assistant_daily_detail` 是计费明细(ledger_amount = 台费金额)
- 不是助教工资(gross_salary,需助教等级时薪 + 抽成 + 罚分)
- 缺 effective_hours / work_days 字段(monthly 表才有)
-**MP-2 真正实施需要新建 dws_assistant_daily_salary 表(ETL 改造,工作量 M)**
- → 推迟到 F1-6 沙箱时光机阶段 B,跟其他 14 个 P1 指标一起做(更高效)
### 第 5 轮:Neo 同意方向 1(推迟 + prep)
本次 Wave B 只做 prep:DB 迁移 + sandbox_replay spec 文档 + tasks.md 状态调整。
## 改动清单
### 1. DB 迁移(zqyy_app)
**文件**:`db/zqyy_app/migrations/20260505__add_effective_date_for_excel_adjustments.sql`
3 张 stg 表(全空)ADD COLUMN effective_date DATE NOT NULL(无 DEFAULT,因 Neo 反馈"截至当前还没有任何 Excel 被上传",强制未来必须带):
- `biz.salary_adjustments`(助教薪资扣款/奖励)
- `biz.stg_finance_expense`(月度支出)
- `biz.stg_platform_income`(平台结算收入)
3 个复合索引 `(site_id, effective_date)` 支持 daily 截断查询。
`biz.stg_recharge_commission` 已有 `recharge_date DATE` 字段,无需改造。
### 2. docs/database/ 同步
**文件**:`docs/database/changes/2026-05-05__add_effective_date_for_excel_adjustments.md`
完整变更说明 + 兼容性 + 回滚 + 5 条校验 SQL + 正式库执行说明。
### 3. 沙箱时光机模块 spec
**文件**:`docs/_overview/sandbox-replay-engine-spec.md`
完整模块设计:
- 22 个相关指标分 P1/P2/P3 优先级
- 14 个 P1 指标(daily 视图已有,后端切换即可)
- 5 个 P2 指标(算法重算,含 MP-2 完整 daily salary)
- 3 个 P3 指标(状态算法 + sandbox_audit_log 用户行为)
- 4 阶段实施路径:阶段 0(本次 prep) → 阶段 A(F1-5a/b 已完成) → 阶段 B(F1-6,2-3 周) → 阶段 C(F1-7+,1-2 周)
- sandbox_replay 模块结构 + runtime_aware decorator 接口契约
- 性能 + 测试模式建议
### 4. F1-5b-tasks.md 状态调整
§4.3 MP-2 行从 `待开始 / C4``延期 F1-6`,理由 + 关联文档已登记。
§4.2 顺序 15 行类似调整。
## Step 4 验证
DB 迁移在测试库执行 + 5/5 校验全 PASS:
1. ✓ 3 表 effective_date 字段存在(NOT NULL DATE 无 default)
2. ✓ 3 个复合索引存在
3. ✓ 索引列序 (site_id, effective_date)
4. ✓ 字段注释含 'F1-5b MP-2 prep'
5. ✓ INSERT 不带 effective_date 触发 NotNullViolation
走查脚本:`_DEL/walkthrough_f1_5b/step_mp2_prep_apply_migration.py`
## 影响范围
| 端 | 改动 | 影响 |
|----|------|------|
| zqyy_app DB | 3 表 schema + 3 索引 | 当前数据为空,无既有数据迁移风险 |
| ETL Excel 上传 | **未改**(F1-6 同期实施) | 旧模板上传会因 NOT NULL 失败 → 等 F1-6 ETL 改造同步 |
| 后端 service | **未改**(MP-2 主体延期) | board-coach sandbox 行为暂未修复(F1-6 解决) |
| admin-web / 小程序 | 无影响 | — |
## 风险与未覆盖
- **board-coach sandbox 行为遗留**:sandbox=2026-04-20 时,board-coach 仍展示月度全量(含 4-21~30 未发生数据),F1-6 才彻底修复
- **Excel 上传未来兼容**:旧模板上传将因 NOT NULL 失败,需要 F1-6 同期完成 ETL UI 改造 + 操作员培训
- **跨页面 audit 已识别**:board-finance / customer-records / coach-service-records / customer-service-records 等已合规(F1-5b A1/A3 + MP-1/3/5 收益),不需要本次额外处理
## 回滚策略
```sql
BEGIN;
DROP INDEX IF EXISTS biz.idx_salary_adj_site_eff_date;
DROP INDEX IF EXISTS biz.idx_stg_finance_expense_site_eff;
DROP INDEX IF EXISTS biz.idx_stg_platform_income_site_eff;
ALTER TABLE biz.salary_adjustments DROP COLUMN IF EXISTS effective_date;
ALTER TABLE biz.stg_finance_expense DROP COLUMN IF EXISTS effective_date;
ALTER TABLE biz.stg_platform_income DROP COLUMN IF EXISTS effective_date;
COMMIT;
```
详见 `docs/database/changes/2026-05-05__add_effective_date_for_excel_adjustments.md`
## Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>

View File

@@ -0,0 +1,173 @@
# 2026-05-05 · 3 张 Excel 暂存表加 effective_date 字段(F1-5b MP-2 prep)
> F1-5b Wave B MP-2 准备工作(后端 board_service 改造延期到 F1-6 沙箱时光机阶段 B)
>
> migration: `db/zqyy_app/migrations/20260505__add_effective_date_for_excel_adjustments.sql`
## 背景
F1-5b MP-2 调研发现:sandbox 模式下需要按业务日上界过滤 Excel 修正(扣款/奖励/支出/收入),但当前 schema 只有 YYYY-MM 月度精度,无法 daily 截断。
**Neo 决策(2026-05-05)**:
1. 三张目标表当前**全为空**(尚无任何 Excel 上传),直接 ADD COLUMN NOT NULL 不带 DEFAULT,强制未来 Excel 上传必须带 effective_date(操作员业务规范)
2. 后端 board_service 改造**延期到 F1-6 沙箱时光机阶段 B**(详见 `docs/_overview/sandbox-replay-engine-spec.md`)
3. 本次仅做 schema 准备 + 索引,不动 ETL 上传 / 后端业务逻辑
## 变更说明
### Schema 变更(3 张表)
| 表 | 用途 | 改动 |
|----|------|------|
| `biz.salary_adjustments` | 助教薪资扣款/奖励 | ADD COLUMN effective_date DATE NOT NULL |
| `biz.stg_finance_expense` | 月度支出(房租/水电/成本) | ADD COLUMN effective_date DATE NOT NULL |
| `biz.stg_platform_income` | 平台结算收入 | ADD COLUMN effective_date DATE NOT NULL |
`biz.stg_recharge_commission` 已有 `recharge_date DATE` 字段,无需改造。
### 字段定义
```sql
ALTER TABLE biz.<table_name>
ADD COLUMN effective_date DATE NOT NULL;
COMMENT ON COLUMN biz.<table_name>.effective_date IS
'生效日期(F1-5b MP-2 prep): Excel 上传时强制带,'
'用于 sandbox 模式下按业务日上界 (effective_date <= business_date) 过滤';
```
### 索引(3 个复合索引)
```sql
CREATE INDEX idx_salary_adj_site_eff_date
ON biz.salary_adjustments (site_id, effective_date);
CREATE INDEX idx_stg_finance_expense_site_eff
ON biz.stg_finance_expense (site_id, effective_date);
CREATE INDEX idx_stg_platform_income_site_eff
ON biz.stg_platform_income (site_id, effective_date);
```
支持后续 daily 截断查询模式 `WHERE site_id = ? AND effective_date <= ?`
## 兼容性
| 端 | 影响 | 处理 |
|----|------|------|
| **现有数据** | 三表当前全为空,ADD COLUMN NOT NULL 不引发既有数据迁移失败 | — |
| **ETL Excel 上传** | 后续(F1-6)需要在导入流程中解析 effective_date 列 | F1-6 内做 |
| **后端 API** | 当前无任何后端 SQL 使用 effective_date,**本次 schema 改动无即时业务影响** | — |
| **管理后台 Excel 上传 UI** | tenant-admin 的 ExcelUpload 模块需要在 Excel 模板新增 effective_date 列 | F1-6 内做(配套) |
| **既有 Excel 文件模板** | 未来上传必须新模板带日期列;旧模板上传会因 NOT NULL 失败 | 需要操作员培训 + 模板分发 |
## 回滚策略
```sql
BEGIN;
DROP INDEX IF EXISTS biz.idx_salary_adj_site_eff_date;
DROP INDEX IF EXISTS biz.idx_stg_finance_expense_site_eff;
DROP INDEX IF EXISTS biz.idx_stg_platform_income_site_eff;
ALTER TABLE biz.salary_adjustments DROP COLUMN IF EXISTS effective_date;
ALTER TABLE biz.stg_finance_expense DROP COLUMN IF EXISTS effective_date;
ALTER TABLE biz.stg_platform_income DROP COLUMN IF EXISTS effective_date;
COMMIT;
```
幂等,执行多次安全。
## 验证 SQL(测试库已 PASS)
### 校验 1:三表都有 effective_date 字段(NOT NULL DATE 无 default)
```sql
SELECT table_name, column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'biz'
AND table_name IN ('salary_adjustments', 'stg_finance_expense', 'stg_platform_income')
AND column_name = 'effective_date'
ORDER BY table_name;
-- 期望: 3 行,全部 data_type=date / is_nullable='NO' / column_default=NULL
```
### 校验 2:三个复合索引都已创建
```sql
SELECT tablename, indexname FROM pg_indexes
WHERE schemaname = 'biz'
AND indexname IN (
'idx_salary_adj_site_eff_date',
'idx_stg_finance_expense_site_eff',
'idx_stg_platform_income_site_eff'
)
ORDER BY indexname;
-- 期望: 3 行
```
### 校验 3:索引列序正确 (site_id, effective_date)
```sql
SELECT i.relname AS index_name,
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) AS columns
FROM pg_class t
JOIN pg_index ix ON t.oid = ix.indrelid
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
WHERE i.relname IN (
'idx_salary_adj_site_eff_date',
'idx_stg_finance_expense_site_eff',
'idx_stg_platform_income_site_eff'
)
GROUP BY i.relname
ORDER BY i.relname;
-- 期望: 全部 ['site_id', 'effective_date']
```
### 校验 4:字段注释存在
```sql
SELECT c.table_name,
col_description((c.table_schema||'.'||c.table_name)::regclass::oid, c.ordinal_position) AS comment
FROM information_schema.columns c
WHERE c.table_schema = 'biz'
AND c.table_name IN ('salary_adjustments', 'stg_finance_expense', 'stg_platform_income')
AND c.column_name = 'effective_date'
ORDER BY c.table_name;
-- 期望: 3 行,comment 含 'F1-5b MP-2 prep'
```
### 校验 5:INSERT 不带 effective_date 应失败(NOT NULL 约束生效)
```sql
BEGIN;
INSERT INTO biz.salary_adjustments
(site_id, assistant_name, assistant_number, salary_month,
adjustment_type, amount, reason, upload_batch_id, created_by)
VALUES (2790685415443269, 'test', 'T001', '2026-04', 'penalty',
100.00, 'test', 1, 1);
ROLLBACK;
-- 期望: NotNullViolation
```
## 正式库执行说明
本次 migration **仅在测试库执行**。生产环境同步时:
```bash
psql "$APP_DB_DSN" -f db/zqyy_app/migrations/20260505__add_effective_date_for_excel_adjustments.sql
```
执行前必须确认三张表为空(若已有数据,需先评估回填策略)。执行后跑 5 条校验 SQL。
## 后续依赖任务(F1-6 沙箱时光机阶段 B)
详见 `docs/_overview/sandbox-replay-engine-spec.md`:
1. **ETL Excel 上传 UI 改造**:tenant-admin/ExcelUpload 模块支持 effective_date 列解析
2. **Excel 模板分发**:操作员培训 + 新模板分发
3. **后端 board_service 改造**:替换 `dws_assistant_salary_calc` 直查为 daily 累计 + adjustments 截断
4. **MP-2 实施 + 双口径走查**
## 关联
- F1-5b MP-2 调研记录:`docs/audit/changes/2026-05-05__wave1_f1_5b_mp2_prep.md`
- 沙箱时光机模块 spec:`docs/_overview/sandbox-replay-engine-spec.md`
- F1-5b 任务清单:`docs/_overview/wave1-findings/F1-5b-tasks.md`(MP-2 状态已调整为"延期 F1-6")