feat(backend): F1-6 sprint2 #5 累计 GMV 加入 sandbox_replay (门店级)

Sprint 2 收尾指标:门店级累计 GMV(与 #1-#4 会员级粒度不同),新建
sandbox_replay/finance_replay.py 模块。无原 fdw_queries.get_total_gmv
函数(0 现有调用方),不写 thin wrapper(spec §5.5 决策原则)。

数据源 dws_finance_daily_summary.gross_amount(门店日度财务汇总,daily 累计)。
SQL 模式 SUM(gross_amount) WHERE stat_date <= ctx.business_date,与 #1-#4
取最新单行不同,是多行累计 SUM。SQL 层 COALESCE(SUM(...), 0) 兜底,无数据
返回 Decimal('0')(开店前累计 GMV = 0,业务语义)。

口径 gross_amount = table_fee + goods + assistant_pd + assistant_cx,**不含
electricity_money**(与会员级 items_sum 略有差异,docstring 明确防止交叉验证)。

双口径数值验证 PASS(直接 Python,site=2790685415443269 朗朗桌球):
- 4a live(today=2026-05-05): ¥5,725,837.51
- 4b sandbox=2026-04-20: ¥5,653,063.37(差异 ¥72,774,即 4-21~4-27 七天合计)

新增防御性回归测试 test_get_total_gmv_no_member_ids_param 阻断未来误加
member_id 参数(门店级粒度强约束)。

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

Sprint 2 收尾(4 项迁移 + #3 推迟 Sprint 3 等 ETL 配合)。

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-06 01:32:14 +08:00
parent 7b1cfadc2e
commit c446920c9e
5 changed files with 222 additions and 4 deletions

View File

@@ -1,6 +1,6 @@
# 审计一览表
> 自动生成于 2026-05-06 01:25:49,请勿手动编辑。
> 自动生成于 2026-05-06 01:32:13,请勿手动编辑。
## 时间线视图
@@ -9,6 +9,7 @@
| 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-06 | 项目级 | 2026-05-06 · F1-6 Sprint 2 #4 — 储值卡余额迁移到 sandbox_replay(SCD2 时光机) | 功能 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_member_balance.md) |
| 2026-05-06 | 项目级 | 2026-05-06 · F1-6 Sprint 2 #2 — 累计消费总额加入 sandbox_replay | 清理 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_total_consume_amount.md) |
| 2026-05-06 | 项目级 | 2026-05-06 · F1-6 Sprint 2 #5 — 累计 GMV 加入 sandbox_replay(门店级) | 文档 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_total_gmv.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) |
@@ -294,6 +295,7 @@
| 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-06 | 2026-05-06 · F1-6 Sprint 2 #4 — 储值卡余额迁移到 sandbox_replay(SCD2 时光机) | 功能 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_member_balance.md) |
| 2026-05-06 | 2026-05-06 · F1-6 Sprint 2 #2 — 累计消费总额加入 sandbox_replay | 清理 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_total_consume_amount.md) |
| 2026-05-06 | 2026-05-06 · F1-6 Sprint 2 #5 — 累计 GMV 加入 sandbox_replay(门店级) | 文档 | 其他 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_total_gmv.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) |
@@ -468,6 +470,7 @@
| 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-06 | 2026-05-06 · F1-6 Sprint 2 #4 — 储值卡余额迁移到 sandbox_replay(SCD2 时光机) | 功能 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_member_balance.md) |
| 2026-05-06 | 2026-05-06 · F1-6 Sprint 2 #2 — 累计消费总额加入 sandbox_replay | 清理 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_total_consume_amount.md) |
| 2026-05-06 | 2026-05-06 · F1-6 Sprint 2 #5 — 累计 GMV 加入 sandbox_replay(门店级) | 文档 | 未知 | [链接](changes/2026-05-06__f1_6_sprint2_total_gmv.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) |

View File

@@ -0,0 +1,121 @@
# 2026-05-06 · F1-6 Sprint 2 #5 — 累计 GMV 加入 sandbox_replay(门店级)
> F1-6 沙箱时光机阶段 B Sprint 2 #5 — **Sprint 2 收尾**(详见 `docs/_overview/wave1-findings/F1-6-tasks.md` §3)
>
> 工作量评估 S / 30-50min(实际 ~ 35min)
>
> Sprint 2 进度:#1 ✅ → #2 ✅ → #3 推迟 Sprint 3 → #4 ✅ → **#5 累计 GMV ☚** → **Sprint 2 完成**
>
> 完整模块 spec:`docs/_overview/sandbox-replay-engine-spec.md` §5.5(thin wrapper 决策)
## 背景
#5 累计 GMV 是 Sprint 2 第 5(末)个迁移指标。**与 #1-#4 不同,这是门店级指标**(无 member_id 参数),且无原 `fdw_queries` 函数。
## 改动清单
### Step 3 实施
**新增文件**(1 个):
- [apps/backend/app/services/sandbox_replay/finance_replay.py](apps/backend/app/services/sandbox_replay/finance_replay.py) — 新建模块,实现门店级 `get_total_gmv` 函数
**修改文件**(1 个):
- [apps/backend/app/services/sandbox_replay/__init__.py](apps/backend/app/services/sandbox_replay/__init__.py) — re-export `get_total_gmv_replay`
**未改文件**:
- `apps/backend/app/services/fdw_queries.py`**不加 thin wrapper**(原无同名查询 + 0 调用方,详见 spec §5.5 决策原则,与 #2 同款)
**测试文件**(本地不入仓 `.gitignore:71`):
- [apps/backend/tests/test_sandbox_replay_sprint2.py](apps/backend/tests/test_sandbox_replay_sprint2.py) — 累计 18 case(本指标 4 case)
测试覆盖:
- 正常返回 Decimal
- 无数据返回 Decimal('0')(SQL 层 COALESCE 兜底)
- sandbox 模式 SQL 显式带 `stat_date <= ctx.business_date` 上界 + 不能含 LIMIT 1
- **防御性签名回归测试**:`get_total_gmv` 函数签名不能含 `member_id` / `member_ids` 参数(门店级粒度,与 #1-#4 区分)
合并 sprint 1 + sprint 2 全部测试套件:**28/28 PASS,无回归**。
### 与 #1-#4 关键差异
| 维度 | #1/#2/#4(会员级) | **#5 累计 GMV(门店级)** |
|------|-------------------|-----------------------|
| 函数签名 | `(conn, site_id, member_id/ids, *, etl_conn, ctx)` | `(conn, site_id, *, etl_conn, ctx)` 无 member |
| SQL 查询模式 | 取最新 stat_date 行 ORDER BY DESC LIMIT 1 | **多行 SUM 累计** WHERE stat_date <= ref_date,无 LIMIT |
| 无数据返回 | None(单行查不到)| **Decimal('0')**(SUM=NULL 时 COALESCE 兜底) |
| fdw_queries thin wrapper | #1/#4 写,#2 不写 | 不写(无原查询) |
### 口径(BD_manual_dws_finance_daily_summary §5,148)
```
gross_amount = table_fee_amount + goods_amount
+ assistant_pd_amount + assistant_cx_amount
# 注意:不含 electricity_money(与会员级 items_sum 略有不同)
```
当前生产 `electricity_money` 全 0(DWD-DOC 第 5 条规则),实际数值无差,但语义层文档化,避免与 #2 累计消费总额(items_sum 含 electricity)交叉验证。
### Step 4 双口径数值验证(无 UI,直接 Python)
**目标 site**: 2790685415443269(朗朗桌球)
**双口径结果**(直接 Python 调用 `sandbox_replay.finance_replay.get_total_gmv`):
| 维度 | 4a live (today=2026-05-05) | 4b sandbox=2026-04-20 | 差异 |
|------|---------------------------|----------------------|------|
| 累计 GMV | **¥5,725,837.51** | **¥5,653,063.37** | ¥72,774.14 |
| 数据范围 | 2025-07-16 ~ 2026-04-27(测试库 ETL 现状)| 同 ~ 2026-04-20 | 4-21 ~ 4-27 七天合计 |
走查脚本(归档):
- `_DEL/walkthrough_f1_6/step_sprint2_total_gmv_4a_4b.py` — 一键完成 4a 验证 + sandbox 切换 + 4b 验证 + 切回 live
**关键证据**:sandbox 切换时,`get_total_gmv` 函数读 `ctx.business_date=2026-04-20` 作为 SQL 上界,SUM 271 天数据中 stat_date <= 2026-04-20 的部分,完全符合"沙箱时光机"语义。
> ⚠️ 测试库 ETL 现状:最新数据 stat_date=2026-04-27 而非 today=2026-05-05。SQL 语义正确(`stat_date <= ref_date` 数据库返回什么就什么),不影响 sandbox 时光机验证。生产环境 ETL 跑批到当天该问题自动消失。
### Step 5 审计
本文件 + F1-6-tasks.md Sprint 2 收尾推进 + audit dashboard 刷新。
## 影响范围
| 端 | 影响 | 验证 |
|----|------|------|
| 后端 sandbox_replay.finance_replay | **新增** 模块 + `get_total_gmv` 函数 | unit test 4/4 + 累计 28/28 PASS |
| 后端 fdw_queries | **无影响**(不加 thin wrapper) | — |
| 后端 board_service.get_finance_overview(区间 GMV)| 无影响(语义不同,两者并存) | — |
| 后端 chat / coach / customer / task_manager | 无影响(0 现有调用方) | — |
| 小程序 / admin-web / 租户后台 / ETL | 无影响(0 现有调用方) | — |
## 测试
- 后端 unit test 28/28 PASS(本地 `apps/backend/tests/test_sandbox_replay_sprint{1,2}.py`,因 `.gitignore:71` 不入仓)
- 直接 Python 双口径数值验证 PASS(¥5,725,837 vs ¥5,653,063,差异 ¥72,774)
- **无 UI 走查**(本指标 0 调用方,Neo 已确认接受路径 A,理由 AI app7 + 新版本迭代)
## 风险与未覆盖(已逐一控制)
- **风险 1 门店级新模式**:函数签名严格设计无 member_ids 参数,模块 docstring 明确"门店级",新增防御性签名回归测试 `test_get_total_gmv_no_member_ids_param` 阻断未来误改 ✓
- **风险 2 gross_amount 不含 electricity**:docstring 明确口径来源 + 与 #2 items_sum 不能交叉验证;生产 electricity_money 全 0 实际数值无差,语义层文档化 ✓
- **风险 3 测试库 ETL 仅到 04-27**:unit test 用 mock 数据(不依赖 ETL 跑批进度);audit 文档明确说明 4a 实际累计到 04-27 是测试库现状;数值差异(¥72k+)依然真实 ✓
- **风险 4 隐藏决策(无数据 0 vs None)**:SQL 层 `COALESCE(SUM(gross_amount), 0)` 兜底返回 Decimal('0');业务语义"开店前累计 GMV = 0"(没营收),不是"未知";单独测试用例覆盖 ✓
## 与架构演进 backlog 关联
本指标**直读 dws 视图**(通过 `app.v_dws_finance_daily_summary`),长远应通过 Core 层统一接口(详见 [架构演进 backlog](../../_overview/architecture-evolution-backlog.md) 第 4-5 项)。
## 回滚策略
```bash
git revert <commit_hash>
```
回滚后:
- 删除 `sandbox_replay/finance_replay.py` 新建文件
- `sandbox_replay/__init__.py` 移除 `get_total_gmv_replay` re-export
- 0 调用点无影响(本指标无现有调用方)
- 测试文件本地保留(`.gitignore` 范围内)
## Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>