ai_member_xiaoxi/scripts/sales_daily_report.py
2026-06-03 08:00:01 +08:00

376 lines
16 KiB
Python
Raw Permalink 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.

#!/usr/bin/env python3
"""
销售日报 v1 — 修复销转总览(计算值)+ 新建投放日报
"""
import json, requests, os, re, time, psycopg2
from collections import defaultdict
from datetime import datetime, timedelta
CRED_DIR = "/root/.openclaw/credentials/xiaoxi"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
SHEET_MAP = {"吴迪": "f975f0", "小龙": "qJF4I", "成都": "qJF4J"}
TARGET_MONTHS = [3, 4, 5, 6]
COSTS = {3: 243, 4: 246, 5: 241}
SALES_ORDER = ["小龙", "吴迪", "Bob", "Tom"]
PG_HOST = "bj-postgres-16pob4sg.sql.tencentcdb.com"
PG_PORT = 28591
PG_USER = "ai_member"
PG_PASSWORD = "LdfjdjL83h3h3^$&**YGG*"
PG_DB = "vala_bi"
BLUE = "#4472C4"; WHITE = "#FFFFFF"
def get_orders_for_accounts(account_ids):
if not account_ids: return {}, []
conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
cur = conn.cursor()
ph = ','.join(['%s'] * len(account_ids))
cur.execute(f"""
SELECT o.account_id, o.trade_no, o.pay_success_date, o.key_from,
o.pay_amount_int, o.order_status
FROM bi_vala_order o JOIN bi_vala_app_account a ON o.account_id = a.id AND a.status = 1
WHERE o.account_id IN ({ph}) AND o.pay_success_date IS NOT NULL AND o.order_status IN (3, 4)
ORDER BY o.account_id, o.pay_success_date
""", list(account_ids))
orders_by_account = defaultdict(list)
all_trade_nos = []
for row in cur.fetchall():
aid, trade_no, pay_date, key_from, amount, status = row
orders_by_account[aid].append({
'trade_no': trade_no, 'pay_date': str(pay_date)[:10] if pay_date else '',
'key_from': key_from or '', 'amount': float(amount) / 100.0 if amount else 0,
'status': status,
})
all_trade_nos.append(trade_no)
cur.close(); conn.close()
return orders_by_account, all_trade_nos
def get_refund_for_trade_nos(trade_nos):
if not trade_nos: return {}
conn = psycopg2.connect(host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASSWORD, dbname=PG_DB)
cur = conn.cursor()
refunds = {}
for i in range(0, len(trade_nos), 500):
batch = trade_nos[i:i+500]
ph = ','.join(['%s'] * len(batch))
cur.execute(f"""
SELECT o.trade_no, COALESCE(SUM(r.refund_amount::numeric), 0)/100.0
FROM bi_vala_order o JOIN bi_refund_order r ON o.trade_no = r.trade_no
WHERE r.status = 3 AND o.order_status = 4 AND o.trade_no IN ({ph})
GROUP BY o.trade_no
""", batch)
for row in cur.fetchall(): refunds[row[0]] = float(row[1])
cur.close(); conn.close()
return refunds
def classify_channel(key_from):
kf = str(key_from).strip()
if not kf: return "其他"
if kf.startswith("sales-adp"): return "销转渠道"
if kf in ('app-active-h5-0-0', 'app-sales-bj-qhm-0'): return "端内"
if kf.startswith("miniprogram"): return "端内"
if kf.startswith("newmedia-daren") or "daren" in kf.lower(): return "达人渠道"
if kf.startswith("newmedia-dianpu-xhs"): return "直购渠道"
if kf.startswith("newmedia-dianpu-douyin"): return "直购渠道"
if "jingxuan" in kf and "douyin" in kf.lower(): return "直购渠道"
if kf.startswith("stream-xhs"): return "直购渠道"
if "wxxd" in kf: return "直购渠道"
if kf.startswith("partner"): return "直购渠道"
if kf.startswith("newmedia-dianpu-wwxx"): return "达人渠道"
if kf.startswith("newmedia-"): return "直购渠道"
return "其他"
def get_token():
with open(os.path.join(CRED_DIR, "config.json")) as f: cfg = json.load(f)
resp = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": cfg["apps"][0]["appId"], "app_secret": cfg["apps"][0]["appSecret"]}, timeout=15)
return resp.json()["tenant_access_token"]
def read_sheet(token, sheet_id):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}",
headers={"Authorization": f"Bearer {token}"}, timeout=60)
return resp.json()["data"]["valueRange"]["values"][2:]
def parse_month(date_str):
m = re.match(r'(\d+)月', str(date_str).strip())
return int(m.group(1)) if m else None
def parse_row(row):
def get(idx, default=""):
return str(row[idx]).strip() if idx < len(row) and row[idx] else default
return {
"sales": get(0), "nickname": get(1), "lead_date": get(2),
"trial_lessons": get(3), "phone": get(4), "grade": get(5),
"history": get(6), "user_id": get(7), "reg_date": get(8),
"download_channel": get(9), "is_order": get(10), "order_date": get(11),
"order_channel": get(12), "product": get(13), "gmv": get(14),
"refund": get(15), "gsv": get(16), "activated": get(17),
"progress": get(18), "last_study": get(19), "study_min": get(20),
"update_time": get(21),
}
def safe_int(val):
try: return int(float(val))
except: return 0
def fmt_pct(val):
return f"{val*100:.2f}%"
def fmt_pct1(val):
return f"{val*100:.1f}%"
def get_or_create_sheet(token, title):
resp = requests.get(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/metainfo",
headers={"Authorization": f"Bearer {token}"}, timeout=15)
for s in resp.json().get("data", {}).get("sheets", []):
if s.get("title") == title:
sid = s["sheetId"]
print(f" 删除旧 sheet: {title} ({sid})")
requests.post(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"requests": [{"deleteSheet": {"sheetId": sid}}]}, timeout=30)
time.sleep(0.5)
break
resp = requests.post(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/sheets_batch_update",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"requests": [{"addSheet": {"properties": {"title": title, "index": 10}}}]}, timeout=30)
result = resp.json()
if result.get("code") == 0:
replies = result["data"]["replies"]
if replies and "addSheet" in replies[0]:
return replies[0]["addSheet"]["properties"]["sheetId"]
print(f" 创建sheet失败: {result}")
return None
def write_values(token, sheet_id, range_str, values):
resp = requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}, timeout=30)
code = resp.json().get("code")
if code != 0: print(f"{range_str}: {resp.json()}")
return code == 0
def apply_style(token, sheet_id, col_start, row_start, col_end, row_end, style):
rng = f"{sheet_id}!{col_start}{row_start}:{col_end}{row_end}"
requests.put(
f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/style",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
json={"appendStyle": {"range": rng, "style": style}}, timeout=30)
def header_style():
return {"backcolor": BLUE, "fontColor": WHITE, "bold": True, "fontSize": 10}
def main():
token = get_token()
print("读取销售明细...")
all_leads = []
for sheet_label, sheet_id in SHEET_MAP.items():
rows = read_sheet(token, sheet_id)
for row in rows: all_leads.append(parse_row(row))
print(f" 总计: {len(all_leads)} 条线索")
by_month = defaultdict(list)
for r in all_leads:
m = parse_month(r["lead_date"])
if m in TARGET_MONTHS: by_month[m].append(r)
all_user_ids = set()
for r in all_leads:
uid = safe_int(r["user_id"])
if uid > 0: all_user_ids.add(uid)
print(f"查询数据库订单({len(all_user_ids)} 个用户)...")
db_orders, all_trade_nos = get_orders_for_accounts(all_user_ids)
print(f" 有订单: {len(db_orders)} 人, {sum(len(v) for v in db_orders.values())}")
print(f"查询退款({len(all_trade_nos)} 笔)...")
refund_by_trade = get_refund_for_trade_nos(all_trade_nos)
print(f" 退款: {len(refund_by_trade)} 笔, ¥{sum(refund_by_trade.values()):,.0f}")
for r in all_leads:
uid = safe_int(r["user_id"])
r["_db_orders"] = db_orders.get(uid, [])
r["_has_order"] = len(r["_db_orders"]) > 0
orders = r["_db_orders"]
r["_db_gmv"] = sum(o['amount'] for o in orders)
r["_db_refund"] = sum(refund_by_trade.get(o['trade_no'], 0) for o in orders)
# ═══════════════════════════════════════════
# 1. 修复销转总览 — 全部计算值
# ═══════════════════════════════════════════
print("\n=== 修复销转总览 ===")
sid_ov = get_or_create_sheet(token, "📊 销转总览")
ov_hdr = [["月份", "销售", "线索数", "订单数", "转化率",
"GMV", "退款金额", "GSV", "投放消耗", "达人GMV",
"达人佣金(40%)", "总成本", "退后ROI"]]
write_values(token, sid_ov, "A1:M1", ov_hdr)
apply_style(token, sid_ov, "A", 1, "M", 1, header_style())
ov_row = 2
for m in TARGET_MONTHS:
cpl = COSTS.get(m, 0)
# 合计
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
refund = sum(r["_db_refund"] for r in oa)
gsv = round(gmv - refund, 2)
dg = sum(o2['amount'] for r in oa for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
ad = lc * cpl if cpl > 0 else 0
comm = round(dg * 0.4, 2)
tc = round(ad + comm, 2)
roi = round(gsv / tc, 2) if tc > 0 else 0
conv = fmt_pct1(oc / lc) if lc > 0 else "0.0%"
write_values(token, sid_ov, f"A{ov_row}:M{ov_row}", [[
f"{m}", "合计", lc, oc, conv,
gmv, refund, gsv,
ad if cpl > 0 else "-", dg, comm,
tc if cpl > 0 else "-", roi if cpl > 0 else "-"
]])
ov_row += 1
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
lc2 = len(leads); oc2 = len(ordered)
gmv2 = sum(r["_db_gmv"] for r in ordered)
refund2 = sum(r["_db_refund"] for r in ordered)
gsv2 = round(gmv2 - refund2, 2)
dg2 = sum(o2['amount'] for r in ordered for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
ad2 = lc2 * cpl if cpl > 0 else 0
comm2 = round(dg2 * 0.4, 2)
tc2 = round(ad2 + comm2, 2)
roi2 = round(gsv2 / tc2, 2) if tc2 > 0 else 0
conv2 = fmt_pct1(oc2 / lc2) if lc2 > 0 else "0.0%"
write_values(token, sid_ov, f"A{ov_row}:M{ov_row}", [[
f"{m}", sales, lc2, oc2, conv2,
gmv2, refund2, gsv2,
ad2 if cpl > 0 else "-", dg2, comm2,
tc2 if cpl > 0 else "-", roi2 if cpl > 0 else "-"
]])
ov_row += 1
if (ov_row - 2) % 5 == 0: time.sleep(0.5)
print(f"{ov_row - 2}")
# ═══════════════════════════════════════════
# 2. 新建投放日报
# ═══════════════════════════════════════════
print("\n=== 新建投放日报 ===")
sid_daily = get_or_create_sheet(token, "📊 投放日报")
row = 1
# --- 标题 ---
write_values(token, sid_daily, f"A{row}:A{row}", [["一、整月汇总"]])
apply_style(token, sid_daily, "A", row, "A", row, {"bold": True, "fontSize": 12})
row += 1
# 表头
daily_hdr = [["月份", "投放消耗", "总成本", "企微线索", "线索成本", "GMV", "GSV", "ROI", "转化率"]]
write_values(token, sid_daily, f"A{row}:I{row}", daily_hdr)
apply_style(token, sid_daily, "A", row, "I", row, header_style())
row += 1
for m in [3, 4, 5]:
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
refund = sum(r["_db_refund"] for r in oa)
gsv = round(gmv - refund, 2)
cpl = COSTS.get(m, 0)
ad_spend = lc * cpl
dg = sum(o2['amount'] for r in oa for o2 in r["_db_orders"] if classify_channel(o2['key_from']) == "达人渠道")
comm = round(dg * 0.4, 2)
tc = round(ad_spend + comm, 2)
roi = round(gsv / tc, 2) if tc > 0 else 0
conv = fmt_pct(oc / lc) if lc > 0 else "0.00%"
write_values(token, sid_daily, f"A{row}:I{row}", [[
f"{m}", ad_spend, tc, lc, cpl, gmv, gsv, roi, conv
]])
row += 1
row += 1 # 空行
# --- 代理商日报 ---
write_values(token, sid_daily, f"A{row}:A{row}", [["二、代理商日报(待补充数据源)"]])
apply_style(token, sid_daily, "A", row, "A", row, {"bold": True, "fontSize": 12})
row += 1
agent_hdr = [["日期", "代理商", "消耗", "线索", "成本", "开口率", "留资率"]]
write_values(token, sid_daily, f"A{row}:G{row}", agent_hdr)
apply_style(token, sid_daily, "A", row, "G", row, header_style())
row += 1
# 占位:谦禾/云智/YTL 数据源待确认
write_values(token, sid_daily, f"A{row}:A{row}", [["⚠️ 代理商消耗数据来源待确认(谦禾/云智/YTL"]])
row += 2
# --- 线索·行课·销转 ---
write_values(token, sid_daily, f"A{row}:A{row}", [["三、线索·行课·销转(当月累计)"]])
apply_style(token, sid_daily, "A", row, "A", row, {"bold": True, "fontSize": 12})
row += 1
funnel_hdr = [["月份", "销售", "月线索", "订单", "GMV", "转化率", "拿号率", "注册率", "首课率", "5课率"]]
write_values(token, sid_daily, f"A{row}:J{row}", funnel_hdr)
apply_style(token, sid_daily, "A", row, "J", row, header_style())
row += 1
for m in [3, 4, 5]:
# 合计
pa = by_month[m]; oa = [r for r in pa if r["_has_order"]]
lc = len(pa); oc = len(oa)
gmv = sum(r["_db_gmv"] for r in oa)
hp = len([r for r in pa if r["phone"] and r["phone"] != "未注册"])
reg = len([r for r in pa if r["user_id"] and r["user_id"] != "未注册"])
trial = [safe_int(r["trial_lessons"]) for r in pa]
first = len([t for t in trial if t >= 1])
fifth = len([t for t in trial if t >= 5])
write_values(token, sid_daily, f"A{row}:J{row}", [[
f"{m}", "合计", lc, oc, gmv,
fmt_pct(oc / lc) if lc else "0.00%",
fmt_pct(hp / lc) if lc else "0.00%",
fmt_pct(reg / lc) if lc else "0.00%",
fmt_pct(first / lc) if lc else "0.00%",
fmt_pct(fifth / lc) if lc else "0.00%",
]])
row += 1
for sales in SALES_ORDER:
leads = [r for r in by_month[m] if r["sales"] == sales]
if not leads: continue
ordered = [r for r in leads if r["_has_order"]]
lc2 = len(leads); oc2 = len(ordered)
gmv2 = sum(r["_db_gmv"] for r in ordered)
hp2 = len([r for r in leads if r["phone"] and r["phone"] != "未注册"])
reg2 = len([r for r in leads if r["user_id"] and r["user_id"] != "未注册"])
trial2 = [safe_int(r["trial_lessons"]) for r in leads]
first2 = len([t for t in trial2 if t >= 1])
fifth2 = len([t for t in trial2 if t >= 5])
write_values(token, sid_daily, f"A{row}:J{row}", [[
f"{m}", sales, lc2, oc2, gmv2,
fmt_pct(oc2 / lc2) if lc2 else "0.00%",
fmt_pct(hp2 / lc2) if lc2 else "0.00%",
fmt_pct(reg2 / lc2) if lc2 else "0.00%",
fmt_pct(first2 / lc2) if lc2 else "0.00%",
fmt_pct(fifth2 / lc2) if lc2 else "0.00%",
]])
row += 1
if (row - 1) % 5 == 0: time.sleep(0.5)
print(f" ✅ 日报 {row - 1}")
print("\n✅ 全部完成")
if __name__ == "__main__":
main()