ai_member_xiaoxi/scripts/refresh_order_summary.py
2026-06-14 08:00:01 +08:00

314 lines
11 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.

#!/usr/bin/env python3
"""
订单汇总 AX 全量镜像刷新
触发Step2Cursor Step1 完成后 @小溪)
归属:小溪 (xiaoxi)
进表条件K=是 · O>0 · 非全额退(P空或P<O) · L≥C
全额退 → 整行不进订单表(销售行清 K/O/P/Q
镜像 AV 原样 + W 渠道归属 + X=1
分工约定见 docs/bot-step2-schedule-and-orders.md
"""
import json, time, re, sys, requests, psycopg2
from datetime import datetime
from feishu_sheet_utils import FeishuSheetWriter
# ── 配置 ──
APP_ID = "cli_a929ae22e0b8dcc8"
APP_SECRET = "OtFjMy7p3qE3VvLbMdcWidwgHOnGD4FJ"
SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug"
SALES_SHEETS = {"f975f0": "吴迪", "qJF4I": "小龙", "qJF4J": "成都"}
DIRECT_SHEET = "1sosYE" # 直购表(小红书店铺+stream-xhs不依赖手机号匹配
SUMMARY_SHEET = "2smjwA"
def _get_pg_password():
import os
secrets_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "secrets.env")
with open(secrets_path) as f:
for line in f:
if line.startswith("PG_ONLINE_PASSWORD="):
return line.strip().split("=", 1)[1].strip('"').strip("'")
raise RuntimeError("PG_ONLINE_PASSWORD not found in secrets.env")
PG_CONFIG = {
"host": "bj-postgres-16pob4sg.sql.tencentcdb.com", "port": 28591,
"user": "ai_member", "password": _get_pg_password(), "database": "vala_bi",
}
GOODS_MAP = {
57: "瓦拉英语level1·单季", 60: "瓦拉英语level1", 63: "瓦拉英语level1·单季",
31: "瓦拉英语年包", 32: "瓦拉英语单季度包", 33: "瓦拉英语level2", 54: "瓦拉英语季度包",
61: "瓦拉英语level1+2",
}
# 达人昵称关键词
DAREN_NICKNAMES = ["晚柠", "学霸", "念妈", "神奇瓜妈", "三人行", "老王"]
def get_token():
r = requests.post("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
json={"app_id": APP_ID, "app_secret": APP_SECRET}, timeout=15)
return r.json()["tenant_access_token"]
def read_sheet(token, sheet_id, range_str):
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}!{range_str}?valueRenderOption=ToString"
r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30)
data = r.json()
if data.get("code") != 0:
print(f"Error reading {sheet_id}: {data}")
return []
return data["data"]["valueRange"]["values"]
def put_values(token, sheet_id, range_str, values, retries=3):
url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values"
body = {"valueRange": {"range": f"{sheet_id}!{range_str}", "values": values}}
for attempt in range(retries):
r = requests.put(url, headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}, json=body, timeout=30)
result = r.json()
if result.get("code") == 0:
return True
print(f" Retry {attempt+1} for {range_str}: {result.get('msg','')}")
time.sleep(1)
print(f" FAILED {range_str}")
return False
def parse_date(s):
"""Parse date string to (year, month, day) tuple."""
s = str(s).strip()
if not s:
return None
# YYYY-MM-DD
m = re.match(r'(\d{4})-(\d{1,2})-(\d{1,2})', s)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
# M月D日
m = re.match(r'(\d{1,2})月(\d{1,2})日', s)
if m:
return (2026, int(m.group(1)), int(m.group(2)))
# YYYY/M/D
m = re.match(r'(\d{4})/(\d{1,2})/(\d{1,2})', s)
if m:
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
return None
def date_le(a, b):
"""Return True if date a <= date b."""
if a is None or b is None:
return False
return a <= b
def classify_w_channel(m_channel, sales_name=""):
"""
W 渠道归属,基于 M 成交渠道 + 销售昵称。
达人newmedia-daren/jingxuan/stream-daren 或昵称含达人关键词
销转sales-adp-* / stream-wxxd-* / 含「微信小店」
端内app-active/app-sales/partner-actives 或 M=端内
直购dianpu-* / stream-xhs-* / 抖音/直购/小红书
其他:其余
"""
m = str(m_channel).strip() if m_channel else ""
ml = m.lower()
# 达人
if any(kw in ml for kw in ["newmedia-daren", "jingxuan", "stream-daren"]):
return "达人"
if any(nick in str(sales_name) for nick in DAREN_NICKNAMES):
return "达人"
# 销转
if any(kw in ml for kw in ["sales-adp", "stream-wxxd"]):
return "销转"
if "微信小店" in m:
return "销转"
# 端内
if any(kw in ml for kw in ["app-active", "app-sales", "partner-active"]):
return "端内"
if m == "端内":
return "端内"
# 直购
if any(kw in ml for kw in ["dianpu-", "stream-xhs"]):
return "直购"
if any(kw in m for kw in ["抖音", "直购", "小红书"]):
return "直购"
return "其他"
def phone_match(sheet_phone, db_tel):
"""Match sheet phone number against DB tel (masked like 138****4503)."""
if not sheet_phone or not db_tel:
return False
sheet_phone = str(sheet_phone).strip()
db_tel = str(db_tel).strip()
if sheet_phone == db_tel:
return True
if "****" in db_tel:
parts = db_tel.split("****")
if len(parts) == 2:
prefix, suffix = parts
if sheet_phone.startswith(prefix) and sheet_phone.endswith(suffix):
return True
return False
def main():
print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] 订单汇总全量刷新 启动")
token = get_token()
# ── Step 1: 读取销售三表 ──
all_rows = []
for sid, name in SALES_SHEETS.items():
print(f"Reading {name}...")
vals = read_sheet(token, sid, "A3:V10000")
filtered = []
for i, row in enumerate(vals):
while len(row) < 22:
row.append("")
b = str(row[1]).strip() if row[1] else ""
e = str(row[4]).strip() if row[4] else ""
h = str(row[7]).strip() if row[7] else ""
if b or e or h:
filtered.append({
"sid": sid, "name": name, "row": i + 3,
"raw": row[:22], # A-V
})
print(f" {len(filtered)} non-empty rows")
all_rows.extend(filtered)
print(f"Total rows: {len(all_rows)}")
# ── Step 1.5: 读取直购表 ──
print(f"Reading 直购...")
direct_vals = read_sheet(token, DIRECT_SHEET, "A2:V10000")
direct_rows = []
for i, row in enumerate(direct_vals):
while len(row) < 22:
row.append("")
b = str(row[1]).strip() if row[1] else ""
h = str(row[7]).strip() if row[7] else ""
if b or h:
direct_rows.append({
"sid": DIRECT_SHEET, "name": "直购", "row": i + 2,
"raw": row[:22],
})
print(f" {len(direct_rows)} non-empty rows")
all_rows.extend(direct_rows)
print(f"Total rows (with 直购): {len(all_rows)}")
# ── Step 2: 筛选进订单汇总的行 ──
# 条件K=是 · O>0 · 非全额退(P空或P<O) · L≥C
order_rows = []
for r in all_rows:
raw = r["raw"]
k = str(raw[10]).strip() if len(raw) > 10 and raw[10] else ""
if k != "":
continue
# O > 0
try:
o_val = float(raw[14]) if len(raw) > 14 and raw[14] not in (None, "") else 0
except (ValueError, TypeError):
o_val = 0
if o_val <= 0:
continue
# 非全额退: P空或P<O
try:
p_val = float(raw[15]) if len(raw) > 15 and raw[15] not in (None, "") else 0
except (ValueError, TypeError):
p_val = 0
if p_val > 0 and p_val >= o_val:
# 全额退 → 不进订单表
continue
# L ≥ C (C为空时跳过此检查如直购用户无进线日期)
c_str = str(raw[2]).strip() if len(raw) > 2 and raw[2] else ""
l_str = str(raw[11]).strip() if len(raw) > 11 and raw[11] else ""
c_date = parse_date(c_str)
l_date = parse_date(l_str)
if c_date is not None and not date_le(c_date, l_date):
continue
# 通过所有条件
order_rows.append(r)
print(f"Order rows after filter: {len(order_rows)}")
# ── Step 2.5: 去重(同一人可能在三表+直购中出现多次)──
# 按 (A销售归属, B微信昵称, O下单金额, P退款金额, L下单日期) 去重
seen = set()
deduped = []
for r in order_rows:
raw = r["raw"]
a = str(raw[0]).strip() if raw[0] else ""
b = str(raw[1]).strip() if len(raw) > 1 and raw[1] else ""
o = str(raw[14]).strip() if len(raw) > 14 and raw[14] else ""
p = str(raw[15]).strip() if len(raw) > 15 and raw[15] else ""
l = str(raw[11]).strip() if len(raw) > 11 and raw[11] else ""
key = (a, b, o, p, l)
if key not in seen:
seen.add(key)
deduped.append(r)
dup_count = len(order_rows) - len(deduped)
if dup_count > 0:
print(f" Removed {dup_count} duplicate rows")
order_rows = deduped
# ── Step 3: 按 L 下单日降序 ──
order_rows.sort(key=lambda r: str(r["raw"][11]) if len(r["raw"]) > 11 and r["raw"][11] else "", reverse=True)
# ── Step 4: 构建 AX 行 ──
summary_rows = []
for r in order_rows:
raw = r["raw"]
# AV 原样镜像
new_row = list(raw[:22])
# W: 渠道归属(基于 M 成交渠道)
m_channel = str(raw[12]).strip() if len(raw) > 12 and raw[12] else ""
sales_name = str(raw[0]).strip() if len(raw) > 0 and raw[0] else ""
w = classify_w_channel(m_channel, sales_name)
new_row.append(w)
# X: 有效成单 = 1
new_row.append(1)
summary_rows.append(new_row)
print(f"Summary rows: {len(summary_rows)}")
# ── Step 5: 写入订单汇总(使用安全写入工具,自动遵守 5000 格上限)──
print("Writing to 订单汇总...")
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
# 先清空旧数据区26 列,自动计算批大小 ≤ 4400 格/批)
writer.clear(SUMMARY_SHEET, start_row=3, end_row=2000, cols=26)
time.sleep(0.5)
# 写入新数据24 列 A-X自动分批
total = len(summary_rows)
writer.write(SUMMARY_SHEET, start_row=3, rows=summary_rows, cols=24)
# ── Step 6: 清除多余旧行 ──
existing = read_sheet(token, SUMMARY_SHEET, "A3:A4000")
old_count = len([r for r in existing if r and any(c for c in r if c)])
if old_count > total:
writer.clear(SUMMARY_SHEET, start_row=3 + total, end_row=3 + old_count - 1, cols=24)
print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] ✅ 订单汇总刷新完成")
if __name__ == "__main__":
main()