Files
inkreach-prefect/flows/platform_order/sdspod.py
T

475 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""平台订单提取(SDSPOD/印美达):写入 platform_order_data 表。
最小包装自 sdspod_extract_v5.py,对应 platform-order-sdspod。
"""
import time
import json
import asyncio
import aiohttp
import math
import requests
import urllib3
from collections import defaultdict
from datetime import date, timedelta, datetime
from prefect import flow
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# ===================== 共享配置 =====================
COMPANY_NO = "印美达"
EXTRA_INFO = "eyJpcCI6IjM4LjcxLjEyNy4xODkiLCJsb2NhdGlvbiI6eyJsYXQiOjM4Ljg4MzMsImxuZyI6LTc3fSwiYWRfaW5mbyI6eyJuYXRpb24iOiLnvo7lm70iLCJwcm92aW5jZSI6IiIsImNpdHkiOiIiLCJkaXN0cmljdCI6IiIsImFkY29kZSI6LTEsIm5hdGlvbl9jb2RlIjo4NDB9LCJmaW5nZXJwcmludElkIjoiN2M3YzllMDUxYWU2NDkzOTEzMGY5OWI0NjhiY2FhYTUiLCJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW4sgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdl2tvKSBDaHJvbWUvMTM4LjAuMC4wIFNhZmFyaS81MzcuMzyiLCJocmVmIjoiaHR0cHM6Ly9tYW5hZ2Uuc2RzcG9kLmNvbS91c2VyL2xvZ2luIn0="
USERNAME_SD = "叶富华"
PASSWORD_SD = "Aa123456."
MAX_RETRY = 3
REQUEST_TIMEOUT = 30
# ===================== 国家映射(共用) =====================
COUNTRY_MAP = {
"United States": "美国", "America": "美国", "USA": "美国",
"Mexico": "墨西哥", "Canada": "加拿大",
"United Kingdom": "英国", "UK": "英国",
"Germany": "德国", "France": "法国", "Italy": "意大利",
"Spain": "西班牙", "Portugal": "葡萄牙",
"Netherlands": "荷兰", "Belgium": "比利时",
"Switzerland": "瑞士", "Sweden": "瑞典", "Norway": "挪威",
"Denmark": "丹麦", "Finland": "芬兰", "Poland": "波兰",
"Czech Republic": "捷克", "Czech": "捷克",
"Austria": "奥地利", "Hungary": "匈牙利",
"Romania": "罗马尼亚", "Greece": "希腊",
"Ireland": "爱尔兰", "Bulgaria": "保加利亚",
"Croatia": "克罗地亚", "Slovakia": "斯洛伐克",
"Slovenia": "斯洛文尼亚", "Lithuania": "立陶宛",
"Latvia": "拉脱维亚", "Estonia": "爱沙尼亚",
"Luxembourg": "卢森堡", "Malta": "马耳他",
"Cyprus": "塞浦路斯", "Iceland": "冰岛",
"Liechtenstein": "列支敦士登", "Monaco": "摩纳哥", "Andorra": "安道尔",
"Australia": "澳大利亚", "New Zealand": "新西兰",
"Japan": "日本", "South Korea": "韩国",
"Brazil": "巴西", "Argentina": "阿根廷",
"Chile": "智利", "Colombia": "哥伦比亚", "Peru": "秘鲁",
"India": "印度", "Russia": "俄罗斯", "Turkey": "土耳其",
"Israel": "以色列",
"UAE": "阿联酋", "United Arab Emirates": "阿联酋",
"Saudi Arabia": "沙特阿拉伯",
"Singapore": "新加坡", "Malaysia": "马来西亚",
"Indonesia": "印度尼西亚", "Thailand": "泰国",
"Philippines": "菲律宾", "Vietnam": "越南",
}
EUROPEAN_COUNTRIES = {
"德国", "法国", "意大利", "西班牙", "葡萄牙", "荷兰", "比利时",
"瑞士", "瑞典", "挪威", "丹麦", "芬兰", "波兰", "捷克", "奥地利",
"匈牙利", "罗马尼亚", "希腊", "爱尔兰", "保加利亚", "克罗地亚", "斯洛伐克",
"斯洛文尼亚", "立陶宛", "拉脱维亚", "爱沙尼亚", "卢森堡", "马耳他", "塞浦路斯",
"冰岛", "列支敦士登", "摩纳哥", "安道尔",
"Germany", "France", "Italy", "Spain", "Portugal",
"Netherlands", "Belgium", "Switzerland", "Sweden", "Norway", "Denmark",
"Finland", "Poland", "Czech Republic", "Austria", "Hungary", "Romania",
"Greece", "Ireland", "Bulgaria", "Croatia", "Slovakia", "Slovenia",
"Lithuania", "Latvia", "Estonia", "Luxembourg", "Malta", "Cyprus",
"Iceland", "Liechtenstein", "Monaco", "Andorra"
}
def to_chinese_country(name):
if not name: return ""
return COUNTRY_MAP.get(name.strip(), name.strip())
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 sd_login_sync():
ts = int(time.time() * 1000)
url = f"https://pod-api.sdspod.com/pod/auth/login?t={ts}"
headers = {"content-type": "application/json;charset=UTF-8"}
payload = {"no": COMPANY_NO, "password": PASSWORD_SD, "username": USERNAME_SD, "extraInfo": EXTRA_INFO}
for i in range(MAX_RETRY):
try:
resp = requests.post(url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT)
data = resp.json()
if data.get("ret") == 0:
token = data.get("data", {}).get("accessToken")
if token:
log_msg("SDSPOD登录成功", "success")
return token
log_msg(f"SD登录失败: {data.get('msg')}", "error")
return ""
except Exception as e:
if i < MAX_RETRY - 1:
time.sleep(1)
return ""
# ===================== 数据库通用插入函数 =====================
def insert_orders_to_db(order_rows, order_system):
"""将统一格式的订单列表插入数据库,order_system 用于区分来源"""
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 = []
for r in order_rows:
# r = [站点, 国家, 平台, 店铺, 订单号, 数量, 日期, 订单系统, 状态]
order_date = r[6] if r[6] else None
db_rows.append((r[0], r[1], r[2], r[3], r[4], r[5], order_date, order_system, r[8], now_str))
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):
batch = db_rows[i:i + batch_size]
cur.executemany(sql, batch)
conn.commit()
cur.execute("SELECT COUNT(*) FROM platform_order_data")
cnt = cur.fetchone()[0]
cur.close()
conn.close()
log_msg(f"已同步到PostgreSQL, 总{cnt}条 (来源: {order_system})", "success")
return True
except Exception as e:
log_msg(f"PostgreSQL导入异常 ({order_system}): {str(e)}", "error")
return False
# ======================================================================
# 脚本1:按支付时间拉取最近30天订单(同步,productionType=1,仅一个商户)
# ======================================================================
def fetch_one_page_pay(token, page, pay_begin, pay_end):
url = "https://pod-api.sdspod.com/pod/orders/page"
headers = {"access-token": token}
params = {
"merchantIds": "35876",
"productionType": 1,
"page": page,
"size": 100,
"originType": "normal",
"payDateBegin": pay_begin,
"payDateEnd": pay_end,
"t": int(time.time() * 1000)
}
for attempt in range(MAX_RETRY):
try:
resp = requests.get(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT)
return resp.json()
except Exception as e:
log_msg(f"支付单第{page}页请求异常:{str(e)},重试 {attempt+1}/{MAX_RETRY}")
time.sleep(0.8)
return None
def fetch_all_orders_by_pay(token):
now = datetime.now()
pay_end = now.strftime("%Y-%m-%d 23:59:59")
pay_begin = (now - timedelta(days=30)).strftime("%Y-%m-%d 00:00:00")
# now = datetime.now()
# pay_end = (now - timedelta(days=120)).strftime("%Y-%m-%d 23:59:59")
# pay_begin = (now - timedelta(days=280)).strftime("%Y-%m-%d 00:00:00")
log_msg(f"[脚本1] 筛选支付时间:{pay_begin} ~ {pay_end}")
all_list = []
first_resp = fetch_one_page_pay(token, page=1, pay_begin=pay_begin, pay_end=pay_end)
if not first_resp:
log_msg("[脚本1] 第一页获取失败,终止拉取", "error")
return all_list
total_count = first_resp.get("totalCount", 0)
if total_count <= 0:
log_msg("[脚本1] 该时间段没有订单数据")
return all_list
total_pages = math.ceil(total_count / 100)
log_msg(f"[脚本1] 总订单:{total_count} 条,共{total_pages}页")
all_list.extend(first_resp.get("list", []))
for pg in range(2, total_pages + 1):
page_resp = fetch_one_page_pay(token, pg, pay_begin=pay_begin, pay_end=pay_end)
if page_resp and "list" in page_resp:
page_data = page_resp.get("list", [])
all_list.extend(page_data)
time.sleep(0.3)
log_msg(f"[脚本1] 拉取完成,合计 {len(all_list)} 条订单")
return all_list
def parse_order_item_pay(item):
"""
解析为统一格式:
[站点, 国家, 平台, 店铺名称, 订单号, 商品数量, 下单日期, 订单系统, 订单状态]
"""
order_no = item.get("no", "")
country_raw = item.get("address", {}).get("countryName", "")
country = to_chinese_country(country_raw)
store_name = item.get("merchantStore", {}).get("name", "")
platform = item.get("merchantStore", {}).get("platformName", "") # 可能不存在
gmt_created = item.get("gmtCreated", "")
order_date = gmt_created.split(" ")[0] if gmt_created else ""
total_qty = 0
for parcel in item.get("parcelList", []):
total_qty += parcel.get("totalQty", 0)
if country_raw in EUROPEAN_COUNTRIES or country in EUROPEAN_COUNTRIES:
country = "欧区"
site = country # 站点暂用国家
else:
site = country
status = str(item.get("status", ""))
return [site, country, platform, store_name, order_no, total_qty, order_date, "SDSPOD", status]
def main_script1():
log_msg("===== 脚本1:按支付时间拉取最近30天订单 =====")
token = sd_login_sync()
if not token:
log_msg("[脚本1] 登录失败,终止", "error")
return
orders = fetch_all_orders_by_pay(token)
if not orders:
log_msg("[脚本1] 没有订单数据")
return
parsed_rows = []
for item in orders:
if item.get("status") == 98:
continue
row = parse_order_item_pay(item)
parsed_rows.append(row)
log_msg(f"[脚本1] 有效订单: {len(parsed_rows)} 单")
if parsed_rows:
insert_orders_to_db(parsed_rows, "SDSPOD")
log_msg("===== 脚本1 完成 =====")
# ======================================================================
# 脚本2:按创建时间回查19天订单(异步,productionType=2,多商户)
# ======================================================================
MERCHANT_IDS = "35876,45637,52649,52699,35956,60756"
PRODUCTION_TYPE = 2
PAGE_SIZE_SD = 100
MAX_CONCURRENT = 5
LOOKBACK_DAYS = 7
def format_date_only(date_str):
if not date_str: return ""
return str(date_str).split(" ")[0]
async def sd_fetch_page(session, sem, params, pg, total):
async with sem:
for i in range(MAX_RETRY):
try:
timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
async with session.get("https://pod-api.sdspod.com/pod/orders/page", params=params, timeout=timeout) as resp:
data = await resp.json()
return data.get("list", [])
except:
if i < MAX_RETRY - 1:
await asyncio.sleep(0.5)
log_msg(f"SD第{pg}/{total}页拉取失败", "error")
return []
async def sd_fetch_batch(token, begin_str, end_str):
headers = {"access-token": token}
begin = f"{begin_str} 00:00:00"
end = f"{end_str} 23:59:59"
first_params = {
"page": 1, "size": PAGE_SIZE_SD, "orderTimeRangeType": "",
"createdDateBegin": begin, "createdDateEnd": end,
"merchantIds": MERCHANT_IDS, "productionType": PRODUCTION_TYPE,
"t": int(time.time() * 1000)
}
connector = aiohttp.TCPConnector(limit=MAX_CONCURRENT * 2, ttl_dns_cache=300)
async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
timeout = aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
async with session.get("https://pod-api.sdspod.com/pod/orders/page", params=first_params, timeout=timeout) as resp:
first_data = await resp.json()
if first_data.get("ret") == 20001:
log_msg("Token过期", "error")
return []
total = first_data.get("totalCount", 0)
if total == 0:
return []
total_pages = math.ceil(total / PAGE_SIZE_SD)
log_msg(f" SD {begin_str}~{end_str}: {total}单/{total_pages}页")
first_list = first_data.get("list", [])
if total_pages == 1:
return first_list
sem = asyncio.Semaphore(MAX_CONCURRENT)
tasks = []
for pg in range(2, total_pages + 1):
pg_params = {
"page": pg, "size": PAGE_SIZE_SD, "orderTimeRangeType": "",
"createdDateBegin": begin, "createdDateEnd": end,
"merchantIds": MERCHANT_IDS, "productionType": PRODUCTION_TYPE,
"t": int(time.time() * 1000)
}
tasks.append(sd_fetch_page(session, sem, pg_params, pg, total_pages))
results = await asyncio.gather(*tasks)
all_data = first_list
for lst in results:
all_data.extend(lst)
return all_data
def sd_format_item(item):
address = item.get("address", {})
country = address.get("countryName", "")
store = item.get("merchantStore", {})
platform = store.get("platformName", "")
create_time = item.get("gmtCreated", "")
parcel_list = item.get("parcelList", [])
if parcel_list and isinstance(parcel_list, list):
qty = sum(p.get("totalQty", 0) for p in parcel_list)
else:
qty = 0
return [
to_chinese_country(country),
to_chinese_country(country),
platform,
store.get("name", "未知"),
item.get("no", ""),
qty,
format_date_only(create_time),
"SDSPOD",
str(item.get("status", ""))
]
async def main_script2():
log_msg("===== 脚本2:SDSPOD订单提取工具 v2.0 =====")
TODAY = date.today()
START_DATE = (TODAY - timedelta(days=LOOKBACK_DAYS)).isoformat()
END_DATE = TODAY.isoformat()
# TODAY = date.today()
# START_DATE = (TODAY - timedelta(days=180)).isoformat()
# END_DATE = (TODAY - timedelta(days=60)).isoformat()
log_msg(f"查询时间: {START_DATE} ~ {END_DATE} (回查{LOOKBACK_DAYS}天)")
sd_raw = []
start_d = datetime.strptime(START_DATE, "%Y-%m-%d").date()
end_d = datetime.strptime(END_DATE, "%Y-%m-%d").date()
d = start_d
batches = []
while d <= end_d:
batch_end = min(d + timedelta(days=2), end_d)
batches.append((d.isoformat(), batch_end.isoformat()))
d = batch_end + timedelta(days=1)
log_msg(f"共{len(batches)}批次拉取")
sd_token = sd_login_sync()
if not sd_token:
log_msg("登录失败,终止", "error")
return
for bi, (b_start, b_end) in enumerate(batches):
raw = await sd_fetch_batch(sd_token, b_start, b_end)
sd_raw.extend(raw)
if bi > 0 and bi % 5 == 0:
sd_token = sd_login_sync()
if not sd_token:
break
log_msg(f"API拉取总数: {len(sd_raw)} 条", "success")
sd_detail = []
for item in sd_raw:
if item.get("status") == 98:
continue
row = sd_format_item(item)
cn = row[1]
if cn in EUROPEAN_COUNTRIES:
row[0] = "欧区"
row[1] = "欧区"
sd_detail.append(row)
sd_total_order = len(sd_detail)
sd_total_qty = sum(r[5] for r in sd_detail)
log_msg(f"有效订单: {sd_total_order}单, {sd_total_qty}件", "success")
tiktok_rows = [r for r in sd_detail if "tiktok" in r[2].lower()]
non_tiktok_rows = [r for r in sd_detail if "tiktok" not in r[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 = sd_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} 件")
if sd_detail:
insert_orders_to_db(sd_detail, "SDSPOD")
log_msg("===== 脚本2 完成 =====")
# ======================================================================
# 主入口:先执行脚本1(支付订单),再执行脚本2(创建订单)
# ======================================================================
@flow(name="platform-order-sdspod", log_prints=True)
async def sdspod_platform_sync():
await asyncio.to_thread(main_script1)
await main_script2()
if __name__ == "__main__":
# 直跑一次用于本地调试;勿改回 serve():同名 upsert 会覆盖 server 上挂 work pool 的正式 deployment
sdspod_platform_sync()