From d418621951b28b383e8eceba7119aa03ee652a8c Mon Sep 17 00:00:00 2001 From: Neo Date: Wed, 6 May 2026 00:27:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20F1-6=20sprint2=20#1=2060d=20?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E8=BF=81=E7=A7=BB=E5=88=B0=20sandbox=5Frepla?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移 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) --- apps/backend/app/services/fdw_queries.py | 28 ++--- .../app/services/sandbox_replay/__init__.py | 2 + .../sandbox_replay/consumption_replay.py | 68 ++++++++++- docs/_overview/wave1-findings/F1-6-tasks.md | 27 +++-- docs/audit/audit_dashboard.md | 11 +- ...026-05-06__f1_6_sprint2_consumption_60d.md | 111 ++++++++++++++++++ 6 files changed, 212 insertions(+), 35 deletions(-) create mode 100644 docs/audit/changes/2026-05-06__f1_6_sprint2_consumption_60d.md diff --git a/apps/backend/app/services/fdw_queries.py b/apps/backend/app/services/fdw_queries.py index 71bf1ed..cd3bc56 100644 --- a/apps/backend/app/services/fdw_queries.py +++ b/apps/backend/app/services/fdw_queries.py @@ -1044,31 +1044,21 @@ def get_service_records_for_task( return records -@trace_service(description_zh="获取近60天消费金额", description_en="Get 60-day consumption") def get_consumption_60d( conn: Any, site_id: int, member_id: int, *, etl_conn: Any = None, ) -> Decimal | None: - """ - 查询客户近 60 天消费金额。 + """thin wrapper — F1-6 sprint 2 已迁移到 sandbox_replay.consumption_replay。 - 来源: app.v_dws_member_consumption_summary(DWS 预聚合表)。 - 与 board-customer spend60 维度统一口径:items_sum,60天窗口,日粒度。 - 取最新 stat_date 的快照行。 + 保持原签名兼容现有 2 处调用(customer_service 主路径 + 月度版本路径)。 + sandbox 时光机语义由 @runtime_aware 自动注入 RuntimeContext + 视图层 + stat_date <= business_date_now() 双重保障。 """ - with _fdw_context(conn, site_id, etl_conn=etl_conn) as cur: - cur.execute( - """ - SELECT consume_amount_60d - FROM app.v_dws_member_consumption_summary - WHERE member_id = %s - ORDER BY stat_date DESC - LIMIT 1 - """, - (member_id,), - ) - row = cur.fetchone() - return Decimal(str(row[0])) if row and row[0] is not None else None + from app.services.sandbox_replay.consumption_replay import ( + get_consumption_60d as _replay_impl, + ) + + return _replay_impl(conn, site_id, member_id, etl_conn=etl_conn) @trace_service(description_zh="获取关系指数", description_en="Get relation index") diff --git a/apps/backend/app/services/sandbox_replay/__init__.py b/apps/backend/app/services/sandbox_replay/__init__.py index 8207969..f3c03fc 100644 --- a/apps/backend/app/services/sandbox_replay/__init__.py +++ b/apps/backend/app/services/sandbox_replay/__init__.py @@ -33,10 +33,12 @@ F1-6 沙箱时光机阶段 B 启动模块。 from app.services.sandbox_replay._decorator import runtime_aware from app.services.sandbox_replay.consumption_replay import ( + get_consumption_60d as get_consumption_60d_replay, get_last_visit_days as get_last_visit_days_replay, ) __all__ = [ "runtime_aware", "get_last_visit_days_replay", + "get_consumption_60d_replay", ] diff --git a/apps/backend/app/services/sandbox_replay/consumption_replay.py b/apps/backend/app/services/sandbox_replay/consumption_replay.py index 681980c..6096df0 100644 --- a/apps/backend/app/services/sandbox_replay/consumption_replay.py +++ b/apps/backend/app/services/sandbox_replay/consumption_replay.py @@ -1,23 +1,23 @@ """消费/到店相关 sandbox 重算实现。 -F1-6 沙箱时光机阶段 B Sprint 1 试点迁移。 +F1-6 沙箱时光机阶段 B Sprint 1/2 试点迁移。 覆盖指标(渐进): - P1-4 距上次到店天数(get_last_visit_days)— sprint 1 已迁移 - - P1-2 60 天消费 — sprint 2 + - P1-2 60 天消费(get_consumption_60d)— sprint 2 ☚ - P1-3 累计消费总额 — sprint 2 - P1-12 累计交易笔数 — sprint 2 - - P1-13 累计 GMV — sprint 2 设计原则: - 通过 @runtime_aware decorator 自动注入 RuntimeContext - 函数体内显式使用 ctx.business_date / ctx.is_sandbox - 与 fdw_queries 中的原始实现保持完全行为一致(回归安全) - - 原 fdw_queries.get_last_visit_days 保留为 thin wrapper 兼容现有调用 + - 原 fdw_queries.* 保留为 thin wrapper 兼容现有调用 """ from __future__ import annotations +from decimal import Decimal from typing import Any from app.services.runtime_context import RuntimeContext @@ -95,3 +95,63 @@ def get_last_visit_days( result[mid] = None return result + + +# ── P1-2 60 天消费 ────────────────────────────────────── + + +@trace_service( + description_zh="获取近 60 天消费金额(sandbox_replay)", + description_en="Get 60-day consumption (sandbox_replay)", +) +@runtime_aware(metric="consumption_60d") +def get_consumption_60d( + conn: Any, + site_id: int, + member_id: int, + *, + etl_conn: Any = None, + ctx: RuntimeContext, +) -> Decimal | None: + """查询客户近 60 天消费金额(sandbox_replay 版本)。 + + 迁移自 fdw_queries.get_consumption_60d。行为完全一致: + - 取业务日为基准(ctx.business_date,sandbox 模式下为 sandbox_date) + - 仅查 stat_date <= business_date 的快照行(显式上界,与视图过滤一致) + - 取最新 stat_date 行的 consume_amount_60d + + 口径(与 board-customer spend60 维度统一): + - items_sum,60 天滚动窗口,日粒度 + - 该字段由 ETL DWS 跑批时计算,sandbox 切到历史日期时,视图自动按 + stat_date <= business_date_now() 过滤,取当时 ETL 写入的近 60 天值 + + Args: + conn: zqyy_app 业务库连接 + site_id: 门店 ID + member_id: 单个会员 ID(保持原 API 签名) + etl_conn: 可选,显式传 ETL 连接(便于测试 mock) + ctx: RuntimeContext(由 @runtime_aware 自动注入) + + Returns: + Decimal 金额或 None(无快照 / 字段为 NULL 时) + """ + # 延迟 import 避免循环依赖 + from app.services.fdw_queries import _fdw_context + + ref_date = ctx.business_date + + with _fdw_context(conn, site_id, etl_conn=etl_conn) as cur: + cur.execute( + """ + SELECT consume_amount_60d + FROM app.v_dws_member_consumption_summary + WHERE member_id = %s + AND stat_date <= %s + ORDER BY stat_date DESC + LIMIT 1 + """, + (member_id, ref_date), + ) + row = cur.fetchone() + + return Decimal(str(row[0])) if row and row[0] is not None else None diff --git a/docs/_overview/wave1-findings/F1-6-tasks.md b/docs/_overview/wave1-findings/F1-6-tasks.md index 9a4d68e..06dc1aa 100644 --- a/docs/_overview/wave1-findings/F1-6-tasks.md +++ b/docs/_overview/wave1-findings/F1-6-tasks.md @@ -13,7 +13,7 @@ | Sprint | 范围 | 工作量 | 状态 | |--------|------|------|------| | **Sprint 1** | 框架(sandbox_replay 模块 + runtime_aware decorator) + 1 个试点指标(距上次到店天数迁移) | M ~ 4-5h | ✅ 完成(2026-05-05) | -| Sprint 2 | 5 个会员相关 P1 指标(余额 / 60d 消费 / 累计交易 / GMV / 累计服务客户数) | M ~ 4h | ⏳ 待启动 | +| **Sprint 2** | 5 个会员相关 P1 指标(60d 消费 / 累计消费总额 / 累计交易笔数 / 储值卡余额 / 累计 GMV) | M ~ 4h | 🔄 进行中(#1 60d 消费 ✅ 2026-05-06) | | Sprint 3 | 5 个助教/门店相关 P1 + **MP-2 完整**(daily salary 含 ETL 改造) | L ~ 8-10h | ⏳ 待启动 | | Sprint 4 | 5 个 P2 指标(RS 重算 / 客户黏性 / 任务完成率 / Excel 修正 / 月度新增流失) | M-L ~ 6-8h | ⏳ 待启动 | @@ -69,27 +69,32 @@ --- -## 三、Sprint 2 范围(待启动) +## 三、Sprint 2 范围(进行中) + +启动顺序按方案 A:从已有 `consumption_replay.py` 模块扩展开始,新模块靠后。 ### 5 个会员相关 P1 指标 -| # | 指标 | 当前实现位置 | 目标迁移到 | 复杂度 | -|---|------|-------------|----------|------| -| 1 | 会员储值卡余额 | `fdw_queries.get_member_balance` | `sandbox_replay/balance_replay.py` | S | -| 2 | 60 天消费 | `fdw_queries.get_consumption_60d` | `sandbox_replay/consumption_replay.py`(扩展) | S | -| 3 | 累计消费总额 | (待补,目前无独立查询) | `sandbox_replay/consumption_replay.py` | S | -| 4 | 累计交易笔数 | (待补,后端无实现) | `sandbox_replay/consumption_replay.py` | S | -| 5 | 累计 GMV | `board_service` 财务总额 | `sandbox_replay/finance_replay.py`(新建) | S | +| # | 指标 | 当前实现位置 | 目标迁移到 | 复杂度 | 状态 | +|---|------|-------------|----------|------|------| +| 1 | 60 天消费 | `fdw_queries.get_consumption_60d` | `sandbox_replay/consumption_replay.py`(扩展) | S | ✅ 2026-05-06 | +| 2 | 累计消费总额 | (待补,目前无独立查询) | `sandbox_replay/consumption_replay.py`(扩展) | S | ⏳ 待启动 | +| 3 | 累计交易笔数 | (待补,后端无实现) | `sandbox_replay/consumption_replay.py`(扩展) | S | ⏳ 待启动 | +| 4 | 会员储值卡余额 | `fdw_queries.get_member_balance` | `sandbox_replay/balance_replay.py`(新建) | S | ⏳ 待启动 | +| 5 | 累计 GMV | `board_service` 财务总额 | `sandbox_replay/finance_replay.py`(新建) | S | ⏳ 待启动 | ### Sprint 2 实施模式 所有指标走 `@runtime_aware` decorator + `app.v_dws_*` 视图查询。每个指标: - 1 个新函数 to sandbox_replay/ - 1 个 thin wrapper in fdw_queries.py(兼容) -- 单元测试 + MCP 走查 +- 单元测试(本地不入仓)+ MCP 双口径走查(navigate_to + snapshot + screenshot,**含 UI 实地验证**) - 审计 + 单独 commit +### Sprint 2 commit +- #1 60d 消费 — `feat(backend): F1-6 sprint2 #1 60d 消费迁移 sandbox_replay`(待提交) + ### 估算 -5 指标 × 30-50min = 3-4h +5 指标 × 30-50min = 3-4h(#1 实际 ~ 40min) --- diff --git a/docs/audit/audit_dashboard.md b/docs/audit/audit_dashboard.md index cebf34d..edde0f4 100644 --- a/docs/audit/audit_dashboard.md +++ b/docs/audit/audit_dashboard.md @@ -1,16 +1,19 @@ # 审计一览表 -> 自动生成于 2026-05-05 22:20:03,请勿手动编辑。 +> 自动生成于 2026-05-06 00:23:40,请勿手动编辑。 ## 时间线视图 | 日期 | 项目 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|------|----------|----------|----------|------|------| +| 2026-05-06 | 项目级 | 2026-05-06 · F1-6 Sprint 2 #1 — 60 天消费迁移到 sandbox_replay | 清理 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_consumption_60d.md) | +| 2026-05-05 | 项目级 | 2026-05-05 · F1-6 Sprint 1 沙箱时光机引擎启动 + get_last_visit_days 试点迁移 | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__f1_6_sprint1_sandbox_replay_kickoff.md) | | 2026-05-05 | 项目级 | 2026-05-05 — Wave 1 F1-5a 完整走查(应查尽查版) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md) | | 2026-05-05 | 项目级 | Wave 1 F1-5a — 沙箱 batch-run 接入 runtime_context(MVP + 漂移防御核心) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md) | | 2026-05-05 | 项目级 | 2026-05-05 · F1-5b A6 ETL 连接显式 client_encoding=UTF8 防御 | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_a6_etl_conn_utf8.md) | | 2026-05-05 | 项目级 | 2026-05-05 · F1-5b BE-1 task-list 403 根因定位 + 修复(B 方案) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md) | | 2026-05-05 | 项目级 | 2026-05-05 · F1-5b BE-3 ai_run_logs runtime 写入回归测试 | 功能 | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_be3_run_log_runtime_regression.md) | +| 2026-05-05 | 项目级 | 2026-05-05 · F1-5b D1-D4 P20 SPEC 同步 + audit dashboard 刷新 | 文档 | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_d1234_spec_sync.md) | | 2026-05-05 | 项目级 | 2026-05-05 · F1-5b MP-1 board-finance 储值充值字段复核(false alarm) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp1_recharge_field_clarification.md) | | 2026-05-05 | 项目级 | 2026-05-05 · F1-5b MP-2 准备工作 + 沙箱时光机模块 spec | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp2_prep.md) | | 2026-05-05 | 项目级 | 2026-05-05 · F1-5b MP-3 customer-detail lastService 业务日上界裁剪 | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp3_lastservice_upper_bound.md) | @@ -286,11 +289,14 @@ | 日期 | 需求摘要 | 变更类型 | 影响模块 | 风险 | 详情 | |------|----------|----------|----------|------|------| +| 2026-05-06 | 2026-05-06 · F1-6 Sprint 2 #1 — 60 天消费迁移到 sandbox_replay | 清理 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_consumption_60d.md) | +| 2026-05-05 | 2026-05-05 · F1-6 Sprint 1 沙箱时光机引擎启动 + get_last_visit_days 试点迁移 | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__f1_6_sprint1_sandbox_replay_kickoff.md) | | 2026-05-05 | 2026-05-05 — Wave 1 F1-5a 完整走查(应查尽查版) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md) | | 2026-05-05 | Wave 1 F1-5a — 沙箱 batch-run 接入 runtime_context(MVP + 漂移防御核心) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md) | | 2026-05-05 | 2026-05-05 · F1-5b A6 ETL 连接显式 client_encoding=UTF8 防御 | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_a6_etl_conn_utf8.md) | | 2026-05-05 | 2026-05-05 · F1-5b BE-1 task-list 403 根因定位 + 修复(B 方案) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md) | | 2026-05-05 | 2026-05-05 · F1-5b BE-3 ai_run_logs runtime 写入回归测试 | 功能 | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_be3_run_log_runtime_regression.md) | +| 2026-05-05 | 2026-05-05 · F1-5b D1-D4 P20 SPEC 同步 + audit dashboard 刷新 | 文档 | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_d1234_spec_sync.md) | | 2026-05-05 | 2026-05-05 · F1-5b MP-1 board-finance 储值充值字段复核(false alarm) | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp1_recharge_field_clarification.md) | | 2026-05-05 | 2026-05-05 · F1-5b MP-2 准备工作 + 沙箱时光机模块 spec | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp2_prep.md) | | 2026-05-05 | 2026-05-05 · F1-5b MP-3 customer-detail lastService 业务日上界裁剪 | bugfix | 其他 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp3_lastservice_upper_bound.md) | @@ -455,11 +461,14 @@ | 日期 | 需求摘要 | 变更类型 | 风险 | 详情 | |------|----------|----------|------|------| +| 2026-05-06 | 2026-05-06 · F1-6 Sprint 2 #1 — 60 天消费迁移到 sandbox_replay | 清理 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_consumption_60d.md) | +| 2026-05-05 | 2026-05-05 · F1-6 Sprint 1 沙箱时光机引擎启动 + get_last_visit_days 试点迁移 | bugfix | 未知 | [链接](changes/2026-05-05__f1_6_sprint1_sandbox_replay_kickoff.md) | | 2026-05-05 | 2026-05-05 — Wave 1 F1-5a 完整走查(应查尽查版) | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5a_backend_walkthrough.md) | | 2026-05-05 | Wave 1 F1-5a — 沙箱 batch-run 接入 runtime_context(MVP + 漂移防御核心) | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5a_sandbox_batch_run.md) | | 2026-05-05 | 2026-05-05 · F1-5b A6 ETL 连接显式 client_encoding=UTF8 防御 | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_a6_etl_conn_utf8.md) | | 2026-05-05 | 2026-05-05 · F1-5b BE-1 task-list 403 根因定位 + 修复(B 方案) | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_be1_task_list_403_root_cause.md) | | 2026-05-05 | 2026-05-05 · F1-5b BE-3 ai_run_logs runtime 写入回归测试 | 功能 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_be3_run_log_runtime_regression.md) | +| 2026-05-05 | 2026-05-05 · F1-5b D1-D4 P20 SPEC 同步 + audit dashboard 刷新 | 文档 | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_d1234_spec_sync.md) | | 2026-05-05 | 2026-05-05 · F1-5b MP-1 board-finance 储值充值字段复核(false alarm) | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp1_recharge_field_clarification.md) | | 2026-05-05 | 2026-05-05 · F1-5b MP-2 准备工作 + 沙箱时光机模块 spec | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp2_prep.md) | | 2026-05-05 | 2026-05-05 · F1-5b MP-3 customer-detail lastService 业务日上界裁剪 | bugfix | 未知 | [链接](changes/2026-05-05__wave1_f1_5b_mp3_lastservice_upper_bound.md) | diff --git a/docs/audit/changes/2026-05-06__f1_6_sprint2_consumption_60d.md b/docs/audit/changes/2026-05-06__f1_6_sprint2_consumption_60d.md new file mode 100644 index 0000000..3d9db8f --- /dev/null +++ b/docs/audit/changes/2026-05-06__f1_6_sprint2_consumption_60d.md @@ -0,0 +1,111 @@ +# 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)