feat(backend): F1-6 sprint2 #4 储值卡余额迁移 sandbox_replay (SCD2 时光机)

新建 sandbox_replay/balance_replay.py 模块,迁移 fdw_queries.get_member_balance,
fdw_queries 改 thin wrapper 保持 5 处现有调用(chat/coach/customer x2/task_manager)
透明兼容。

数据源 dim_member_card_account 是 SCD2 维度表(原生支持时光机),sandbox 改造
关键是替换 scd2_is_current=1 过滤为 scd2_start_time + scd2_end_time 时间过滤
(ref_date+1day 边界 = 当天结束时仍 active 的版本,timestamptz 比较稳定)。

双口径 UI 走查 PASS(member=2799207363643141 葛先生,SCD2 历史余额变化样本):
- 4a live(today=2026-05-05): 储值余额 ¥6,602
- 4b sandbox=2026-04-20: 储值余额 ¥18,080(差异 1.1w+,时光机效果显著)

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

附带本次 sprint 2 触发的架构级登记:
- 新建 docs/_overview/architecture-evolution-backlog.md(DWD 孤立 + Core 中间件 +
  库重组,长远架构演进 backlog)
- F1-6-tasks.md 登记 #3 累计交易笔数推迟 Sprint 3(ETL 配合新增
  total_open_table_count,因现有 total_visit_count 实算 COUNT(settle_type IN (1,3))
  含商城订单,不符 Neo "开台次数"业务语义)
- sandbox-replay-engine-spec §5.5 thin wrapper 决策原则(已在 #2 commit)

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

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

View File

@@ -182,38 +182,21 @@ def get_member_info(
return result
@trace_service(description_zh="获取会员余额", description_en="Get member balance")
def get_member_balance(
conn: Any, site_id: int, member_ids: list[int],
*, etl_conn: Any = None,
) -> dict[int, Decimal]:
"""thin wrapper — F1-6 sprint 2 #4 已迁移到 sandbox_replay.balance_replay。
保持原签名兼容现有 5 处调用(chat / coach / customer ×2 / task_manager)。
sandbox 时光机语义:走 SCD2 维度表的 scd2_start_time + scd2_end_time 时间过滤,
替代原 scd2_is_current=1(原仅 live 时刻有效)。
"""
批量查询会员储值卡余额。
from app.services.sandbox_replay.balance_replay import (
get_member_balance as _replay_impl,
)
⚠️ DQ-7: 通过 tenant_member_id 关联 app.v_dim_member_card_account
取 scd2_is_current=1禁止使用 settlement_head.member_card_type_name。
返回 {member_id: balance} 映射。
"""
if not member_ids:
return {}
result: dict[int, Decimal] = {}
with _fdw_context(conn, site_id, etl_conn=etl_conn) as cur:
# CHANGE 2026-03-29 | 修复多卡膨胀:同一客户多张卡需 SUM 聚合
cur.execute(
"""
SELECT tenant_member_id AS member_id, SUM(balance) AS balance
FROM app.v_dim_member_card_account
WHERE tenant_member_id = ANY(%s) AND scd2_is_current = 1
GROUP BY tenant_member_id
""",
(member_ids,),
)
for row in cur.fetchall():
result[row[0]] = Decimal(str(row[1])) if row[1] is not None else Decimal("0")
return result
return _replay_impl(conn, site_id, member_ids, etl_conn=etl_conn)
def get_last_visit_days(