1
This commit is contained in:
222
scripts/ops/member_consumption_report.py
Normal file
222
scripts/ops/member_consumption_report.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
会员消费报表:2025-11-01 至今,按手机号归总,消费金额从高到低
|
||||
口径:items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money
|
||||
台费拆分:通过 dws.cfg_area_category 配置,category_code='KTV' 为 K包,其余为一般包厢台费
|
||||
会员信息:通过 member_id LEFT JOIN dim_member 获取 mobile/nickname
|
||||
(12月起 settlement_head.member_phone 为空,必须走 dim_member)
|
||||
输出:CSV → export/SYSTEM/REPORTS/member_reports/
|
||||
"""
|
||||
import os
|
||||
import csv
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
PG_DSN = os.environ.get("TEST_DB_DSN") or os.environ.get("PG_DSN")
|
||||
if not PG_DSN:
|
||||
raise RuntimeError("TEST_DB_DSN / PG_DSN 未配置")
|
||||
|
||||
SYSTEM_ANALYZE_ROOT = os.environ.get("SYSTEM_ANALYZE_ROOT")
|
||||
if not SYSTEM_ANALYZE_ROOT:
|
||||
raise RuntimeError("SYSTEM_ANALYZE_ROOT 未配置")
|
||||
|
||||
import psycopg2
|
||||
|
||||
SQL = r"""
|
||||
WITH date_range AS (
|
||||
SELECT '2025-11-01'::date AS start_date, CURRENT_DATE AS end_date
|
||||
),
|
||||
|
||||
area_mapping AS (
|
||||
SELECT source_area_name, source_table_name, category_code
|
||||
FROM dws.cfg_area_category
|
||||
WHERE is_active = true AND match_type = 'EXACT'
|
||||
),
|
||||
|
||||
-- 先筛出有会员的结算单(member_id 有值,或 member_phone 有值)
|
||||
settle_ids AS (
|
||||
SELECT sh.order_settle_id
|
||||
FROM dwd.dwd_settlement_head sh
|
||||
CROSS JOIN date_range dr
|
||||
WHERE sh.settle_type IN (1, 3)
|
||||
AND sh.pay_time >= dr.start_date
|
||||
AND sh.pay_time < dr.end_date + 1
|
||||
AND (
|
||||
(sh.member_id IS NOT NULL AND sh.member_id != 0)
|
||||
OR (sh.member_phone IS NOT NULL AND sh.member_phone != '')
|
||||
)
|
||||
),
|
||||
|
||||
table_fee_split AS (
|
||||
SELECT
|
||||
tfl.order_settle_id,
|
||||
SUM(CASE WHEN COALESCE(am.category_code, 'OTHER') = 'KTV'
|
||||
THEN tfl.ledger_amount ELSE 0 END) AS k_room_fee,
|
||||
SUM(CASE WHEN COALESCE(am.category_code, 'OTHER') != 'KTV'
|
||||
THEN tfl.ledger_amount ELSE 0 END) AS normal_table_fee
|
||||
FROM dwd.dwd_table_fee_log tfl
|
||||
INNER JOIN settle_ids si ON tfl.order_settle_id = si.order_settle_id
|
||||
LEFT JOIN dwd.dim_table dt
|
||||
ON dt.table_id = tfl.site_table_id AND dt.scd2_is_current = 1
|
||||
LEFT JOIN area_mapping am
|
||||
ON tfl.site_table_area_name = am.source_area_name
|
||||
AND dt.table_name = am.source_table_name
|
||||
WHERE tfl.is_delete = 0
|
||||
GROUP BY tfl.order_settle_id
|
||||
),
|
||||
|
||||
-- 关联 dim_member 获取手机号,优先 dim_member.mobile,回退 settlement_head.member_phone
|
||||
consumption AS (
|
||||
SELECT
|
||||
sh.order_settle_id,
|
||||
COALESCE(NULLIF(dm.mobile, ''), NULLIF(sh.member_phone, '')) AS phone,
|
||||
sh.pay_time,
|
||||
COALESCE(sh.table_charge_money, 0)
|
||||
+ COALESCE(sh.goods_money, 0)
|
||||
+ COALESCE(sh.assistant_pd_money, 0)
|
||||
+ COALESCE(sh.assistant_cx_money, 0)
|
||||
+ COALESCE(sh.electricity_money, 0) AS items_sum,
|
||||
COALESCE(sh.goods_money, 0) AS goods_money,
|
||||
COALESCE(sh.assistant_pd_money, 0) + COALESCE(sh.assistant_cx_money, 0) AS assistant_money
|
||||
FROM dwd.dwd_settlement_head sh
|
||||
CROSS JOIN date_range dr
|
||||
LEFT JOIN dwd.dim_member dm
|
||||
ON dm.member_id = sh.member_id AND dm.scd2_is_current = 1
|
||||
WHERE sh.settle_type IN (1, 3)
|
||||
AND sh.pay_time >= dr.start_date
|
||||
AND sh.pay_time < dr.end_date + 1
|
||||
AND (
|
||||
(sh.member_id IS NOT NULL AND sh.member_id != 0)
|
||||
OR (sh.member_phone IS NOT NULL AND sh.member_phone != '')
|
||||
)
|
||||
),
|
||||
|
||||
-- 过滤掉最终仍无手机号的记录
|
||||
consumption_with_phone AS (
|
||||
SELECT * FROM consumption WHERE phone IS NOT NULL
|
||||
),
|
||||
|
||||
monthly AS (
|
||||
SELECT
|
||||
c.phone,
|
||||
EXTRACT(YEAR FROM c.pay_time)::int AS yr,
|
||||
EXTRACT(MONTH FROM c.pay_time)::int AS mo,
|
||||
SUM(c.items_sum) AS month_total,
|
||||
SUM(c.goods_money) AS month_goods,
|
||||
SUM(c.assistant_money) AS month_assistant,
|
||||
SUM(COALESCE(tf.k_room_fee, 0)) AS month_k_room,
|
||||
SUM(COALESCE(tf.normal_table_fee, 0)) AS month_normal_table
|
||||
FROM consumption_with_phone c
|
||||
LEFT JOIN table_fee_split tf ON c.order_settle_id = tf.order_settle_id
|
||||
GROUP BY c.phone, yr, mo
|
||||
),
|
||||
|
||||
member_agg AS (
|
||||
SELECT
|
||||
phone,
|
||||
SUM(month_total) AS total_consumption,
|
||||
SUM(CASE WHEN yr = 2025 AND mo = 11 THEN month_total ELSE 0 END) AS m11,
|
||||
SUM(CASE WHEN yr = 2025 AND mo = 12 THEN month_total ELSE 0 END) AS m12,
|
||||
SUM(CASE WHEN yr = 2026 AND mo = 1 THEN month_total ELSE 0 END) AS m01,
|
||||
SUM(CASE WHEN yr = 2026 AND mo = 2 THEN month_total ELSE 0 END) AS m02,
|
||||
SUM(CASE WHEN yr = 2026 AND mo = 3 THEN month_total ELSE 0 END) AS m03,
|
||||
SUM(month_k_room) AS k_room_total,
|
||||
SUM(month_normal_table) AS normal_table_total,
|
||||
SUM(month_assistant) AS assistant_total,
|
||||
SUM(month_goods) AS goods_total
|
||||
FROM monthly
|
||||
GROUP BY phone
|
||||
),
|
||||
|
||||
-- 昵称:合并 dim_member.nickname 和 settlement_head.member_name
|
||||
member_names AS (
|
||||
SELECT
|
||||
COALESCE(NULLIF(dm.mobile, ''), NULLIF(sh.member_phone, '')) AS phone,
|
||||
STRING_AGG(
|
||||
DISTINCT COALESCE(NULLIF(dm.nickname, ''), NULLIF(sh.member_name, '')),
|
||||
' | '
|
||||
ORDER BY COALESCE(NULLIF(dm.nickname, ''), NULLIF(sh.member_name, ''))
|
||||
) FILTER (
|
||||
WHERE COALESCE(NULLIF(dm.nickname, ''), NULLIF(sh.member_name, '')) IS NOT NULL
|
||||
) AS names
|
||||
FROM dwd.dwd_settlement_head sh
|
||||
CROSS JOIN date_range dr
|
||||
LEFT JOIN dwd.dim_member dm
|
||||
ON dm.member_id = sh.member_id AND dm.scd2_is_current = 1
|
||||
WHERE sh.settle_type IN (1, 3)
|
||||
AND sh.pay_time >= dr.start_date
|
||||
AND sh.pay_time < dr.end_date + 1
|
||||
AND (
|
||||
(sh.member_id IS NOT NULL AND sh.member_id != 0)
|
||||
OR (sh.member_phone IS NOT NULL AND sh.member_phone != '')
|
||||
)
|
||||
AND COALESCE(NULLIF(dm.mobile, ''), NULLIF(sh.member_phone, '')) IS NOT NULL
|
||||
GROUP BY phone
|
||||
)
|
||||
|
||||
SELECT
|
||||
COALESCE(mn.names, '') AS "会员昵称",
|
||||
ma.phone AS "手机号",
|
||||
ROUND(ma.total_consumption, 2) AS "11月至今共消费",
|
||||
ROUND(ma.m11, 2) AS "11月消费共计",
|
||||
ROUND(ma.m12, 2) AS "12月消费共计",
|
||||
ROUND(ma.m01, 2) AS "1月消费共计",
|
||||
ROUND(ma.m02, 2) AS "2月消费共计",
|
||||
ROUND(ma.m03, 2) AS "3月消费共计",
|
||||
ROUND(ma.k_room_total, 2) AS "K包",
|
||||
CASE WHEN ma.total_consumption > 0
|
||||
THEN ROUND(ma.k_room_total / ma.total_consumption * 100, 1)
|
||||
ELSE 0 END AS "K包占比%",
|
||||
ROUND(ma.normal_table_total, 2) AS "一般包厢台费",
|
||||
CASE WHEN ma.total_consumption > 0
|
||||
THEN ROUND(ma.normal_table_total / ma.total_consumption * 100, 1)
|
||||
ELSE 0 END AS "一般包厢台费占比%",
|
||||
ROUND(ma.assistant_total, 2) AS "助教费",
|
||||
CASE WHEN ma.total_consumption > 0
|
||||
THEN ROUND(ma.assistant_total / ma.total_consumption * 100, 1)
|
||||
ELSE 0 END AS "助教费占比%",
|
||||
ROUND(ma.goods_total, 2) AS "商品费",
|
||||
CASE WHEN ma.total_consumption > 0
|
||||
THEN ROUND(ma.goods_total / ma.total_consumption * 100, 1)
|
||||
ELSE 0 END AS "商品费占比%"
|
||||
FROM member_agg ma
|
||||
LEFT JOIN member_names mn ON ma.phone = mn.phone
|
||||
ORDER BY ma.total_consumption DESC
|
||||
"""
|
||||
|
||||
|
||||
def run_report():
|
||||
with psycopg2.connect(PG_DSN, connect_timeout=15, options="-c statement_timeout=300000") as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(SQL)
|
||||
columns = [desc[0] for desc in cur.description]
|
||||
rows = cur.fetchall()
|
||||
|
||||
report_dir = os.path.join(os.path.dirname(SYSTEM_ANALYZE_ROOT), "member_reports")
|
||||
os.makedirs(report_dir, exist_ok=True)
|
||||
|
||||
csv_path = os.path.join(report_dir, "member_consumption_202511_to_now.csv")
|
||||
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(columns)
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f"✅ 报表已生成: {csv_path}")
|
||||
print(f" 共 {len(rows)} 位会员")
|
||||
|
||||
print(f"\n{'='*200}")
|
||||
header = " | ".join(f"{c:>14}" if i > 1 else f"{c:<20}" for i, c in enumerate(columns))
|
||||
print(header)
|
||||
print(f"{'='*200}")
|
||||
for row in rows[:20]:
|
||||
line = " | ".join(
|
||||
f"{str(v):<20}" if i <= 1 else f"{str(v):>14}"
|
||||
for i, v in enumerate(row)
|
||||
)
|
||||
print(line)
|
||||
if len(rows) > 20:
|
||||
print(f"... 还有 {len(rows) - 20} 行(见 CSV 文件)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_report()
|
||||
Reference in New Issue
Block a user