# 2026-05-06 · F1-6 Sprint 2 #1 — 60 天消费迁移到 sandbox_replay > F1-6 沙箱时光机阶段 B Sprint 2(详见 `docs/_overview/wave1-findings/F1-6-tasks.md` §3) > > 工作量评估 S / 30-50min(实际 ~ 40min,含 4a/4b 双口径 UI 走查) > > Sprint 2 计划 5 指标按方案 A 顺序:**60d 消费 ☚** → 累计消费总额 → 累计交易笔数 → 储值卡余额 → 累计 GMV > > 完整模块 spec:`docs/_overview/sandbox-replay-engine-spec.md` ## 背景 Sprint 1 完成 sandbox_replay 模块脚手架 + `get_last_visit_days` 试点(commit 9f1e35d)。 Sprint 2 #1 沿用试点模式,迁移第二个会员相关 P1 指标 `get_consumption_60d`。 ## 改动清单 ### Step 3 实施 **修改文件**(3 个): - [apps/backend/app/services/sandbox_replay/consumption_replay.py](apps/backend/app/services/sandbox_replay/consumption_replay.py) — 追加 `get_consumption_60d` 函数,沿用 `@trace_service + @runtime_aware` 装饰组合 - [apps/backend/app/services/sandbox_replay/__init__.py](apps/backend/app/services/sandbox_replay/__init__.py) — re-export `get_consumption_60d_replay` - [apps/backend/app/services/fdw_queries.py](apps/backend/app/services/fdw_queries.py) `get_consumption_60d`(行 ~1047)改 thin wrapper,委托给 sandbox_replay 实现,保持 2 处现有调用兼容 **测试文件**(本地不入仓,`.gitignore:71`): - [apps/backend/tests/test_sandbox_replay_sprint2.py](apps/backend/tests/test_sandbox_replay_sprint2.py) — 5 case 全 PASS 测试覆盖: - 正常返回 Decimal - 无快照行返回 None - `consume_amount_60d` 字段为 NULL 返回 None - sandbox 模式 SQL 显式带 `stat_date <= ctx.business_date` 上界(双保险) - thin wrapper 委托链路验证 合并 sprint 1 测试套件:**15/15 PASS,无回归**。 ### Step 4 双口径走查证据 **目标 member**: 2799207087163141(黄先生) **UI 实地展示位置**:`pages/customer-detail/customer-detail.wxml:45` — 顶部 banner 下方 stat 卡条**第 2 格**,标签"60天消费",数值由 `fmt.money(detail.consumption60d)` 渲染。 `utils/format.wxs:23` `money()` 用 `Math.round(Math.abs(value))` 取整,所以小数被舍入。 | 维度 | 4a live (today=2026-05-05) | 4b sandbox=2026-04-20 | |------|---------------------------|----------------------| | stat_date 数据 | 2026-05-01(真实) | 2026-04-15(walkthrough 测试快照) | | `consume_amount_60d` 字段值 | 115.36 | 88.88(测试值) | | API `consumption60D` 返回 | **115.36** ✓ | **88.88** ✓ | | 小程序 `detail.consumption60d` setData | 115.36 | 88.88 | | **小程序 stat 卡条 #2 渲染**(`fmt.money` 取整) | **¥115** ✓ | **¥89** ✓ | | `daysSinceVisit` 第 4 格(回归 sprint 1) | 32天 | 31天 | 走查截图(双口径 stat 卡条对照清晰): - `_DEL/walkthrough_f1_6/sprint2_4a_live_consumption60d.png` — `¥547 / ¥115 / -- / 32天` - `_DEL/walkthrough_f1_6/sprint2_4b_sandbox_consumption60d.png` — `¥547 / ¥89 / -- / 31天` 走查脚本: - `_DEL/walkthrough_f1_6/step_sprint2_60d_seed.py`(插测试快照 + 切 sandbox) - `_DEL/walkthrough_f1_6/step_sprint2_60d_cleanup.py`(删快照 + 切回 live) - `_DEL/walkthrough_f1_6/step_sprint2_60d_direct_call.py`(直接 Python 验证) **关键证据**:sandbox 切换时,`get_consumption_60d` 函数读 `ctx.business_date=2026-04-20` 作为 SQL 上界,视图层同时按 `stat_date <= app.business_date_now()=2026-04-20` 过滤,双重保障下取 stat_date=2026-04-15 行的 `consume_amount_60d=88.88`,完全符合"沙箱时光机"语义。 测试快照已清理:`DELETE FROM dws.dws_member_consumption_summary WHERE site_id=2790685415443269 AND member_id=2799207087163141 AND stat_date='2026-04-15'`(1 行)。 sandbox 已切回 live。 ### Step 5 审计 本文件 + F1-6-tasks.md 进度推进(待更新)。 ## 影响范围 | 端 | 影响 | 验证 | |----|------|------| | 后端 sandbox_replay.consumption_replay | 新增 `get_consumption_60d` 函数 | unit test 5/5 + 累计 15/15 PASS | | 后端 fdw_queries.get_consumption_60d | 改 thin wrapper(行为完全一致) | MCP 4a/4b 双口径 PASS | | 后端 customer_service.py 2 处调用 | 无感(thin wrapper 透明委托,签名不变) | 4a=115.36 / 4b=88.88 | | 小程序 customer-detail | 无感(API 字段名不变) | UI snapshot + screenshot 双确认 | | admin-web / 租户后台 / ETL | 无影响 | — | ## 测试 - 后端 unit test 15/15 PASS(本地 `apps/backend/tests/test_sandbox_replay_sprint{1,2}.py`,因 `.gitignore:71` 不入仓) - MCP 端到端 4a/4b 双口径 PASS(含 navigate_to + snapshot + screenshot,**完整 UI 验证**) - 直接 Python 调用 `sandbox_replay.consumption_replay.get_consumption_60d` PASS - 测试快照清理验证 PASS ## 风险与未覆盖 - **本指标 sandbox 数据局限**:测试库 `dws_member_consumption_summary` 仅 stat_date=2026-05-01 一行,sandbox=04-20 时所有快照都被视图层过滤,需通过插 walkthrough 测试快照(stat_date=2026-04-15 with `consume_amount_60d=88.88`)演示框架行为。生产数据应有连续 daily 快照,不会有此问题。 - **CamelCase 边角**:Pydantic 把 `consumption_60d` → `consumption60D`(数字后字母大写),前端 `customer-detail.ts:117` 已注释说明并兼容(`d.consumption60D ?? d.consumption60d`),本次改动未触发该问题。 - **未覆盖 sprint 2 剩余 4 个指标**:#2 累计消费总额 / #3 累计交易笔数 / #4 储值卡余额 / #5 累计 GMV,后续按方案 A 顺序逐项推进。 ## 回滚策略 ```bash git revert ``` 回滚后: - `sandbox_replay/consumption_replay.py` 删除 `get_consumption_60d` 函数 - `sandbox_replay/__init__.py` 移除 re-export - `fdw_queries.get_consumption_60d` 恢复原直接 SQL 实现(单 SELECT) - 2 处调用点无影响(thin wrapper 透明) - 测试文件本地保留(`.gitignore` 范围内) ## Co-Authored-By Claude Opus 4.7 (1M context)