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

284 lines
9.9 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": "成都"}
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
# W列渠道归属分类规则 [王虹茗确认 2026-06-15]
def classify_w_channel(m_channel, sales_name=""):
"""
W 渠道归属,基于 M 成交渠道 + 销售昵称。
端内: 精确匹配 3 个渠道
销转: sales-adp-*
达人: newmedia-daren-* / newmedia-dianpu-wwxx-0-0 / 昵称含达人关键词
直购: 其余全部
"""
m = str(m_channel).strip() if m_channel else ""
# 达人(昵称关键词优先,保持原有逻辑)
if any(nick in str(sales_name) for nick in DAREN_NICKNAMES):
return "达人"
if not m:
return "直购"
if m in ("app-active-h5-0-0", "app-sales-bj-qhm-0", "app-sales-bj-wd-0"):
return "端内"
if m.startswith("sales-adp-"):
return "销转"
if m.startswith("newmedia-daren-") or m == "newmedia-dianpu-wwxx-0-0":
return "达人"
# 其余: dianpu(不含wwxx) + partner/stream/miniprogram/jingxuan/空/shuadan等 → 直购
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 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()