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:
@@ -40,6 +40,9 @@ from app.services.sandbox_replay.consumption_replay import (
|
||||
get_last_visit_days as get_last_visit_days_replay,
|
||||
get_total_consume_amount as get_total_consume_amount_replay,
|
||||
)
|
||||
from app.services.sandbox_replay.finance_replay import (
|
||||
get_total_gmv as get_total_gmv_replay,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"runtime_aware",
|
||||
@@ -47,4 +50,5 @@ __all__ = [
|
||||
"get_consumption_60d_replay",
|
||||
"get_total_consume_amount_replay",
|
||||
"get_member_balance_replay",
|
||||
"get_total_gmv_replay",
|
||||
]
|
||||
|
||||
89
apps/backend/app/services/sandbox_replay/finance_replay.py
Normal file
89
apps/backend/app/services/sandbox_replay/finance_replay.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""门店财务相关 sandbox 重算实现。
|
||||
|
||||
F1-6 沙箱时光机阶段 B Sprint 2 #5 新建。
|
||||
|
||||
覆盖指标:
|
||||
- P1-13 累计 GMV(get_total_gmv)— sprint 2 #5
|
||||
|
||||
设计要点:
|
||||
- **门店级**指标(不是会员级),函数签名无 member_id 参数
|
||||
- 数据源:`dws_finance_daily_summary`(门店日度财务汇总,daily 累计)
|
||||
- sandbox 语义:SUM(gross_amount) WHERE stat_date <= ctx.business_date
|
||||
- 与 #1/#2(会员级单行查询)不同,本指标是多行 SUM 聚合
|
||||
- 无原 fdw_queries.get_total_gmv 函数 → **不写 thin wrapper**(改动最小化)
|
||||
|
||||
口径(BD_manual_dws_finance_daily_summary §字段说明):
|
||||
gross_amount = table_fee_amount + goods_amount
|
||||
+ assistant_pd_amount + assistant_cx_amount
|
||||
# 注意:**不含 electricity_money**(与会员级 items_sum 不同)
|
||||
# 当前生产 electricity_money 全 0(DWD-DOC 第 5 条规则),实际数值无差,
|
||||
# 但语义层文档化,避免与 #2 累计消费总额做语义混淆/交叉验证
|
||||
|
||||
与 board_service.get_finance_overview 的关系:
|
||||
`board_service.get_finance_overview` 已用 SUM(gross_amount) 但是按
|
||||
start_date / end_date 区间聚合(BOARD-3 经营一览面板),语义是"区间 GMV"。
|
||||
本指标是"开店至 ref_date 累计 GMV",**两者并存,语义不同,本指标不影响那个**。
|
||||
|
||||
无数据语义:
|
||||
开店前 / 无 dws 数据时 SUM=NULL,SQL 层用 COALESCE(SUM(...), 0) 兜底
|
||||
返回 Decimal('0'),不返回 None。业务语义:开店前累计 GMV = 0(没营收),
|
||||
不是"未知"。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from app.services.runtime_context import RuntimeContext
|
||||
from app.services.sandbox_replay._decorator import runtime_aware
|
||||
from app.trace.decorators import trace_service
|
||||
|
||||
|
||||
@trace_service(
|
||||
description_zh="获取累计 GMV(sandbox_replay,门店级)",
|
||||
description_en="Get cumulative GMV (sandbox_replay, store level)",
|
||||
)
|
||||
@runtime_aware(metric="total_gmv")
|
||||
def get_total_gmv(
|
||||
conn: Any,
|
||||
site_id: int,
|
||||
*,
|
||||
etl_conn: Any = None,
|
||||
ctx: RuntimeContext,
|
||||
) -> Decimal:
|
||||
"""查询门店从开店至 ref_date 的累计 GMV(sandbox_replay 版本)。
|
||||
|
||||
设计要点:
|
||||
- **门店级**指标(无 member_id 参数,与 #1-#4 会员级粒度不同)
|
||||
- 取业务日为基准(ctx.business_date,sandbox 模式下为 sandbox_date)
|
||||
- 仅 SUM stat_date <= business_date 的快照行(显式上界,与视图过滤一致)
|
||||
- SQL 层 COALESCE 兜底,无数据返回 Decimal('0')(非 None)
|
||||
|
||||
Args:
|
||||
conn: zqyy_app 业务库连接
|
||||
site_id: 门店 ID
|
||||
etl_conn: 可选,显式传 ETL 连接(便于测试 mock)
|
||||
ctx: RuntimeContext(由 @runtime_aware 自动注入)
|
||||
|
||||
Returns:
|
||||
Decimal 累计 GMV(无数据时返回 Decimal('0'),不返回 None)
|
||||
|
||||
使用方:暂未接入(Sprint 2 #5 留给 AI app7 + 新版本迭代场景直接 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 COALESCE(SUM(gross_amount), 0) AS total_gmv
|
||||
FROM app.v_dws_finance_daily_summary
|
||||
WHERE stat_date <= %s
|
||||
""",
|
||||
(ref_date,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
return Decimal(str(row[0])) if row and row[0] is not None else Decimal("0")
|
||||
Reference in New Issue
Block a user