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

253 lines
8.7 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: 读取销售三表(含 X/Y 列)──
all_rows = []
for sid, name in SALES_SHEETS.items():
print(f"Reading {name}...")
vals = read_sheet(token, sid, "A3:Y10000")
filtered = []
for i, row in enumerate(vals):
while len(row) < 25:
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[:25], # A-Y
})
print(f" {len(filtered)} non-empty rows")
all_rows.extend(filtered)
print(f"Total rows: {len(all_rows)}")
# ── Step 2: 筛选进订单汇总的行 ──
# 条件X=1 (gate)
order_rows = []
for r in all_rows:
raw = r["raw"]
# X 列 (index 23) = 1
try:
x_val = int(float(raw[23])) if len(raw) > 23 and raw[23] not in (None, "") else 0
except (ValueError, TypeError):
x_val = 0
if x_val != 1:
continue
order_rows.append(r)
print(f"Order rows after filter: {len(order_rows)}")
# ── Step 2.5: 去重(同 UID 多表出现,保留行号最小)──
uid_best = {}
for r in order_rows:
raw = r["raw"]
uid = str(raw[7]).strip() if len(raw) > 7 and raw[7] not in (None, "") else ""
try:
uid = str(int(float(raw[7])))
except:
pass
if not uid:
continue
if uid not in uid_best or r["row"] < uid_best[uid][0]:
uid_best[uid] = (r["row"], r)
order_rows = [v[1] for v in uid_best.values()]
print(f" After UID dedup: {len(order_rows)} rows")
# ── Step 3: 按 K 下单日降序 ──
order_rows.sort(key=lambda r: str(r["raw"][10]) if len(r["raw"]) > 10 and r["raw"][10] else "", reverse=True)
# ── Step 4: 构建 AW 行A-V 镜像 + W=渠道=三表Y + 无X列──
summary_rows = []
for r in order_rows:
raw = r["raw"]
# AV 原样镜像 (indices 0-21)
new_row = list(raw[:22])
# W: 渠道归属 = 三表 Y 列 (index 24)
y_val = str(raw[24]).strip() if len(raw) > 24 and raw[24] not in (None, "") else ""
new_row.append(y_val)
summary_rows.append(new_row)
print(f"Summary rows: {len(summary_rows)}")
# ── Step 5: 写入订单汇总(使用安全写入工具,自动遵守 5000 格上限)──
print("Writing to 订单汇总...")
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
# 先清空旧数据区23 列 A-W自动计算批大小 ≤ 4400 格/批)
writer.clear(SUMMARY_SHEET, start_row=3, end_row=2000, cols=23)
time.sleep(0.5)
# 写入新数据23 列 A-W自动分批
total = len(summary_rows)
writer.write(SUMMARY_SHEET, start_row=3, rows=summary_rows, cols=23)
# ── 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=23)
print(f"[{datetime.now():%Y-%m-%d %H:%M:%S}] ✅ 订单汇总刷新完成")
if __name__ == "__main__":
main()