开发机迁移

This commit is contained in:
Neo
2026-04-10 06:24:13 +08:00
parent f65c1d038b
commit 79d3c2e97e
50 changed files with 1565 additions and 318 deletions

View File

@@ -937,18 +937,18 @@ def get_consumption_60d(
"""
查询客户近 60 天消费金额。
来源: app.v_dwd_assistant_service_log
⚠️ DWD-DOC 规则 1: 使用 ledger_amountitems_sum 口径)
⚠️ 废单排除: is_delete = 0
来源: app.v_dws_member_consumption_summaryDWS 预聚合表)
与 board-customer spend60 维度统一口径items_sum60天窗口日粒度
取最新 stat_date 的快照行
"""
with _fdw_context(conn, site_id, etl_conn=etl_conn) as cur:
cur.execute(
"""
SELECT COALESCE(SUM(ledger_amount), 0)
FROM app.v_dwd_assistant_service_log
WHERE tenant_member_id = %s
AND is_delete = 0
AND create_time >= (CURRENT_DATE - INTERVAL '60 days')::timestamptz
SELECT consume_amount_60d
FROM app.v_dws_member_consumption_summary
WHERE member_id = %s
ORDER BY stat_date DESC
LIMIT 1
""",
(member_id,),
)
@@ -1729,6 +1729,8 @@ def get_coach_sv_data(
result: dict[int, dict] = {}
with _fdw_context(conn, site_id) as cur:
# CHANGE 2026-04-07 | Fix-6sv_consume 改为从结算表按 start_date/end_date 过滤,
# 使其随时间筛选联动,而非固定 60 天窗口。
cur.execute(
"""
WITH coach_members AS (
@@ -1737,11 +1739,21 @@ def get_coach_sv_data(
FROM app.v_dws_member_assistant_relation_index ri
WHERE ri.assistant_id = ANY(%s)
AND ri.session_count > 0
),
period_consume AS (
SELECT sh.member_id,
COALESCE(SUM(sh.items_sum), 0) AS consume_amount
FROM app.v_dwd_settlement_head sh
WHERE sh.member_id = ANY(SELECT member_id FROM coach_members)
AND sh.settle_type IN (1, 3)
AND sh.pay_time::date >= %s::date
AND sh.pay_time::date <= %s::date
GROUP BY sh.member_id
)
SELECT cm.assistant_id,
COALESCE(SUM(ca_agg.balance), 0) AS sv_amount,
COUNT(DISTINCT CASE WHEN ca_agg.balance > 0 THEN cm.member_id END) AS sv_customer_count,
COALESCE(SUM(cs.consume_amount_60d), 0) AS sv_consume
COALESCE(SUM(pc.consume_amount), 0) AS sv_consume
FROM coach_members cm
LEFT JOIN (
SELECT tenant_member_id, SUM(balance) AS balance
@@ -1749,11 +1761,11 @@ def get_coach_sv_data(
WHERE scd2_is_current = 1
GROUP BY tenant_member_id
) ca_agg ON cm.member_id = ca_agg.tenant_member_id
LEFT JOIN app.v_dws_member_consumption_summary cs
ON cm.member_id = cs.member_id
LEFT JOIN period_consume pc
ON cm.member_id = pc.member_id
GROUP BY cm.assistant_id
""",
(assistant_ids,),
(assistant_ids, start_date, end_date),
)
for row in cur.fetchall():
result[row[0]] = {
@@ -1769,19 +1781,20 @@ def get_coach_sv_data(
# ---------------------------------------------------------------------------
def _project_filter_clause(project: str) -> tuple[str, tuple]:
def _project_filter_clause(project: str, member_col: str = "member_id") -> tuple[str, tuple]:
"""
生成项目筛选 SQL 片段(用于 BOARD-2 会员维度查询)。
CHANGE 2026-03-20 | R3 修复project 参数直接接收 category_code
BILLIARD/SNOOKER/MAHJONG/KTV/ALL去掉 chinese→BILLIARD 映射层。
CHANGE 2026-04-07 | Fix-1member_col 参数化,修复 6 个维度别名不匹配导致 SQL 500。
返回 (sql_fragment, params)sql_fragment 以 AND 开头,可直接拼入 WHERE 子句。
"""
_valid_categories = {"BILLIARD", "SNOOKER", "MAHJONG", "KTV"}
if project == "ALL" or project not in _valid_categories:
return "", ()
clause = """
AND vd.member_id IN (
clause = f"""
AND {member_col} IN (
SELECT mpt.member_id
FROM app.v_dws_member_project_tag mpt
WHERE mpt.category_code = %s AND mpt.is_tagged = true
@@ -1802,13 +1815,13 @@ def get_customer_board_recall(
ideal_days → ideal_interval_days, wbi_score → display_score,
elapsed_days → CURRENT_DATE - last_visit_time::date (计算列),
overdue_days → elapsed_days - ideal_interval_days (计算列),
visits_30d 不存在(有 visits_14d/visits_60d),用 visits_14d 近似,
CHANGE 2026-04-07 | Fix-3visits_30d 新增字段,替代 visits_14d 近似,
balance_amount → balance (v_dim_member_card_account)
⚠️ DQ-6: 客户姓名通过 member_id JOIN v_dim_member。
⚠️ DQ-7: 余额通过 JOIN v_dim_member_card_account。
按 display_score 降序LIMIT/OFFSET 分页。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "wi.member_id")
with _fdw_context(conn, site_id) as cur:
# 总数
@@ -1817,6 +1830,7 @@ def get_customer_board_recall(
SELECT COUNT(*)
FROM app.v_dws_member_winback_index wi
WHERE 1=1 {proj_clause}
""",
proj_params,
)
@@ -1831,7 +1845,7 @@ def get_customer_board_recall(
wi.ideal_interval_days,
CURRENT_DATE - wi.last_visit_time::date AS elapsed_days,
(CURRENT_DATE - wi.last_visit_time::date) - COALESCE(wi.ideal_interval_days, 0) AS overdue_days,
wi.visits_14d,
wi.visits_30d,
wi.display_score,
COALESCE(ca.balance, 0) AS balance
FROM app.v_dws_member_winback_index wi
@@ -1844,7 +1858,7 @@ def get_customer_board_recall(
GROUP BY tenant_member_id
) ca ON wi.member_id = ca.tenant_member_id
WHERE 1=1 {proj_clause}
ORDER BY wi.display_score DESC
ORDER BY wi.display_score DESC, wi.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -1870,16 +1884,18 @@ def _derive_potential_tags(
level_score: float | None,
speed_score: float | None,
stability_score: float | None,
) -> list[str]:
"""从三维分数派生潜力标签display_score 为 0-100 区间)。"""
tags = []
threshold = 60.0
) -> list[dict]:
"""从三维分数派生潜力标签display_score 为 0-10 区间)。
返回 [{text, theme}] 格式,与前端 potentialTags 类型一致。
"""
tags: list[dict] = []
threshold = 6.0
if level_score is not None and float(level_score) >= threshold:
tags.append("high_level")
tags.append({"text": "高消费力", "theme": "success"})
if speed_score is not None and float(speed_score) >= threshold:
tags.append("fast_growth")
tags.append({"text": "快增长", "theme": "warning"})
if stability_score is not None and float(stability_score) >= threshold:
tags.append("stable")
tags.append({"text": "稳定", "theme": "primary"})
return tags
@@ -1934,7 +1950,7 @@ def get_customer_board_potential(
) ca_agg ON spi.member_id = ca_agg.tenant_member_id
WHERE 1=1
{f"AND spi.member_id IN (SELECT member_id FROM app.v_dws_member_project_tag WHERE category_code = %s AND is_tagged = true)" if proj_params else ""}
ORDER BY spi.display_score DESC NULLS LAST, COALESCE(ca_agg.balance, 0) DESC
ORDER BY spi.display_score DESC NULLS LAST, COALESCE(ca_agg.balance, 0) DESC, spi.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -1969,7 +1985,7 @@ def get_customer_board_balance(
⚠️ DQ-7: 余额通过 tenant_member_id JOIN取 scd2_is_current=1。
按 balance 降序。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "ca.tenant_member_id")
with _fdw_context(conn, site_id) as cur:
# CHANGE 2026-03-28 | 修复客户重复dim_member_card_account 同一 member 有多条记录,
@@ -2009,9 +2025,14 @@ def get_customer_board_balance(
) ca_agg
LEFT JOIN app.v_dim_member dm
ON ca_agg.member_id = dm.member_id AND dm.scd2_is_current = 1
LEFT JOIN app.v_dws_member_consumption_summary vd
ON ca_agg.member_id = vd.member_id
ORDER BY ca_agg.balance DESC
LEFT JOIN LATERAL (
SELECT cs.days_since_last, cs.consume_amount_60d
FROM app.v_dws_member_consumption_summary cs
WHERE cs.member_id = ca_agg.member_id
ORDER BY cs.stat_date DESC
LIMIT 1
) vd ON true
ORDER BY ca_agg.balance DESC, ca_agg.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -2026,9 +2047,10 @@ def get_customer_board_balance(
"last_visit": f"{row[3]}天前" if row[3] is not None else "--",
"last_visit_date": row[3],
"ideal_days": None, # balance 维度无 ideal_days由 board_service 补充
"monthly_consume": float(row[4]) if row[4] is not None else 0.0,
# CHANGE 2026-04-07 | Fix-4consume_amount_60d 是 60 天总额,月均 = /2
"monthly_consume": float(row[4]) / 2 if row[4] is not None else 0.0,
"available_months": (
f"{float(row[2]) / float(row[4]):.1f}个月"
f"{2 * float(row[2]) / float(row[4]):.1f}个月"
if row[2] and row[4] and float(row[4]) > 0
else "--"
),
@@ -2050,7 +2072,7 @@ def get_customer_board_recharge(
balance_amount → balance (v_dim_member_card_account)
按 last_recharge_date (MAX(pay_time::date)) 降序。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "ro.member_id")
with _fdw_context(conn, site_id) as cur:
cur.execute(
@@ -2084,11 +2106,16 @@ def get_customer_board_recharge(
WHERE scd2_is_current = 1
GROUP BY tenant_member_id
) ca_agg ON ro.member_id = ca_agg.tenant_member_id
LEFT JOIN app.v_dws_member_consumption_summary cs
ON ro.member_id = cs.member_id
LEFT JOIN LATERAL (
SELECT cs2.days_since_last
FROM app.v_dws_member_consumption_summary cs2
WHERE cs2.member_id = ro.member_id
ORDER BY cs2.stat_date DESC
LIMIT 1
) cs ON true
WHERE 1=1 {proj_clause}
GROUP BY ro.member_id, dm.nickname, ca_agg.balance, cs.days_since_last
ORDER BY MAX(ro.pay_time::date) DESC
ORDER BY MAX(ro.pay_time::date) DESC, ro.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -2121,7 +2148,7 @@ def get_customer_board_recent(
不再硬编码为 0。来源: v_dws_member_visit_detail + v_dim_member + v_dws_member_winback_index。
按 last_visit_date 降序。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "vd.member_id")
with _fdw_context(conn, site_id) as cur:
cur.execute(
@@ -2161,7 +2188,7 @@ def get_customer_board_recent(
ON ma.member_id = dm.member_id AND dm.scd2_is_current = 1
LEFT JOIN app.v_dws_member_winback_index wi
ON ma.member_id = wi.member_id
ORDER BY ma.last_visit_date DESC
ORDER BY ma.last_visit_date DESC, ma.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -2199,15 +2226,21 @@ def get_customer_board_spend60(
high_spend_tag/avg_spend 不存在,用 avg_ticket_amount 替代 avg_spend
high_spend_tag 通过阈值计算。
按 consume_amount_60d 降序。
CHANGE 2026-04-08 | Fixconsumption_summary 按 stat_date 有多行快照,
用 DISTINCT ON 取最新快照避免同一客户出现多次。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "cs.member_id")
with _fdw_context(conn, site_id) as cur:
cur.execute(
f"""
SELECT COUNT(*)
FROM app.v_dws_member_consumption_summary cs
WHERE 1=1 {proj_clause}
FROM (
SELECT DISTINCT ON (cs.member_id) cs.member_id
FROM app.v_dws_member_consumption_summary cs
WHERE 1=1 {proj_clause}
ORDER BY cs.member_id, cs.stat_date DESC
) sub
""",
proj_params,
)
@@ -2216,16 +2249,23 @@ def get_customer_board_spend60(
offset = (page - 1) * page_size
cur.execute(
f"""
WITH latest_cs AS (
SELECT DISTINCT ON (cs.member_id)
cs.member_id, cs.consume_amount_60d,
cs.visit_count_60d, cs.avg_ticket_amount
FROM app.v_dws_member_consumption_summary cs
WHERE 1=1 {proj_clause}
ORDER BY cs.member_id, cs.stat_date DESC
)
SELECT cs.member_id,
dm.nickname,
cs.consume_amount_60d,
cs.visit_count_60d,
cs.avg_ticket_amount
FROM app.v_dws_member_consumption_summary cs
FROM latest_cs cs
LEFT JOIN app.v_dim_member dm
ON cs.member_id = dm.member_id AND dm.scd2_is_current = 1
WHERE 1=1 {proj_clause}
ORDER BY cs.consume_amount_60d DESC
ORDER BY cs.consume_amount_60d DESC, cs.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -2256,15 +2296,20 @@ def get_customer_board_freq60(
CHANGE 2026-03-20 | 修正列名items_sum_60d → consume_amount_60d,
avg_interval_days 不存在,用 60/visit_count_60d 近似计算。
按 visit_count_60d 降序。
CHANGE 2026-04-08 | Fix同 spend60DISTINCT ON 取最新快照。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "cs.member_id")
with _fdw_context(conn, site_id) as cur:
cur.execute(
f"""
SELECT COUNT(*)
FROM app.v_dws_member_consumption_summary cs
WHERE 1=1 {proj_clause}
FROM (
SELECT DISTINCT ON (cs.member_id) cs.member_id
FROM app.v_dws_member_consumption_summary cs
WHERE 1=1 {proj_clause}
ORDER BY cs.member_id, cs.stat_date DESC
) sub
""",
proj_params,
)
@@ -2273,15 +2318,21 @@ def get_customer_board_freq60(
offset = (page - 1) * page_size
cur.execute(
f"""
WITH latest_cs AS (
SELECT DISTINCT ON (cs.member_id)
cs.member_id, cs.visit_count_60d, cs.consume_amount_60d
FROM app.v_dws_member_consumption_summary cs
WHERE 1=1 {proj_clause}
ORDER BY cs.member_id, cs.stat_date DESC
)
SELECT cs.member_id,
dm.nickname,
cs.visit_count_60d,
cs.consume_amount_60d
FROM app.v_dws_member_consumption_summary cs
FROM latest_cs cs
LEFT JOIN app.v_dim_member dm
ON cs.member_id = dm.member_id AND dm.scd2_is_current = 1
WHERE 1=1 {proj_clause}
ORDER BY cs.visit_count_60d DESC
ORDER BY cs.visit_count_60d DESC, cs.member_id
LIMIT %s OFFSET %s
""",
(*proj_params, page_size, offset),
@@ -2316,20 +2367,21 @@ def _get_weekly_visits_batch(cur: Any, member_ids: list[int]) -> dict[int, list[
"""
批量查询客户最近 8 周的到店次数(用于 freq60 维度柱状图)。
来源: app.v_dwd_assistant_service_log,按 ISO 周分组。
CHANGE 2026-04-07 | Fix-5数据源从 v_dwd_assistant_service_log 改为
v_dwd_settlement_headsettle_type IN (1,3)),与汇总维度口径一致。
返回 {member_id: [{val: int, pct: int}, ...]},固定 8 个元素。
"""
cur.execute(
"""
WITH weekly AS (
SELECT tenant_member_id AS member_id,
DATE_TRUNC('week', create_time::date) AS week_start,
COUNT(*) AS cnt
FROM app.v_dwd_assistant_service_log
WHERE tenant_member_id = ANY(%s)
AND is_delete = 0
AND create_time >= CURRENT_DATE - INTERVAL '56 days'
GROUP BY tenant_member_id, DATE_TRUNC('week', create_time::date)
SELECT member_id,
DATE_TRUNC('week', pay_time::date) AS week_start,
COUNT(DISTINCT pay_time::date) AS cnt
FROM app.v_dwd_settlement_head
WHERE member_id = ANY(%s)
AND settle_type IN (1, 3)
AND pay_time >= CURRENT_DATE - INTERVAL '56 days'
GROUP BY member_id, DATE_TRUNC('week', pay_time::date)
)
SELECT member_id, week_start, cnt
FROM weekly
@@ -2379,7 +2431,7 @@ def get_customer_board_loyal(
来源: app.v_dws_member_assistant_relation_index。
按 max_rs最高亲密度降序。
"""
proj_clause, proj_params = _project_filter_clause(project)
proj_clause, proj_params = _project_filter_clause(project, "ri.member_id")
with _fdw_context(conn, site_id) as cur:
cur.execute(
@@ -2403,7 +2455,7 @@ def get_customer_board_loyal(
FROM app.v_dws_member_assistant_relation_index ri
WHERE 1=1 {proj_clause}
GROUP BY ri.member_id
ORDER BY MAX(ri.rs_display) DESC
ORDER BY MAX(ri.rs_display) DESC, ri.member_id
LIMIT %s OFFSET %s
)
SELECT mt.member_id,
@@ -2417,7 +2469,7 @@ def get_customer_board_loyal(
ON mt.member_id = dm.member_id AND dm.scd2_is_current = 1
LEFT JOIN app.v_dim_assistant da
ON mt.top_assistant_id = da.assistant_id AND da.scd2_is_current = 1
ORDER BY mt.max_rs DESC
ORDER BY mt.max_rs DESC, mt.member_id
""",
(*proj_params, page_size, offset),
)