feat(backend): F1-6 sprint2 #2 累计消费总额加入 sandbox_replay

新增指标(无 fdw_queries 原查询 + 0 现有调用方 + 无 thin wrapper),沿用
sprint 1/sprint2 #1 模式 @trace_service + @runtime_aware decorator + 显式
stat_date <= ctx.business_date 上界 + dws_member_consumption_summary
.total_consume_amount 字段 items_sum 口径。

双口径数值验证 PASS(member=2799207087163141 黄先生,直接 Python 调用):
- 4a live(today=2026-05-05): get_total_consume_amount=1252.65
- 4b sandbox=2026-04-20: get_total_consume_amount=999.99(walkthrough 测试快照)

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

记录 thin wrapper 决策原则到 spec §5.5(迁移辅助层,非常态架构;
fdw_queries 长远退化纯 ETL 物理访问层,清理放收尾 sprint)。

注:#3 累计交易笔数因 spec §4 字段未明确(dws_order_summary vs
total_visit_count)暂停,等 Neo 决断后继续。

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-06 00:52:08 +08:00
parent d418621951
commit 32716bc71a
6 changed files with 211 additions and 6 deletions

View File

@@ -35,10 +35,12 @@ 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,
get_total_consume_amount as get_total_consume_amount_replay,
)
__all__ = [
"runtime_aware",
"get_last_visit_days_replay",
"get_consumption_60d_replay",
"get_total_consume_amount_replay",
]

View File

@@ -155,3 +155,68 @@ def get_consumption_60d(
row = cur.fetchone()
return Decimal(str(row[0])) if row and row[0] is not None else None
# ── P1-3 累计消费总额 ────────────────────────────────────
@trace_service(
description_zh="获取累计消费总额(sandbox_replay)",
description_en="Get total consume amount (sandbox_replay)",
)
@runtime_aware(metric="total_consume_amount")
def get_total_consume_amount(
conn: Any,
site_id: int,
member_id: int,
*,
etl_conn: Any = None,
ctx: RuntimeContext,
) -> Decimal | None:
"""查询会员从注册至 ref_date 的累计消费总额(sandbox_replay 版本)。
设计要点:
- **新增指标**:fdw_queries 无原查询,sandbox_replay 直接落地
- 取业务日为基准(ctx.business_date,sandbox 模式下为 sandbox_date)
- 仅查 stat_date <= business_date 的快照行(显式上界,与视图过滤一致)
- 取最新 stat_date 行的 total_consume_amount
口径(BD_manual_dws_member_consumption_summary §金额口径):
- items_sum = table_charge_money + goods_money + assistant_pd_money
+ assistant_cx_money + electricity_money
- total_consume_amount 是 ETL 跑批时按 items_sum 累计的全期消费
- sandbox 切到历史日期时,视图按 stat_date <= business_date_now() 过滤,
取当时 ETL 写入的累计快照(符合"沙箱时光机"语义)
Args:
conn: zqyy_app 业务库连接
site_id: 门店 ID
member_id: 单个会员 ID
etl_conn: 可选,显式传 ETL 连接(便于测试 mock)
ctx: RuntimeContext(由 @runtime_aware 自动注入)
Returns:
Decimal 累计金额或 None(无快照 / 字段为 NULL 时)
使用方:暂未接入(Sprint 2 #2 留给 AI app7 客户分析 prompt 拼接 +
新版本迭代直接 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 total_consume_amount
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