🤖 每日自动备份 - 2026-06-18 08:00:01

This commit is contained in:
小溪 2026-06-18 08:00:01 +08:00
parent bf639859ac
commit ada43679aa
5 changed files with 453 additions and 37 deletions

View File

@ -45,6 +45,7 @@
| 刘彦江 | `1da2afbf` |
| 姜小龙 | `bc227c85` |
| 赵一凡 | `a3168f9d` |
| 李丹 | `ea523c46` |
> ⚠️ 以上用户拥有全部数据查询权限,但其个人信息、查询内容、对话记录**禁止写入 MEMORY.md长期记忆**,仅可记录在短期日记忆中用于会话连续性。

4
memory/2026-06-17.md Normal file
View File

@ -0,0 +1,4 @@
# 2026-06-17 工作日志
## 权限变更
- [李承龙确认] 将李丹(`ea523c46`)添加至 USER.md 一级完整权限用户列表

15
memory/2026-06-18.md Normal file
View 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列

View File

@ -0,0 +1,377 @@
#!/usr/bin/env python3
"""
财务口径订单刷新 v3 从订单汇总 2smjwA 出发
逻辑:
1. 读订单汇总 2smjwA (A3:W)
2. 逐行复制 AU 镜像段 E 列手机号查 DB 该用户全部订单
3. N = N 每行 X=trade_noKP 写该单金额
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
# AU 镜像段 (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:
# 无订单写一行AU 镜像,其余留空
row = a_to_u[:21] # AU
row += ["", "", "", "", "", ""] # V W X Y Z
rows.append(row)
else:
for o in orders:
# AU 镜像段 (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())

View File

@ -376,7 +376,24 @@ def query_all_pg(all_entries, phone_map):
if aid not in info:
continue
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]
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: 下单日期, L: 成交渠道, M: 产品
k_vals.append([order_date])
l_vals.append([di["order_channel"]])
x_vals.append([di.get("trade_no", "")])
# 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: 微信昵称
@ -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 ═══