🤖 每日自动备份 - 2026-06-18 08:00:01
This commit is contained in:
parent
bf639859ac
commit
ada43679aa
1
USER.md
1
USER.md
@ -45,6 +45,7 @@
|
||||
| 刘彦江 | `1da2afbf` |
|
||||
| 姜小龙 | `bc227c85` |
|
||||
| 赵一凡 | `a3168f9d` |
|
||||
| 李丹 | `ea523c46` |
|
||||
|
||||
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md(长期记忆)**,仅可记录在短期日记忆中用于会话连续性。
|
||||
|
||||
|
||||
4
memory/2026-06-17.md
Normal file
4
memory/2026-06-17.md
Normal file
@ -0,0 +1,4 @@
|
||||
# 2026-06-17 工作日志
|
||||
|
||||
## 权限变更
|
||||
- [李承龙确认] 将李丹(`ea523c46`)添加至 USER.md 一级完整权限用户列表
|
||||
15
memory/2026-06-18.md
Normal file
15
memory/2026-06-18.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 2026-06-18 工作日志
|
||||
|
||||
## 细水入海变更同步
|
||||
- [陈逸鸫 00:56] 同步「细水入海」近期变更:
|
||||
- full_refresh 改回 @大麦,小溪不再跑 Bot 三表→订单汇总 merge
|
||||
- 新增「订单-财务口径」tab (2hSLSg),仅陈逸鸫点名时触发
|
||||
- 财务 tab 从 2smjwA 出发,不读三表全量线索
|
||||
- Y=0 全额退/无效单(之前是留空,改为写 0)
|
||||
- 不碰 r1 表头 + r2 合计行(Cursor 公式)
|
||||
- W 列留空由 Cursor 写公式
|
||||
- 新增「销售结算汇总」r8ZHO,由 Cursor 从财务 tab 生成,小溪无需操作
|
||||
|
||||
## 脚本更新
|
||||
- `scripts/finance_orders_refresh.py` v3: Y 列全额退→0(之前留空)
|
||||
- `scripts/sales_leads_full_refresh.py`: 更新 pick_valid_order 逻辑 + 汇总格式 A-W 23列
|
||||
377
scripts/finance_orders_refresh.py
Normal file
377
scripts/finance_orders_refresh.py
Normal file
@ -0,0 +1,377 @@
|
||||
#!/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())
|
||||
@ -376,7 +376,24 @@ def query_all_pg(all_entries, phone_map):
|
||||
if aid not in info:
|
||||
continue
|
||||
info[aid]["has_order"] = True
|
||||
latest = olist[0]
|
||||
|
||||
# pick_valid_order: 选最新非全额退订单(一手机多单只绑当前有效单)
|
||||
valid_orders = []
|
||||
for o in olist:
|
||||
tn = o[1]
|
||||
o_gmv = o[5] / 100.0
|
||||
o_refund = refund_map.get(tn, 0) / 100.0
|
||||
o_gsv = o_gmv - o_refund
|
||||
if o_gsv > 0:
|
||||
valid_orders.append((o, o_gmv, o_refund, o_gsv))
|
||||
|
||||
if valid_orders:
|
||||
latest, _, _, _ = valid_orders[0]
|
||||
info[aid]["all_refunded"] = False
|
||||
else:
|
||||
latest = olist[0]
|
||||
info[aid]["all_refunded"] = True
|
||||
|
||||
# K列格式: M月D日 HH:MM:SS, 同时保留 raw 用于日期比较
|
||||
if latest[2]:
|
||||
dt = latest[2]
|
||||
@ -583,7 +600,7 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||
gsv_int = int(di["gsv"])
|
||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||
|
||||
# 有效订单判定: 有订单 且 非全额退清 且 K(下单日) >= C(线索日期)
|
||||
# 有效订单判定: GSV>0 · 非全额退 · K≥C · 进线早于下单
|
||||
order_date = di["order_date"]
|
||||
should_y_yes = di["has_order"] and not is_full_refund
|
||||
|
||||
@ -593,18 +610,23 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||
if order_date_raw < clue_date:
|
||||
should_y_yes = False
|
||||
|
||||
# 已全退单不写 X/L(一手机多单:线索行只绑当前有效单)
|
||||
if is_full_refund:
|
||||
n_vals.append([""])
|
||||
o_vals.append([""])
|
||||
p_vals.append([""])
|
||||
k_vals.append([""])
|
||||
l_vals.append([""])
|
||||
x_vals.append([""])
|
||||
else:
|
||||
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||
o_vals.append([refund_int if refund_int > 0 else ""])
|
||||
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||
k_vals.append([order_date])
|
||||
l_vals.append([di["order_channel"]])
|
||||
x_vals.append([di.get("trade_no", "")])
|
||||
|
||||
# K: 下单日期, L: 成交渠道, M: 产品
|
||||
k_vals.append([order_date])
|
||||
l_vals.append([di["order_channel"]])
|
||||
# M: 产品
|
||||
m_vals.append([di["product"] if di["has_order"] else ""])
|
||||
|
||||
# Q: 激活课程
|
||||
@ -620,8 +642,6 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||
lm = di["lesson_minutes"]
|
||||
t_vals.append([lm if lm > 0 else ""])
|
||||
|
||||
# X: 订单号
|
||||
x_vals.append([di.get("trade_no", "")])
|
||||
# Y: 有效订单 — 每次全量重新判断
|
||||
y_vals.append([1 if should_y_yes else ""])
|
||||
# Z: 渠道归属(销转/直购/端内/达人)
|
||||
@ -652,7 +672,7 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
||||
# ═══ Step 5: 汇总到「订单汇总」sheet ═══
|
||||
|
||||
def clear_summary_sheet(token):
|
||||
"""先清空订单汇总 sheet 的旧数据(A~X列,从第3行开始),再写入新数据。"""
|
||||
"""先清空订单汇总 sheet 的旧数据(A~W列,从第3行开始),再写入新数据。"""
|
||||
log(" 检查订单汇总 sheet 现有数据...")
|
||||
try:
|
||||
rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:A5000")
|
||||
@ -665,9 +685,9 @@ def clear_summary_sheet(token):
|
||||
log(" 订单汇总 sheet 无旧数据,跳过清空")
|
||||
return
|
||||
|
||||
log(f" 清空 A3:X{last_data_row}({last_data_row - 2} 行旧数据)...")
|
||||
log(f" 清空 A3:W{last_data_row}({last_data_row - 2} 行旧数据)...")
|
||||
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
||||
writer.clear(SUMMARY_SHEET_ID, start_row=3, end_row=last_data_row, cols=24)
|
||||
writer.clear(SUMMARY_SHEET_ID, start_row=3, end_row=last_data_row, cols=23)
|
||||
log(" 清空完成")
|
||||
except Exception as e:
|
||||
log(f" 清空异常: {e}")
|
||||
@ -676,17 +696,20 @@ def clear_summary_sheet(token):
|
||||
def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||
"""
|
||||
将三个销售 sheet 中 Y=1(有效订单)的行汇总到「订单汇总」sheet。
|
||||
先清空旧数据,再全量写入。
|
||||
订单汇总 sheet 的列结构(A~X, 24列):
|
||||
A~U: 镜像三表, V: 渠道归属(Z), W: 留空, X: 订单号
|
||||
先清空旧数据,再全量覆盖。
|
||||
订单汇总 sheet 的列结构(A~W, 23列):
|
||||
A~U: 镜像三表 gate 行, V: 渠道归属, W: trade_no(=三表X, 同一run)
|
||||
同 X 多进线 → 汇总 1行 / unique X
|
||||
"""
|
||||
# 先清空旧数据
|
||||
clear_summary_sheet(token)
|
||||
|
||||
log(" 汇总订单数据...")
|
||||
|
||||
# 收集所有有效订单的行
|
||||
summary_rows = []
|
||||
# 收集所有有效订单的行,按 trade_no(X) 聚合
|
||||
# key: trade_no -> {sales, nickname, ...}
|
||||
trade_rows = {} # trade_no -> row_data
|
||||
|
||||
for sid, sname, _ in SALES_SHEETS:
|
||||
entries = all_entries[sid]
|
||||
for e in entries:
|
||||
@ -721,8 +744,14 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||
continue
|
||||
|
||||
trade_no = di.get("trade_no", "")
|
||||
if not trade_no:
|
||||
continue
|
||||
|
||||
# 构建汇总行 (A~X, 24列): A-U镜像 + V=渠道归属 + W=空 + X=订单号
|
||||
# 同 X 多进线 → 汇总 1行 / unique X(保留第一次出现的进线)
|
||||
if trade_no in trade_rows:
|
||||
continue
|
||||
|
||||
# 构建汇总行 (A~W, 23列): A-U镜像gate + V=渠道归属 + W=trade_no
|
||||
row_data = [
|
||||
e["sales"], # A: 销售归属
|
||||
e["nickname"], # B: 微信昵称
|
||||
@ -734,7 +763,7 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||
str(aid) if aid > 0 else "", # H: 用户ID
|
||||
di.get("reg_date", ""), # I: 注册日期
|
||||
di.get("download_channel", ""), # J: 下载渠道
|
||||
di.get("order_date", ""), # K: 下单日期
|
||||
di.get("order_date", ""), # K: 下单日期
|
||||
di.get("order_channel", ""), # L: 成交渠道
|
||||
di.get("product", ""), # M: 产品
|
||||
gmv_int if gmv_int > 0 else "", # N: GMV
|
||||
@ -746,41 +775,31 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||
di.get("lesson_minutes", 0) or "", # T: 学习时长
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # U: 更新时间
|
||||
classify_channel(di.get("order_channel", "")), # V: 渠道归属
|
||||
"", # W: 留空
|
||||
trade_no, # X: 订单号
|
||||
trade_no, # W: trade_no(=三表X, 同一run)
|
||||
]
|
||||
summary_rows.append((trade_no, row_data))
|
||||
trade_rows[trade_no] = row_data
|
||||
|
||||
# 同订单号去重(保留第一次出现)
|
||||
seen_trade = set()
|
||||
deduped = []
|
||||
for trade_no, row_data in summary_rows:
|
||||
if trade_no and trade_no in seen_trade:
|
||||
continue
|
||||
if trade_no:
|
||||
seen_trade.add(trade_no)
|
||||
deduped.append(row_data)
|
||||
|
||||
log(f" 共 {len(summary_rows)} 条有效订单, 去重后 {len(deduped)} 条, 唯一订单号 {len(seen_trade)}")
|
||||
deduped = list(trade_rows.values())
|
||||
log(f" 共 {len(deduped)} 条有效订单(按 unique X 去重)")
|
||||
|
||||
if not deduped:
|
||||
log(" 无有效订单,跳过汇总")
|
||||
return
|
||||
|
||||
# 写入订单汇总 sheet(从第3行开始,A~X 共24列)
|
||||
# 写入订单汇总 sheet(从第3行开始,A~W 共23列)
|
||||
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
||||
|
||||
# 构建 A~X 的值数组(24列),确保每行长度一致
|
||||
# 构建 A~W 的值数组(23列),确保每行长度一致
|
||||
values = []
|
||||
for row_data in deduped:
|
||||
padded = row_data[:24]
|
||||
while len(padded) < 24:
|
||||
padded = row_data[:23]
|
||||
while len(padded) < 23:
|
||||
padded.append("")
|
||||
values.append(padded)
|
||||
|
||||
writer.write(SUMMARY_SHEET_ID, start_row=3, rows=values, cols=24)
|
||||
writer.write(SUMMARY_SHEET_ID, start_row=3, rows=values, cols=23)
|
||||
|
||||
log(f" 订单汇总写入完成, 共 {len(summary_rows)} 行")
|
||||
log(f" 订单汇总写入完成, 共 {len(deduped)} 行")
|
||||
|
||||
|
||||
# ═══ Main ═══
|
||||
|
||||
Loading…
Reference in New Issue
Block a user