Files
Neo-ZQYY/docs/audit/changes/2026-05-06__f1_6_sprint2_consumption_60d.md
Neo d418621951 feat(backend): F1-6 sprint2 #1 60d 消费迁移到 sandbox_replay
迁移 fdw_queries.get_consumption_60d 到 sandbox_replay.consumption_replay,
沿用 sprint 1 模式: @trace_service + @runtime_aware decorator + 显式
stat_date <= ctx.business_date 上界(与视图过滤双保险),fdw_queries 改
thin wrapper 保持 customer_service 2 处调用兼容。

双口径走查 PASS(member=2799207087163141 黄先生):
- 4a live(today=2026-05-05): 小程序 stat 卡条 60天消费 ¥115(consume_amount_60d=115.36)
- 4b sandbox=2026-04-20: 小程序 stat 卡条 60天消费 ¥89(walkthrough 测试快照 88.88)

unit test sprint1+sprint2 累计 15/15 PASS,无回归。

详见 docs/audit/changes/2026-05-06__f1_6_sprint2_consumption_60d.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 00:27:13 +08:00

5.7 KiB

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 个):

测试文件(本地不入仓,.gitignore:71):

测试覆盖:

  • 正常返回 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_60dconsumption60D(数字后字母大写),前端 customer-detail.ts:117 已注释说明并兼容(d.consumption60D ?? d.consumption60d),本次改动未触发该问题。
  • 未覆盖 sprint 2 剩余 4 个指标:#2 累计消费总额 / #3 累计交易笔数 / #4 储值卡余额 / #5 累计 GMV,后续按方案 A 顺序逐项推进。

回滚策略

git revert <commit_hash>

回滚后:

  • 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) noreply@anthropic.com