开发机迁移
This commit is contained in:
@@ -937,18 +937,18 @@ def get_consumption_60d(
|
||||
"""
|
||||
查询客户近 60 天消费金额。
|
||||
|
||||
来源: app.v_dwd_assistant_service_log。
|
||||
⚠️ DWD-DOC 规则 1: 使用 ledger_amount(items_sum 口径)。
|
||||
⚠️ 废单排除: is_delete = 0。
|
||||
来源: app.v_dws_member_consumption_summary(DWS 预聚合表)。
|
||||
与 board-customer spend60 维度统一口径:items_sum,60天窗口,日粒度。
|
||||
取最新 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-6:sv_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-1:member_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-3:visits_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-4:consume_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 | Fix:consumption_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:同 spend60,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,
|
||||
)
|
||||
@@ -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_head(settle_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),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user