🤖 每日自动备份 - 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` |
|
| 刘彦江 | `1da2afbf` |
|
||||||
| 姜小龙 | `bc227c85` |
|
| 姜小龙 | `bc227c85` |
|
||||||
| 赵一凡 | `a3168f9d` |
|
| 赵一凡 | `a3168f9d` |
|
||||||
|
| 李丹 | `ea523c46` |
|
||||||
|
|
||||||
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md(长期记忆)**,仅可记录在短期日记忆中用于会话连续性。
|
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 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:
|
if aid not in info:
|
||||||
continue
|
continue
|
||||||
info[aid]["has_order"] = True
|
info[aid]["has_order"] = True
|
||||||
|
|
||||||
|
# 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]
|
latest = olist[0]
|
||||||
|
info[aid]["all_refunded"] = True
|
||||||
|
|
||||||
# K列格式: M月D日 HH:MM:SS, 同时保留 raw 用于日期比较
|
# K列格式: M月D日 HH:MM:SS, 同时保留 raw 用于日期比较
|
||||||
if latest[2]:
|
if latest[2]:
|
||||||
dt = 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"])
|
gsv_int = int(di["gsv"])
|
||||||
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
is_full_refund = (gmv_int > 0 and gmv_int == refund_int)
|
||||||
|
|
||||||
# 有效订单判定: 有订单 且 非全额退清 且 K(下单日) >= C(线索日期)
|
# 有效订单判定: GSV>0 · 非全额退 · K≥C · 进线早于下单
|
||||||
order_date = di["order_date"]
|
order_date = di["order_date"]
|
||||||
should_y_yes = di["has_order"] and not is_full_refund
|
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:
|
if order_date_raw < clue_date:
|
||||||
should_y_yes = False
|
should_y_yes = False
|
||||||
|
|
||||||
|
# 已全退单不写 X/L(一手机多单:线索行只绑当前有效单)
|
||||||
if is_full_refund:
|
if is_full_refund:
|
||||||
n_vals.append([""])
|
n_vals.append([""])
|
||||||
o_vals.append([""])
|
o_vals.append([""])
|
||||||
p_vals.append([""])
|
p_vals.append([""])
|
||||||
|
k_vals.append([""])
|
||||||
|
l_vals.append([""])
|
||||||
|
x_vals.append([""])
|
||||||
else:
|
else:
|
||||||
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
n_vals.append([gmv_int if gmv_int > 0 else ""])
|
||||||
o_vals.append([refund_int if refund_int > 0 else ""])
|
o_vals.append([refund_int if refund_int > 0 else ""])
|
||||||
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
p_vals.append([gsv_int if gsv_int > 0 else ""])
|
||||||
|
|
||||||
# K: 下单日期, L: 成交渠道, M: 产品
|
|
||||||
k_vals.append([order_date])
|
k_vals.append([order_date])
|
||||||
l_vals.append([di["order_channel"]])
|
l_vals.append([di["order_channel"]])
|
||||||
|
x_vals.append([di.get("trade_no", "")])
|
||||||
|
|
||||||
|
# M: 产品
|
||||||
m_vals.append([di["product"] if di["has_order"] else ""])
|
m_vals.append([di["product"] if di["has_order"] else ""])
|
||||||
|
|
||||||
# Q: 激活课程
|
# Q: 激活课程
|
||||||
@ -620,8 +642,6 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
lm = di["lesson_minutes"]
|
lm = di["lesson_minutes"]
|
||||||
t_vals.append([lm if lm > 0 else ""])
|
t_vals.append([lm if lm > 0 else ""])
|
||||||
|
|
||||||
# X: 订单号
|
|
||||||
x_vals.append([di.get("trade_no", "")])
|
|
||||||
# Y: 有效订单 — 每次全量重新判断
|
# Y: 有效订单 — 每次全量重新判断
|
||||||
y_vals.append([1 if should_y_yes else ""])
|
y_vals.append([1 if should_y_yes else ""])
|
||||||
# Z: 渠道归属(销转/直购/端内/达人)
|
# Z: 渠道归属(销转/直购/端内/达人)
|
||||||
@ -652,7 +672,7 @@ def write_sales_sheets(token, all_entries, phone_map, db_info):
|
|||||||
# ═══ Step 5: 汇总到「订单汇总」sheet ═══
|
# ═══ Step 5: 汇总到「订单汇总」sheet ═══
|
||||||
|
|
||||||
def clear_summary_sheet(token):
|
def clear_summary_sheet(token):
|
||||||
"""先清空订单汇总 sheet 的旧数据(A~X列,从第3行开始),再写入新数据。"""
|
"""先清空订单汇总 sheet 的旧数据(A~W列,从第3行开始),再写入新数据。"""
|
||||||
log(" 检查订单汇总 sheet 现有数据...")
|
log(" 检查订单汇总 sheet 现有数据...")
|
||||||
try:
|
try:
|
||||||
rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:A5000")
|
rows = read_sheet(token, SUMMARY_SHEET_ID, "A3:A5000")
|
||||||
@ -665,9 +685,9 @@ def clear_summary_sheet(token):
|
|||||||
log(" 订单汇总 sheet 无旧数据,跳过清空")
|
log(" 订单汇总 sheet 无旧数据,跳过清空")
|
||||||
return
|
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 = 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(" 清空完成")
|
log(" 清空完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f" 清空异常: {e}")
|
log(f" 清空异常: {e}")
|
||||||
@ -676,17 +696,20 @@ def clear_summary_sheet(token):
|
|||||||
def write_summary_sheet(token, all_entries, phone_map, db_info):
|
def write_summary_sheet(token, all_entries, phone_map, db_info):
|
||||||
"""
|
"""
|
||||||
将三个销售 sheet 中 Y=1(有效订单)的行汇总到「订单汇总」sheet。
|
将三个销售 sheet 中 Y=1(有效订单)的行汇总到「订单汇总」sheet。
|
||||||
先清空旧数据,再全量写入。
|
先清空旧数据,再全量覆盖。
|
||||||
订单汇总 sheet 的列结构(A~X, 24列):
|
订单汇总 sheet 的列结构(A~W, 23列):
|
||||||
A~U: 镜像三表, V: 渠道归属(Z), W: 留空, X: 订单号
|
A~U: 镜像三表 gate 行, V: 渠道归属, W: trade_no(=三表X, 同一run)
|
||||||
|
同 X 多进线 → 汇总 1行 / unique X
|
||||||
"""
|
"""
|
||||||
# 先清空旧数据
|
# 先清空旧数据
|
||||||
clear_summary_sheet(token)
|
clear_summary_sheet(token)
|
||||||
|
|
||||||
log(" 汇总订单数据...")
|
log(" 汇总订单数据...")
|
||||||
|
|
||||||
# 收集所有有效订单的行
|
# 收集所有有效订单的行,按 trade_no(X) 聚合
|
||||||
summary_rows = []
|
# key: trade_no -> {sales, nickname, ...}
|
||||||
|
trade_rows = {} # trade_no -> row_data
|
||||||
|
|
||||||
for sid, sname, _ in SALES_SHEETS:
|
for sid, sname, _ in SALES_SHEETS:
|
||||||
entries = all_entries[sid]
|
entries = all_entries[sid]
|
||||||
for e in entries:
|
for e in entries:
|
||||||
@ -721,8 +744,14 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
trade_no = di.get("trade_no", "")
|
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 = [
|
row_data = [
|
||||||
e["sales"], # A: 销售归属
|
e["sales"], # A: 销售归属
|
||||||
e["nickname"], # B: 微信昵称
|
e["nickname"], # B: 微信昵称
|
||||||
@ -746,41 +775,31 @@ def write_summary_sheet(token, all_entries, phone_map, db_info):
|
|||||||
di.get("lesson_minutes", 0) or "", # T: 学习时长
|
di.get("lesson_minutes", 0) or "", # T: 学习时长
|
||||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # U: 更新时间
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # U: 更新时间
|
||||||
classify_channel(di.get("order_channel", "")), # V: 渠道归属
|
classify_channel(di.get("order_channel", "")), # V: 渠道归属
|
||||||
"", # W: 留空
|
trade_no, # W: trade_no(=三表X, 同一run)
|
||||||
trade_no, # X: 订单号
|
|
||||||
]
|
]
|
||||||
summary_rows.append((trade_no, row_data))
|
trade_rows[trade_no] = row_data
|
||||||
|
|
||||||
# 同订单号去重(保留第一次出现)
|
deduped = list(trade_rows.values())
|
||||||
seen_trade = set()
|
log(f" 共 {len(deduped)} 条有效订单(按 unique X 去重)")
|
||||||
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)}")
|
|
||||||
|
|
||||||
if not deduped:
|
if not deduped:
|
||||||
log(" 无有效订单,跳过汇总")
|
log(" 无有效订单,跳过汇总")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 写入订单汇总 sheet(从第3行开始,A~X 共24列)
|
# 写入订单汇总 sheet(从第3行开始,A~W 共23列)
|
||||||
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
writer = FeishuSheetWriter(SPREADSHEET_TOKEN, token)
|
||||||
|
|
||||||
# 构建 A~X 的值数组(24列),确保每行长度一致
|
# 构建 A~W 的值数组(23列),确保每行长度一致
|
||||||
values = []
|
values = []
|
||||||
for row_data in deduped:
|
for row_data in deduped:
|
||||||
padded = row_data[:24]
|
padded = row_data[:23]
|
||||||
while len(padded) < 24:
|
while len(padded) < 23:
|
||||||
padded.append("")
|
padded.append("")
|
||||||
values.append(padded)
|
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 ═══
|
# ═══ Main ═══
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user