#!/usr/bin/env python3 """ 财务口径订单刷新 v3 — 从订单汇总 2smjwA 出发 逻辑: 1. 读订单汇总 2smjwA (A3:W) 2. 逐行复制 A–U 镜像段,按 E 列手机号查 DB → 该用户全部订单 3. N 单 = N 行:每行 X=trade_no,K–P 写该单金额 4. Y=该单有效→1 (GSV>0 · 非全额退 · 进线≤下单) 5. Z=渠道归属,W=留空(Cursor公式) 6. 先 clear A3:Z5000 全删,再全量写入 """ import json, re, time, sys, os, requests, psycopg2 from datetime import datetime from collections import defaultdict SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) WORKSPACE = os.path.dirname(SCRIPTS_DIR) CRED_DIR = "/root/.openclaw/credentials/xiaoxi" sys.path.insert(0, SCRIPTS_DIR) from phone_encrypt import encrypt_phone from feishu_sheet_utils import FeishuSheetWriter SPREADSHEET_TOKEN = "NoZqsFi47hIOHEt9j8WcfRtbnug" SUMMARY_SHEET_ID = "2smjwA" FINANCE_SHEET_ID = "2hSLSg" GOODS_NAMES = { 57: "瓦拉英语level1·单季", 60: "瓦拉英语level1", 63: "瓦拉英语level1·单季", 31: "瓦拉英语年包", 32: "瓦拉英语单季度包", 33: "瓦拉英语level2", 54: "瓦拉英语季度包", 61: "瓦拉英语level1+2", } def classify_channel(key_from): if not key_from: return "直购" kf = key_from.strip() if kf in ("app-active-h5-0-0", "app-sales-bj-qhm-0", "app-sales-bj-wd-0"): return "端内" if kf.startswith("sales-adp-"): return "销转" if kf.startswith("newmedia-daren-") or kf == "newmedia-dianpu-wwxx-0-0": return "达人" return "直购" LOG_FILE = "/var/log/xiaoxi_finance_orders.log" def log(msg): ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") line = f"[{ts}] {msg}" print(line) with open(LOG_FILE, "a") as f: f.write(line + "\n") def get_secret(key): with open(os.path.join(WORKSPACE, "secrets.env")) as f: for line in f: if line.startswith(f"{key}="): return line.strip().split("=", 1)[1].strip("'\"") def get_fs_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, range_str=None): url = f"https://open.feishu.cn/open-apis/sheets/v2/spreadsheets/{SPREADSHEET_TOKEN}/values/{sheet_id}" if range_str: url += f"!{range_str}" resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=30) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"读取失败 {sheet_id}: {data}") return data["data"]["valueRange"]["values"] def safe_cell(row, idx): if len(row) > idx and row[idx] is not None: try: if isinstance(row[idx], (int, float)): if row[idx] == int(row[idx]): return str(int(row[idx])) return str(row[idx]).strip() except (ValueError, TypeError): return str(row[idx]).strip() return "" def batch_in(cur, sql_tpl, params, chunk=500): results = [] for i in range(0, len(params), chunk): batch = params[i:i + chunk] ph = ",".join(["%s"] * len(batch)) cur.execute(sql_tpl % ph, batch) results.extend(cur.fetchall()) return results def parse_date_str(s): """'6月7日'/'6月7日 10:23:48' → '2026-06-07'/'2026-06-07 10:23:48'""" if not s: return "" s = s.strip() if re.match(r'^\d{4}-\d{2}-\d{2}', s): return s m = re.match(r'^(\d{1,2})月(\d{1,2})日(?:\s+(\d{1,2}:\d{2}:\d{2}))?', s) if m: year = datetime.now().year date_part = f"{year}-{int(m.group(1)):02d}-{int(m.group(2)):02d}" if m.group(3): return f"{date_part} {m.group(3)}" return date_part return s # ═══ Step 1: 读订单汇总 2smjwA ═══ def parse_summary(token): """返回 [(a_to_u, phone, uid, clue_date_parsed), ...]""" rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:W2000") entries = [] for row in rows: if not row or all(not cell for cell in row): continue phone = safe_cell(row, 4) # E 列 uid = safe_cell(row, 7) # H 列 if not phone and not uid: continue # A–U 镜像段 (21 列, indices 0-20) a_to_u = [safe_cell(row, i) for i in range(21)] # C 列进线日期 (index 2) clue_date = parse_date_str(safe_cell(row, 2)) entries.append((a_to_u, phone, uid, clue_date)) log(f" 订单汇总: {len(entries)} 行") return entries # ═══ Step 2: XXTEA 匹配 + 收集 UID ═══ def resolve_uids(entries): phone_set = set() for _, phone, uid, _ in entries: if re.match(r'^\d{11}$', phone): phone_set.add(phone) phone_to_uid = {} if phone_set: log(f" XXTEA 加密: {len(phone_set)} 个手机号") phone_enc_map = {} for phone in phone_set: try: phone_enc_map[encrypt_phone(phone)] = phone except Exception as ex: log(f" 加密失败 {phone}: {ex}") conn = psycopg2.connect( host="bj-postgres-16pob4sg.sql.tencentcdb.com", port=28591, user="ai_member", password=get_secret("PG_ONLINE_PASSWORD"), dbname="vala_bi", connect_timeout=30 ) cur = conn.cursor() enc_list = list(phone_enc_map.keys()) for i in range(0, len(enc_list), 500): chunk = enc_list[i:i + 500] ph = ",".join(["%s"] * len(chunk)) cur.execute( f"SELECT id, tel_encrypt FROM bi_vala_app_account " f"WHERE tel_encrypt IN ({ph}) AND status=1 AND deleted_at IS NULL", chunk ) for uid, tel_enc in cur.fetchall(): plain = phone_enc_map.get(tel_enc) if plain: phone_to_uid[plain] = str(uid) cur.close() conn.close() log(f" 匹配到 {len(phone_to_uid)} 个 UID") uid_set = set() for _, phone, uid, _ in entries: if re.match(r'^\d{11}$', phone) and phone in phone_to_uid: uid_set.add(int(phone_to_uid[phone])) elif uid and uid.isdigit() and int(uid) > 0: uid_set.add(int(uid)) log(f" 有效 UID: {len(uid_set)}") return phone_to_uid, uid_set # ═══ Step 3: 查全量订单 ═══ def query_all_orders(uid_set): uid_list = list(uid_set) if not uid_list: return {} conn = psycopg2.connect( host="bj-postgres-16pob4sg.sql.tencentcdb.com", port=28591, user="ai_member", password=get_secret("PG_ONLINE_PASSWORD"), dbname="vala_bi", connect_timeout=30 ) cur = conn.cursor() log(" 查询全量订单...") orders = batch_in(cur, "SELECT account_id, trade_no, pay_success_date, key_from, goods_id, " "pay_amount_int, order_status " "FROM bi_vala_order WHERE account_id IN (%s) AND pay_success_date IS NOT NULL " "ORDER BY pay_success_date DESC", uid_list ) trade_nos = [o[1] for o in orders if o[1]] refund_map = {} if trade_nos: refunds = batch_in(cur, "SELECT trade_no, refund_amount_int FROM bi_refund_order " "WHERE trade_no IN (%s) AND status=3", trade_nos ) for tn, amt in refunds: refund_map[tn] = amt cur.close() conn.close() uid_orders = defaultdict(list) for o in orders: aid = o[0] tn = o[1] gmv = o[5] / 100.0 refund = refund_map.get(tn, 0) / 100.0 gsv = gmv - refund dt = o[2] order_date = f"{dt.month}月{dt.day}日 {dt.strftime('%H:%M:%S')}" if dt else "" order_date_raw = dt.strftime("%Y-%m-%d %H:%M:%S") if dt else "" uid_orders[aid].append({ "trade_no": tn or "", "order_date": order_date, "order_date_raw": order_date_raw, "key_from": o[3] or "", "product": GOODS_NAMES.get(o[4], f"商品{o[4]}"), "gmv": int(gmv), "refund": int(refund), "gsv": int(gsv), "order_status": o[6], }) log(f" 全量订单: {sum(len(v) for v in uid_orders.values())} 条") return uid_orders # ═══ Step 4: 展开写入财务 tab ═══ def write_finance_sheet(token, entries, phone_to_uid, uid_orders): now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rows = [] for a_to_u, phone, uid_str, clue_date in entries: # 确定 UID aid = 0 if re.match(r'^\d{11}$', phone) and phone in phone_to_uid: aid = int(phone_to_uid[phone]) elif uid_str and uid_str.isdigit() and int(uid_str) > 0: aid = int(uid_str) orders = uid_orders.get(aid, []) if not orders: # 无订单:写一行,A–U 镜像,其余留空 row = a_to_u[:21] # A–U row += ["", "", "", "", "", ""] # V W X Y Z rows.append(row) else: for o in orders: # A–U 镜像段 (21列) row = a_to_u[:21] # 确保有21列 while len(row) < 21: row.append("") # K(10): 下单日期 row[10] = o["order_date"] # L(11): 成交渠道 row[11] = o["key_from"] # M(12): 产品 row[12] = o["product"] # N(13): GMV row[13] = o["gmv"] if o["gmv"] > 0 else "" # O(14): 退款 row[14] = o["refund"] if o["refund"] > 0 else "" # P(15): GSV row[15] = o["gsv"] if o["gsv"] > 0 else "" # U(20): 更新时间 row[20] = now_str # V(21): 渠道归属 row.append(classify_channel(o["key_from"])) # W(22): 留空 (Cursor公式) row.append("") # X(23): 订单号 row.append(o["trade_no"]) # Y(24): 该单有效→1 · 全额退款→0 · GSV≤0→0 gmv_val = o["gmv"] refund_val = o["refund"] gsv_val = o["gsv"] is_full_refund = (gmv_val > 0 and gmv_val == refund_val) order_valid = (gsv_val > 0 and not is_full_refund) # 进线早于下单检查 if order_valid and clue_date and o["order_date_raw"]: if o["order_date_raw"] < clue_date: order_valid = False row.append(1 if order_valid else 0) # Z(25): 留空 (Cursor补) row.append("") rows.append(row) log(f" 展开后共 {len(rows)} 行") # 确保每行 26 列 for row in rows: while len(row) < 26: row.append("") # 先 clear A3:Z5000 全删 log(" 清空 A3:Z5000...") writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token) writer.clear(FINANCE_SHEET_ID, start_row=3, end_row=5000, cols=26) # 写入 writer.write(FINANCE_SHEET_ID, start_row=3, rows=rows, cols=26) log(f" 财务订单写入完成") # ═══ Main ═══ def main(): log("=" * 60) log("财务口径订单刷新 v3 启动") try: token = get_fs_token() log("Step 1: 读订单汇总 2smjwA") entries = parse_summary(token) log("Step 2: XXTEA 匹配 UID") phone_to_uid, uid_set = resolve_uids(entries) log("Step 3: 查询全量订单") uid_orders = query_all_orders(uid_set) log("Step 4: 展开写入财务 tab") write_finance_sheet(token, entries, phone_to_uid, uid_orders) log("✅ 财务订单刷新完成") return 0 except Exception as e: log(f"❌ ERROR: {e}") import traceback traceback.print_exc() return 1 if __name__ == "__main__": sys.exit(main())