276 lines
11 KiB
Python
276 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""平台订单提取(柔汇/子元素):写入 platform_order_data 表。
|
||
|
||
最小包装自 rouhui_extract_v3.py,对应 platform-order-rouhui。
|
||
"""
|
||
|
||
import time
|
||
import json
|
||
import hmac
|
||
import hashlib
|
||
import random
|
||
import requests
|
||
import urllib3
|
||
from urllib.parse import urlparse, parse_qs, quote
|
||
from collections import defaultdict
|
||
from datetime import date, timedelta, datetime
|
||
|
||
from prefect import flow
|
||
|
||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||
|
||
# ===================== 柔汇openapi配置 =====================
|
||
host_rouhui = "https://openapi.rouhuipod.com"
|
||
app_id_rouhui = "smt01zi783662727"
|
||
app_key_rouhui = "fd1bec5b190e5c52b6a15d9fe1086fe9"
|
||
redirect_uri_rouhui = "https://demo-callback.virtual-api.com/oauth/callback"
|
||
login_url_rouhui = f"{host_rouhui}/openapi/v2/oauth2/login"
|
||
token_url_rouhui = f"{host_rouhui}/openapi/v2/oauth2/token"
|
||
order_list_url_rouhui = "/openapi/v2/order"
|
||
|
||
PLATFORM_MAP = {
|
||
2: "Temu-半托管", 3: "亚马逊", 4: "Tiktok-自运营",
|
||
9: "SHEIN-半托管", 10: "Temu-全托管", 12: "Temu-本土店", 13: "Shein-自运营"
|
||
}
|
||
PLATFORM_ID_LIST = [2, 3, 4, 9, 10, 12, 13]
|
||
SHORT_PLATFORM_MAP = {
|
||
"Temu-半托管": "Temu", "亚马逊": "亚马逊", "Tiktok-自运营": "tiktok",
|
||
"SHEIN-半托管": "shein", "Temu-全托管": "Temu", "Temu-本土店": "Temu", "Shein-自运营": "shein"
|
||
}
|
||
|
||
rouhui_login_form = {
|
||
"_token": "merchant", "merchant": "smt01",
|
||
"username": "smt01", "password": "Sanmu111.",
|
||
"app_id": app_id_rouhui, "redirect_uri": redirect_uri_rouhui
|
||
}
|
||
|
||
LOOKBACK_DAYS = 30 # 回查天数
|
||
HISTORY_COMPLEMENT_MODE = False # True=历史补数模式;False=日常增量模式
|
||
|
||
def log_msg(msg, level="info"):
|
||
ts = time.strftime('%H:%M:%S')
|
||
tag = {"success":"[OK]","error":"[ERR]"}.get(level,"")
|
||
print(f"[{ts}] {tag} {msg}")
|
||
|
||
def format_date_only(date_str):
|
||
if not date_str: return ""
|
||
return str(date_str).split(" ")[0]
|
||
|
||
# ===================== 柔汇签名与登录 =====================
|
||
def get_rand(): return str(random.randint(100000, 999999))
|
||
def get_ts(): return str(int(time.time()))
|
||
|
||
def calc_sign(params):
|
||
clean = {}
|
||
for k, v in params.items():
|
||
if v not in (None, ""): clean[k] = v
|
||
buf = []
|
||
for k in sorted(clean.keys()):
|
||
v = clean[k]
|
||
if isinstance(v, (dict, list)):
|
||
v = json.dumps(v, ensure_ascii=False, separators=(',', ':'))
|
||
buf.append(f"{k}={v}")
|
||
return hmac.new(app_key_rouhui.encode("utf-8"), "&".join(buf).encode("utf-8"), hashlib.sha256).hexdigest()
|
||
|
||
def rouhui_get_token():
|
||
"""登录获取access_token"""
|
||
headers = {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||
"Referer": f"{host_rouhui}/openapi/v2/oauth2?app_id={app_id_rouhui}&redirect_uri={quote(redirect_uri_rouhui)}",
|
||
"Origin": host_rouhui
|
||
}
|
||
try:
|
||
resp = requests.post(login_url_rouhui, data=rouhui_login_form, headers=headers, allow_redirects=False, verify=False, timeout=15)
|
||
redirect_url = resp.headers.get("Location")
|
||
if not redirect_url:
|
||
log_msg("柔汇登录无302跳转", "error"); return ""
|
||
code = parse_qs(urlparse(redirect_url).query)["code"][0]
|
||
log_msg("柔汇登录成功", "success")
|
||
except Exception as e:
|
||
log_msg(f"柔汇登录失败: {e}", "error"); return ""
|
||
|
||
params = {"code": code, "app_id": app_id_rouhui, "rand_str": get_rand(), "timestamp": get_ts()}
|
||
params["signature"] = calc_sign(params)
|
||
try:
|
||
resp = requests.post(token_url_rouhui, json=params, headers={"Content-Type": "application/json"}, timeout=10, verify=False)
|
||
data = resp.json()
|
||
if data.get("code") == 1:
|
||
log_msg("柔汇token获取成功", "success")
|
||
return data["data"]["access_token"]
|
||
log_msg(f"柔汇token失败: {data.get('message')}", "error")
|
||
except Exception as e:
|
||
log_msg(f"柔汇token网络异常: {e}", "error")
|
||
return ""
|
||
|
||
# ✅增加入参 begin_date,end_date,不再读取外部变量
|
||
def rouhui_fetch_platform_order(access_token, platform_id, begin_date, end_date):
|
||
"""拉取指定平台订单"""
|
||
bd = begin_date
|
||
ed = end_date
|
||
all_orders = []
|
||
page = 1
|
||
page_size = 200
|
||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {access_token}"}
|
||
full_name = PLATFORM_MAP[platform_id]
|
||
|
||
while True:
|
||
query = {"page": page, "page_size": page_size, "platform_id": platform_id,
|
||
"status": 3, "create_time": [bd, ed]}
|
||
common = {"app_id": app_id_rouhui, "rand_str": get_rand(), "timestamp": get_ts()}
|
||
all_params = {**query, **common}
|
||
all_params["signature"] = calc_sign(all_params)
|
||
|
||
try:
|
||
resp = requests.post(f"{host_rouhui}{order_list_url_rouhui}", json=all_params, headers=headers, timeout=15, verify=False)
|
||
data = resp.json()
|
||
if data.get("code") != 1:
|
||
log_msg(f"{full_name} 查询失败: {data.get('message')}", "error")
|
||
break
|
||
lst = data["data"].get("list", [])
|
||
if not lst: break
|
||
all_orders.extend(lst)
|
||
if len(lst) < page_size: break
|
||
page += 1
|
||
time.sleep(0.2)
|
||
except Exception as e:
|
||
log_msg(f"{full_name} page{page}异常: {e}", "error")
|
||
time.sleep(0.5)
|
||
|
||
return all_orders
|
||
|
||
# ===================== 主流程 =====================
|
||
@flow(name="platform-order-rouhui", log_prints=True)
|
||
def rouhui_platform_sync():
|
||
TODAY = date.today()
|
||
if HISTORY_COMPLEMENT_MODE:
|
||
# 历史补数模式: [T‑2*N , T‑N]
|
||
start_dt = TODAY - timedelta(days=LOOKBACK_DAYS * 2)
|
||
end_dt = TODAY - timedelta(days=LOOKBACK_DAYS)
|
||
else:
|
||
# 日常增量模式: [T‑N , T]
|
||
start_dt = TODAY - timedelta(days=LOOKBACK_DAYS)
|
||
end_dt = TODAY
|
||
|
||
START_DATE = start_dt.isoformat()
|
||
END_DATE = end_dt.isoformat()
|
||
|
||
log_msg(f"===== 子元素(柔汇)订单提取 v2.0 =====")
|
||
log_msg(f"查询时间: {START_DATE} ~ {END_DATE} (回查{LOOKBACK_DAYS}天)")
|
||
|
||
merged_detail = []
|
||
rouhui_stat = {}
|
||
rouhui_total_order = 0
|
||
rouhui_total_qty = 0
|
||
|
||
try:
|
||
rh_token = rouhui_get_token()
|
||
if not rh_token:
|
||
log_msg("柔汇登录失败,终止", "error")
|
||
return
|
||
|
||
for pid in PLATFORM_ID_LIST:
|
||
full_name = PLATFORM_MAP[pid]
|
||
short_name = SHORT_PLATFORM_MAP[full_name]
|
||
log_msg(f"\n拉取柔汇【{full_name}】(pid={pid})")
|
||
# ✅调用时把main内部日期传进去
|
||
order_list = rouhui_fetch_platform_order(rh_token, pid, START_DATE, END_DATE)
|
||
p_order_cnt = len(order_list)
|
||
p_qty = 0
|
||
rouhui_stat[short_name] = [p_order_cnt, 0]
|
||
|
||
if not order_list:
|
||
log_msg(f" {full_name}: 无订单")
|
||
continue
|
||
|
||
for order in order_list:
|
||
order_no = order.get("order_no", "")
|
||
store = order.get("store_name", "未知")
|
||
create_t = order.get("create_time", "")
|
||
create_date_only = format_date_only(create_t)
|
||
items = order.get("order_items", [])
|
||
total_qty = sum(i.get("third_qty", 1) for i in items)
|
||
p_qty += total_qty
|
||
merged_detail.append(["墨西哥", "墨西哥", short_name, store, order_no, total_qty, create_date_only, "子元素", "已发货"])
|
||
|
||
rouhui_stat[short_name][1] = p_qty
|
||
rouhui_total_order += p_order_cnt
|
||
rouhui_total_qty += p_qty
|
||
log_msg(f" {full_name}: {p_order_cnt}单, {p_qty}件")
|
||
|
||
except Exception as e:
|
||
log_msg(f"柔汇API异常: {e}", "error")
|
||
|
||
# 统一平台名称
|
||
for row in merged_detail:
|
||
if row[2].upper() == "TEMU": row[2] = "Temu"
|
||
|
||
# 拆分Tiktok/非Tiktok
|
||
tiktok_rows = [row for row in merged_detail if "tiktok" in row[2].lower()]
|
||
non_tiktok_rows = [row for row in merged_detail if "tiktok" not in row[2].lower()]
|
||
|
||
# 统计
|
||
nt_plat_stat = defaultdict(lambda: [0, 0])
|
||
nt_country_stat = defaultdict(lambda: [0, 0])
|
||
for row in non_tiktok_rows:
|
||
nt_plat_stat[row[2]][0] += 1; nt_plat_stat[row[2]][1] += row[5]
|
||
nt_country_stat[row[1]][0] += 1; nt_country_stat[row[1]][1] += row[5]
|
||
|
||
tk_plat_stat = defaultdict(lambda: [0, 0])
|
||
tk_country_stat = defaultdict(lambda: [0, 0])
|
||
for row in tiktok_rows:
|
||
tk_plat_stat[row[2]][0] += 1; tk_plat_stat[row[2]][1] += row[5]
|
||
tk_country_stat[row[1]][0] += 1; tk_country_stat[row[1]][1] += row[5]
|
||
|
||
total_qty = rouhui_total_qty
|
||
|
||
log_msg(f"\n========== 统计汇总 ==========")
|
||
log_msg(f"非Tiktok总件数:{sum(v[1] for v in nt_plat_stat.values())} 件")
|
||
log_msg(f"Tiktok总件数:{sum(v[1] for v in tk_plat_stat.values())} 件")
|
||
log_msg(f"全部商品总件数:{total_qty} 件")
|
||
|
||
# ===== 导入PostgreSQL =====
|
||
try:
|
||
import psycopg2
|
||
conn = psycopg2.connect(host="192.168.124.92", port=5432, user="postgres", password="Inkreach123456.", database="smt")
|
||
cur = conn.cursor()
|
||
cur.execute("""
|
||
CREATE TABLE IF NOT EXISTS platform_order_data (
|
||
id SERIAL PRIMARY KEY, site VARCHAR(50), country VARCHAR(50),
|
||
platform VARCHAR(50), store_name VARCHAR(200), order_no VARCHAR(100),
|
||
quantity INTEGER, order_date DATE, order_system VARCHAR(50),
|
||
order_status VARCHAR(100),
|
||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(order_no, order_system)
|
||
)
|
||
""")
|
||
conn.commit()
|
||
|
||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
db_rows = [(r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], now_str) for r in merged_detail]
|
||
|
||
sql = """
|
||
INSERT INTO platform_order_data (site, country, platform, store_name, order_no, quantity, order_date, order_system, order_status, update_time)
|
||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||
ON CONFLICT (order_no, order_system) DO UPDATE SET
|
||
quantity = EXCLUDED.quantity,
|
||
order_date = EXCLUDED.order_date,
|
||
order_status = EXCLUDED.order_status,
|
||
update_time = EXCLUDED.update_time
|
||
"""
|
||
batch_size = 500
|
||
for i in range(0, len(db_rows), batch_size):
|
||
cur.executemany(sql, db_rows[i:i + batch_size])
|
||
conn.commit()
|
||
|
||
cur.execute("SELECT COUNT(*) FROM platform_order_data")
|
||
cnt = cur.fetchone()[0]
|
||
cur.close(); conn.close()
|
||
log_msg(f"已同步到PostgreSQL, 总{cnt}条", "success")
|
||
except Exception as e:
|
||
log_msg(f"PostgreSQL异常: {e}", "error")
|
||
|
||
log_msg("===== 提取完成 =====")
|
||
|
||
if __name__ == "__main__":
|
||
rouhui_platform_sync.serve(name="platform-order-rouhui", cron="*/15 * * * *") |