Files
Neo-ZQYY/scripts/ops/member_consumption_report.py
2026-03-15 10:15:02 +08:00

223 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
会员消费报表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()