CaiWuXiTong/scripts/order-profit-shengsuan-sync.py
renshen 3a97bf942c feat: 订单利润接入聚胜算页面费用结果 (#987)
订单利润的包材费、快递费、账单费用直接读取聚胜算页面接口结果;新增抓取脚本与导入命令。
2026-11-07 17:34:38 +08:00

225 lines
8.5 KiB
Python
Executable File
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.

#!/usr/bin/env python3
"""从聚胜算订单利润页面接口抓取页面结果并导入财务系统。
使用前需要本机有已登录聚胜算的 Chrome 调试窗口(remote debugging port 9333),
并且本机 Docker 容器 jiangling_app 运行财务系统。
示例:
# 指定订单
python3 scripts/order-profit-shengsuan-sync.py --orders 5127581702031023636
# 按支付时间拉 7 天
python3 scripts/order-profit-shengsuan-sync.py --date-fld pay_date \
--from 2026-11-01 --to 2026-11-07
# 只打印 JSON,不导入
python3 scripts/order-profit-shengsuan-sync.py --orders 5127581702031023636 --no-import
"""
import argparse
import json
import subprocess
import sys
import time
import urllib.request
import websocket
API_URL = 'https://pf.erp321.com/WebApi/PF/OrderSku/GetOrderSkuList?uvalue=pfcweb_profit-report.order-profit'
CHROME_LIST_URL = 'http://127.0.0.1:9333/json/list'
def page_target(port: int):
with urllib.request.urlopen(f'http://127.0.0.1:{port}/json/list', timeout=5) as response:
targets = json.load(response)
pages = [target for target in targets
if target.get('type') == 'page' and 'ss.erp321.com' in (target.get('url') or '')]
if not pages:
raise RuntimeError('未找到已登录的胜算页面,请先扫码登录并打开订单利润页面。')
return pages[0]
def evaluate(ws: websocket.WebSocket, expression: str, await_promise: bool = False):
payload = {
'id': 1,
'method': 'Runtime.evaluate',
'params': {
'expression': expression,
'returnByValue': True,
'awaitPromise': await_promise,
},
}
ws.send(json.dumps(payload))
while True:
message = json.loads(ws.recv())
if message.get('id') == 1:
if 'error' in message:
raise RuntimeError(json.dumps(message['error'], ensure_ascii=False))
if 'exceptionDetails' in message.get('result', {}):
raise RuntimeError(json.dumps(message['result']['exceptionDetails'], ensure_ascii=False))
return message.get('result', {}).get('result', {}).get('value')
def fetch_page(ws: websocket.WebSocket, condition: dict, current_page: int, page_size: int):
request = {
'uid': '', 'coid': '', 'ip': '',
'page': {'currentPage': current_page, 'pageSize': page_size},
'data': {'condition': condition, 'isAds': False},
'ssProjectType': 'SS',
'ssClientType': 'Web',
}
expression = f"""(async () => {{
const res = await fetch('{API_URL}', {{
method: 'POST', credentials: 'include',
headers: {{'Content-Type': 'application/json'}},
body: JSON.stringify({json.dumps(request, ensure_ascii=False)})
}});
const text = await res.text();
return {{status: res.status, text}};
}})()"""
result = evaluate(ws, expression, await_promise=True)
if result is None or result.get('status') != 200:
raise RuntimeError(f'接口返回异常:{result}')
decoded = json.loads(result['text'])
if decoded.get('code') == 1908:
raise RuntimeError('RATE_LIMIT')
if decoded.get('code') not in (0, None):
raise RuntimeError(f"接口业务错误 code={decoded.get('code')} msg={decoded.get('msg')}")
return decoded
def condition_from_args(args):
base = {
'type': 'soId',
'dateFld': 'pay_date',
'combineDate': None,
'shop': [],
'multiPriceType': 'User3',
'multiplePrice': 63,
'costTypeList': [],
'orderType': [],
'drpCoidtos': [],
'wmsCoidList': [],
'buIdList': [],
'skuIn': {'type': 'anyonesku', 'values': [], 'select': 'in'},
'skuOut': {'type': 'anyonesku', 'values': [], 'select': 'out'},
'labelIn': {'type': 'anyonelabel', 'values': [], 'select': 'in'},
'labelout': {'type': 'anyonelabel', 'values': [], 'select': 'out'},
'creatorList': [], 'cusIdList': [], 'brands': [], 'categorys': [], 'vcNames': [],
'goodsCostGroup': {
'skuCostAmountSearchType': {'value': 2, 'label': '按固定值搜索商品成本'},
'value': '', 'values': ['', ''],
},
'backGoodsType': 1,
'splitCombine': True,
'groupByType': 1,
'skuCostAmountSearchType': 2,
'skuCostAmountList': [],
'firstfreightList': [''],
'stateCityList': [],
'isOL': True,
'skugroup': 'so_id',
'isValidateDate': False,
'isCrossBorder': False,
}
if args.orders:
base['type'] = 'soId'
base['idType'] = ','.join(args.orders)
base['dateFld'] = 'pay_date'
return base
base['dateFld'] = args.date_fld
base['type'] = 'oid' if args.date_fld == 'send_date' else 'soId'
base['groupByType'] = 1 if base['type'] == 'soId' else 2
base['combineDate'] = None
base['beginDate'] = args.begin
base['endDate'] = args.end
if args.shop:
base['shop'] = [{'value': shop, 'label': shop} for shop in args.shop]
return base
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--orders', nargs='*', default=[])
parser.add_argument('--date-fld', choices=['pay_date', 'send_date', 'order_date'], default='pay_date')
parser.add_argument('--from', dest='begin', default=None)
parser.add_argument('--to', dest='end', default=None)
parser.add_argument('--shop', nargs='*', default=[])
parser.add_argument('--page-size', type=int, default=5000)
parser.add_argument('--max-pages', type=int, default=0)
parser.add_argument('--page-delay', type=float, default=0.8)
parser.add_argument('--debug-port', type=int, default=9333)
parser.add_argument('--no-import', action='store_true')
parser.add_argument('--json-out', default='/tmp/order-profit-shengsuan.json')
args = parser.parse_args()
if not args.orders and (not args.begin or not args.end):
parser.error('订单模式需要 --orders,日期模式需要 --from 和 --to')
target = page_target(args.debug_port)
ws = websocket.create_connection(target['webSocketDebuggerUrl'], timeout=180)
try:
ws.send(json.dumps({'id': 0, 'method': 'Runtime.enable', 'params': {}}))
condition = condition_from_args(args)
current = 1
collected = []
heads = None
total = None
while True:
page = None
for attempt in range(1, 8):
try:
page = fetch_page(ws, condition, current, args.page_size)
break
except RuntimeError as exception:
if str(exception) != 'RATE_LIMIT':
raise
wait = min(300, 60 * attempt)
print(f'第 {attempt} 次限流,等待 {wait} 秒后重试……', flush=True)
time.sleep(wait)
if page is None:
raise RuntimeError('聚胜算接口持续限流,本次未完成。')
data = page.get('data') or {}
if heads is None:
heads = data.get('fldHeads') or []
collected.extend(data.get('dataList') or [])
page_info = page.get('page') or {}
total = page_info.get('count', len(collected))
pages = page_info.get('pages', 1)
if current >= pages or len(collected) >= total:
break
if args.max_pages and current >= args.max_pages:
break
current += 1
time.sleep(args.page_delay)
payload = {'data': {'fldHeads': heads, 'dataList': collected}, 'page': {'count': total}}
text = json.dumps(payload, ensure_ascii=False)
with open(args.json_out, 'w') as handle:
handle.write(text)
print(f'抓取 {len(collected)} 条,JSON:{args.json_out}')
if not args.no_import:
process = subprocess.run(
['docker', 'exec', '-i', '-u', 'www-data', 'jiangling_app',
'php', '-d', 'memory_limit=2G', 'artisan',
'order-profit:import-shengsuan-rows', '--stdin'],
input=text.encode('utf-8'),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False,
)
sys.stdout.write(process.stdout.decode('utf-8', 'replace'))
if process.returncode != 0:
sys.stderr.write(process.stderr.decode('utf-8', 'replace'))
return process.returncode
finally:
ws.close()
return 0
if __name__ == '__main__':
raise SystemExit(main())