feat(yun8_sync): add API pipeline automating full freight flow (18 interfaces + SQL)
Implements the "运输流程" collection from yun8.postman_collection.json as a Python pipeline: driver-side order accept/load/unload -> freight accounting -> payment, with SQL writes, driven by yun8_sync/config.yaml (api/auth/accounting/payment/database/sql/dianyuntong sections).
This commit is contained in:
@@ -0,0 +1,965 @@
|
||||
"""
|
||||
yun8 API 客户端
|
||||
===============
|
||||
封装 Postman Collection 中运输流程的全部 16 个接口。
|
||||
Header 构造、Pre-request Script 逻辑、Test Script 变量提取全部在此实现。
|
||||
|
||||
接口列表:
|
||||
[Driver 端 — 5个]
|
||||
01. 查询货源 GET /ntocc-tms-api/order/orderHallList
|
||||
02. 接单 POST /ntocc-tms-api/orderDetail/receive
|
||||
03. 查询待运输单 GET /ntocc-tms-api/orderDetail
|
||||
04. 装货 PUT /ntocc-tms-api/orderDetail/fill
|
||||
05. 卸货 PUT /ntocc-tms-api/orderDetail/uploadTicketImage
|
||||
|
||||
[Admin 端 — 11个]
|
||||
06. 运费核算查询运单 GET /ntocc-tms-api/orderDetail/orderDetailHandlePageList
|
||||
07. 查询运单详情 POST /ntocc-tms-api/orderDetail/getOrderDetailExpensesParamList
|
||||
08. 核算试算 PUT /ntocc-tms-api/orderDetail/calculateOrderDetail
|
||||
09. 核算提交 PUT /ntocc-tms-api/orderDetail/orderHandleEditSubmitHandle
|
||||
10. 查询运单审核 GET /ntocc-tms-api/orderDetail?status=6&isSettle=1
|
||||
11. 核算审核通过 POST /ntocc-tms-api/orderDetail/auditOrderDetail
|
||||
12. 垫资-财务打款 POST /ntocc-tms-api/arpaTransfer/payToDriver
|
||||
13. 垫资-货主打款 POST /ntocc-tms-api/orderDetail/applicationForPayment
|
||||
14. 查询运单上传 GET /ntocc-basic-api/dytBusiness/orderDetail?status=5
|
||||
15. 查询打款记录 GET /ntocc-tms-api/remitDefinite
|
||||
16. 查询文件名前缀 GET /ntocc-tms-api/centralizedPayment/downLoadElectronicReceipt
|
||||
"""
|
||||
|
||||
import math
|
||||
import re
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Pre-request Script 逻辑:生成当前时间戳
|
||||
# ============================================================
|
||||
def _current_time_str() -> str:
|
||||
"""Postman pre-request: 生成 'YYYY-MM-DD HH:MM:SS' 格式的当前时间"""
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _debug_full_url(url: str):
|
||||
"""调试:打印 URL 中 ticketImageUrls / unloadImgaes 参数片段"""
|
||||
import re as _re
|
||||
# 找 ticketImageUrls[]= 后面的值(到下一个 & 或行尾)
|
||||
m = _re.search(r'ticketImageUrls%5B%5D=([^&]{0,300})', url)
|
||||
if m:
|
||||
print(f" [DEBUG] ticketImageUrls[] = {m.group(1)}")
|
||||
m2 = _re.search(r'unloadImgaes=([^&]{0,300})', url)
|
||||
if m2:
|
||||
print(f" [DEBUG] unloadImgaes = {m2.group(1)}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Yun8Client
|
||||
# ============================================================
|
||||
class Yun8Client:
|
||||
"""封装 yun8 全部 API 请求"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
api = config["api"]
|
||||
self.base_url = api["base_url"].rstrip("/")
|
||||
self.verify_ssl = api.get("verify_ssl", False)
|
||||
self.common = api.get("common_headers", {})
|
||||
|
||||
# ---- Driver Session ----
|
||||
self.driver = requests.Session()
|
||||
drv = api.get("driver", {})
|
||||
self._init_session(self.driver, drv)
|
||||
self.driver.headers["host"] = self.common.get("host", "")
|
||||
self.driver.headers["accept-language"] = self.common.get("accept-language", "")
|
||||
self.driver.headers["accept-encoding"] = self.common.get("accept-encoding", "")
|
||||
|
||||
# ---- Admin Session ----
|
||||
self.admin = requests.Session()
|
||||
adm = api.get("admin", {})
|
||||
self._init_session(self.admin, adm)
|
||||
self.admin.headers["host"] = self.common.get("host", "")
|
||||
self.admin.headers["accept-language"] = self.common.get("accept-language", "")
|
||||
self.admin.headers["accept-encoding"] = self.common.get("accept-encoding", "")
|
||||
|
||||
# ---- Third-party Session (滇运通,独立 token/host) ----
|
||||
tp = config.get("dianyuntong", {})
|
||||
self.tp_enabled = tp.get("enabled", False)
|
||||
self.tp = None
|
||||
if self.tp_enabled:
|
||||
self.tp = requests.Session()
|
||||
tp_headers = {
|
||||
"host": "yndyt.cxtech.vip",
|
||||
"authorization": tp.get("token", ""),
|
||||
"deviceid": "0000000000",
|
||||
"secret": "GouldAMap=d58bb5f7da1aa1d6b78741ded2450f2d",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0",
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"origin": tp.get("base_url", ""),
|
||||
"referer": f"{tp.get('base_url', '')}/admin/",
|
||||
"cookie": tp.get("cookie", ""),
|
||||
}
|
||||
for k, v in tp_headers.items():
|
||||
if v:
|
||||
self.tp.headers[k] = v
|
||||
self.tp.trust_env = False
|
||||
self.tp.proxies = {"http": None, "https": None}
|
||||
self.tp_base = tp.get("base_url", "").rstrip("/")
|
||||
|
||||
# 快捷引用
|
||||
self.dp = config.get("driver_params", {})
|
||||
self.ac = config.get("accounting", {})
|
||||
self.pm = config.get("payment", {})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _init_session(self, sess: requests.Session, cfg: dict):
|
||||
for k, v in cfg.items():
|
||||
if v:
|
||||
sess.headers[k] = v
|
||||
# 禁用系统代理,直连服务器(避免本地代理修改请求导致 WAF 误判 SQL 注入)
|
||||
sess.trust_env = False
|
||||
sess.proxies = {"http": None, "https": None}
|
||||
|
||||
def set_driver_auth(self, token: str):
|
||||
self.driver.headers["authorization"] = token
|
||||
|
||||
def set_admin_auth(self, token: str):
|
||||
self.admin.headers["authorization"] = token
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _get(self, sess: requests.Session, path: str, params: dict = None) -> dict:
|
||||
resp = sess.get(f"{self.base_url}{path}", params=params, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _post(self, sess: requests.Session, path: str, params: dict = None, data: dict = None) -> dict:
|
||||
resp = sess.post(f"{self.base_url}{path}", params=params, data=data, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _put(self, sess: requests.Session, path: str, params: dict = None, data: dict = None) -> dict:
|
||||
resp = sess.put(f"{self.base_url}{path}", params=params, data=data, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _put_with_query_string(self, sess: requests.Session, path: str,
|
||||
params: dict) -> dict:
|
||||
"""
|
||||
PUT 请求,手工构建 query string。
|
||||
|
||||
与 _put(params=...) 的区别:query string 由 urlencode 一次性编码后
|
||||
拼接为完整 URL 再发给 requests,避免 requests 内部再次编码导致
|
||||
URL 值中已有的 %XX(如 AWS Signature 的 %2B)被二次转义为 %25XX,
|
||||
触发服务端 WAF 的 SQL 注入误判。
|
||||
"""
|
||||
qs = urllib.parse.urlencode(params, doseq=True,
|
||||
quote_via=urllib.parse.quote)
|
||||
full_url = f"{self.base_url}{path}?{qs}"
|
||||
|
||||
# DEBUG: 打印实际发出的 URL(截取 ticketImageUrls 参数附近供对比)
|
||||
_debug_full_url(full_url)
|
||||
|
||||
resp = sess.put(full_url, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _post_with_query_string(self, sess: requests.Session, path: str,
|
||||
params: dict) -> dict:
|
||||
"""
|
||||
POST 请求,手工构建 query string(与 _put_with_query_string 同理)。
|
||||
避免 requests 对逗号、URL 等字符做不必要的编码。
|
||||
"""
|
||||
qs = urllib.parse.urlencode(params, doseq=True,
|
||||
quote_via=urllib.parse.quote)
|
||||
full_url = f"{self.base_url}{path}?{qs}"
|
||||
resp = sess.post(full_url, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# ==================================================================
|
||||
# Step 01: 查询货源
|
||||
# ==================================================================
|
||||
def step01_query_goods(self, page_size: int = 10, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("mainOrderCode", jsonData.data.records[0].code);
|
||||
pm.collectionVariables.set("residueNumber", jsonData.data.records[0].lastWeight);
|
||||
|
||||
Returns: {"mainOrderCode": str, "residueNumber": str}
|
||||
"""
|
||||
params = {
|
||||
"isFromOrderHall": "1",
|
||||
"sortType": "desc",
|
||||
"sortField": "gmt_created",
|
||||
"pageSize": page_size,
|
||||
"pageNum": page_num,
|
||||
}
|
||||
data = self._get(self.driver, "/ntocc-tms-api/order/orderHallList", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
if not records:
|
||||
raise RuntimeError("查询货源: 无记录返回")
|
||||
|
||||
r = records[0]
|
||||
return {
|
||||
"mainOrderCode": r.get("code", ""),
|
||||
"residueNumber": str(r.get("lastWeight", "")),
|
||||
"shipperLatitude": str(r.get("shipperLatitude", "")),
|
||||
"shipperLongitude": str(r.get("shipperLongitude", "")),
|
||||
"consigneeLatitude": str(r.get("consigneeLatitude", "")),
|
||||
"consigneeLongitude": str(r.get("consigneeLongitude", "")),
|
||||
}
|
||||
|
||||
# ==================================================================
|
||||
# Step 02: 接单
|
||||
# ==================================================================
|
||||
def step02_accept_order(self, main_order_code: str, residue_number: str,
|
||||
message_code: str = "",
|
||||
latitude: str = None, longitude: str = None) -> dict:
|
||||
"""
|
||||
Postman Body (urlencoded):
|
||||
residueNumber / perWeight / latitude / messageCode / mainOrderCode / longitude
|
||||
latitude/longitude 优先取货源接口的 shipperLat/shipperLng,没有则用 config
|
||||
"""
|
||||
data = {
|
||||
"residueNumber": residue_number,
|
||||
"perWeight": self.dp.get("per_weight", "1"),
|
||||
"latitude": latitude or self.dp.get("latitude", ""),
|
||||
"messageCode": message_code,
|
||||
"mainOrderCode": main_order_code,
|
||||
"longitude": longitude or self.dp.get("longitude", ""),
|
||||
}
|
||||
return self._post(self.driver, "/ntocc-tms-api/orderDetail/receive", data=data)
|
||||
|
||||
# ==================================================================
|
||||
# Step 03: 查询待运输单
|
||||
# ==================================================================
|
||||
def step03_query_pending_transport(self, page_size: int = 10, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("code", jsonData.data.records[0].code);
|
||||
pm.collectionVariables.set("vehicleCode", jsonData.data.records[0].vehicleCode);
|
||||
pm.collectionVariables.set("orderCode", jsonData.data.records[0].orderCode);
|
||||
|
||||
Returns: {"code": str, "vehicleCode": str, "orderCode": str}
|
||||
"""
|
||||
params = {
|
||||
"assignmentStatus": "waitTrans",
|
||||
"placeOfReceipt": "",
|
||||
"gmtCreatedEnd": "",
|
||||
"gmtCreatedStart": "",
|
||||
"pageSize": page_size,
|
||||
"placeOfShipment": "",
|
||||
"pageNum": page_num,
|
||||
"shipmentName": "",
|
||||
"status": "1",
|
||||
"vehicleLicense": "",
|
||||
}
|
||||
data = self._get(self.driver, "/ntocc-tms-api/orderDetail", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
if not records:
|
||||
raise RuntimeError("查询待运输单: 无记录返回")
|
||||
|
||||
r = records[0]
|
||||
return {
|
||||
"code": r.get("code", ""),
|
||||
"vehicleCode": r.get("vehicleCode", ""),
|
||||
"orderCode": r.get("orderCode", ""),
|
||||
}
|
||||
|
||||
# ==================================================================
|
||||
# Step 04: 装货
|
||||
# ==================================================================
|
||||
def step04_load_goods(self, code: str, vehicle_code: str,
|
||||
img_url: str = "", quantity: str = "",
|
||||
weight: str = None,
|
||||
latitude: str = None, longitude: str = None) -> dict:
|
||||
"""
|
||||
Postman Pre-request Script: 生成 currentTime
|
||||
Body: imgUrl / orderType / quantity / code / latitude / weight / vehicleCode / time / longitude
|
||||
latitude/longitude 优先取货源接口的 shipperLat/shipperLng
|
||||
"""
|
||||
if weight is None:
|
||||
weight = self.dp.get("weight", "1")
|
||||
body = {
|
||||
"imgUrl": img_url or self.dp.get("load_img_url", ""),
|
||||
"orderType": "1",
|
||||
"quantity": quantity,
|
||||
"code": code,
|
||||
"latitude": latitude or self.dp.get("latitude", ""),
|
||||
"weight": weight,
|
||||
"vehicleCode": vehicle_code,
|
||||
"time": _current_time_str(),
|
||||
"longitude": longitude or self.dp.get("longitude", ""),
|
||||
}
|
||||
return self._put(self.driver, "/ntocc-tms-api/orderDetail/fill", data=body)
|
||||
|
||||
# ==================================================================
|
||||
# Step 05: 卸货
|
||||
# ==================================================================
|
||||
def step05_unload_goods(self, code: str, img_url: str = "",
|
||||
quantity: str = "", weight: str = None,
|
||||
latitude: str = None, longitude: str = None) -> dict:
|
||||
"""
|
||||
Postman Pre-request Script: 生成 currentTime
|
||||
Body: imgUrl / code / quantity / latitude / weight / time / longitude
|
||||
latitude/longitude 优先取货源接口的 consigneeLat/consigneeLng
|
||||
"""
|
||||
if weight is None:
|
||||
weight = self.dp.get("weight", "1")
|
||||
body = {
|
||||
"imgUrl": img_url or self.dp.get("unload_img_url", ""),
|
||||
"code": code,
|
||||
"quantity": quantity,
|
||||
"latitude": latitude or self.dp.get("latitude", ""),
|
||||
"weight": weight,
|
||||
"time": _current_time_str(),
|
||||
"longitude": longitude or self.dp.get("longitude", ""),
|
||||
}
|
||||
return self._put(self.driver, "/ntocc-tms-api/orderDetail/uploadTicketImage", data=body)
|
||||
|
||||
# ==================================================================
|
||||
# Step 06: 运费核算查询运单
|
||||
# ==================================================================
|
||||
def step06_query_for_accounting(self, order_code: str,
|
||||
page_size: int = 20, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script: 设置 30+ 个 collection variables
|
||||
从 data.records[0] 中提取所有字段
|
||||
|
||||
Returns: dict with all fields from records[0]
|
||||
"""
|
||||
params = {
|
||||
"status": "3",
|
||||
"lastLoadingTimeRange": "",
|
||||
"fillTimeRange": "",
|
||||
"lastUnloadingTimeRange": "",
|
||||
"signTimeRange": "",
|
||||
"gmtModifiedRange": "",
|
||||
"rangeValue_start_1": "",
|
||||
"rangeValue_end_1": "",
|
||||
"rangeValue_start_2": "",
|
||||
"rangeValue_end_2": "",
|
||||
"rangeValue_start_3": "",
|
||||
"rangeValue_end_3": "",
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"gmtCreatedStart": "",
|
||||
"gmtCreatedEnd": "",
|
||||
"lastUnloadingTimeStart": "",
|
||||
"lastUnloadingTimeEnd": "",
|
||||
"lastLoadingTimeStart": "",
|
||||
"lastLoadingTimeEnd": "",
|
||||
"gmtModifiedStart": "",
|
||||
"gmtModifiedEnd": "",
|
||||
"fillTimeStart": "",
|
||||
"fillTimeEnd": "",
|
||||
"signTimeStart": "",
|
||||
"signTimeEnd": "",
|
||||
"ordCreatedByName": "",
|
||||
"isUnloadingConfirm": "1",
|
||||
"isCannotAccounting": "0",
|
||||
"riskControlStatus": "",
|
||||
"riskControlReason": "",
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"orderCode": order_code,
|
||||
}
|
||||
data = self._get(self.admin, "/ntocc-tms-api/orderDetail/orderDetailHandlePageList", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
if not records:
|
||||
raise RuntimeError("运费核算查询运单: 无记录返回")
|
||||
return records[0]
|
||||
|
||||
# ==================================================================
|
||||
# Step 07: 查询运单详情
|
||||
# ==================================================================
|
||||
def step07_query_order_expenses(self, code: str) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("canWastage", ...);
|
||||
pm.collectionVariables.set("deliveryCashFee", ...);
|
||||
pm.collectionVariables.set("deliveryFeeDeserved", ...);
|
||||
pm.collectionVariables.set("deliveryFeePractical", ...);
|
||||
pm.collectionVariables.set("driverActualCash", ...);
|
||||
pm.collectionVariables.set("driverActualFreight", ...);
|
||||
|
||||
Returns: {canWastage, deliveryCashFee, deliveryFeeDeserved,
|
||||
deliveryFeePractical, driverActualCash, driverActualFreight}
|
||||
"""
|
||||
params = {"codes": code}
|
||||
data = self._post(self.admin,
|
||||
"/ntocc-tms-api/orderDetail/getOrderDetailExpensesParamList",
|
||||
params=params)
|
||||
items = data.get("data", [])
|
||||
if not items:
|
||||
raise RuntimeError("查询运单详情: 无数据返回")
|
||||
r = items[0]
|
||||
return {
|
||||
"canWastage": str(r.get("canWastage", "")),
|
||||
"deliveryCashFee": str(r.get("deliveryCashFee", "")),
|
||||
"deliveryFeeDeserved": str(r.get("deliveryFeeDeserved", "")),
|
||||
"deliveryFeePractical": str(r.get("deliveryFeePractical", "")),
|
||||
"driverActualCash": str(r.get("driverActualCash", "")),
|
||||
"driverActualFreight": str(r.get("driverActualFreight", "")),
|
||||
}
|
||||
|
||||
# ==================================================================
|
||||
# Step 08: 核算试算 (calculateOrderDetail)
|
||||
# ==================================================================
|
||||
def step08_calculate_order_detail(self, code: str) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("ticketImageUrls[]", jsonData.data.ticketImageUrls);
|
||||
|
||||
Returns: dict with ticketImageUrls + 其他 step08 返回的核算数据 (taxPrice 等)
|
||||
"""
|
||||
params = {"code": code, "isFirst": "1"}
|
||||
data = self._put(self.admin, "/ntocc-tms-api/orderDetail/calculateOrderDetail", params=params)
|
||||
inner = data.get("data", {}) or {}
|
||||
ticket_urls = inner.get("ticketImageUrls", "")
|
||||
if isinstance(ticket_urls, list):
|
||||
ticket_urls = ticket_urls[0] if ticket_urls else ""
|
||||
# 返回所有有用字段,供 pipeline 存入 vars
|
||||
result = {"ticketImageUrls": str(ticket_urls)}
|
||||
for key in ("taxPrice", "taxFee", "taxPayFee", "taxPoint",
|
||||
"price", "realPrice", "shipmentActualCash",
|
||||
"shipmentActualFreight"):
|
||||
if key in inner:
|
||||
result[key] = str(inner[key])
|
||||
return result
|
||||
|
||||
# ==================================================================
|
||||
# Step 09: 核算提交
|
||||
# ==================================================================
|
||||
def step09_submit_accounting(self, accounting_vars: dict) -> dict:
|
||||
"""
|
||||
提交核算,参数来自前面步骤累积的所有变量。
|
||||
accounting_vars 应包含 steps 03/06/07/08 的全部字段。
|
||||
优先从 step06 返回值取值,取不到再用 config.yaml 默认值。
|
||||
"""
|
||||
av = accounting_vars
|
||||
# 便捷取值:vars 优先 → config 兜底
|
||||
def _v(key, cfg_key, default=""):
|
||||
return av.get(key) or self.ac.get(cfg_key, default)
|
||||
|
||||
# Build the massive query string
|
||||
params = {
|
||||
"abnormalFee": "0",
|
||||
"accountCheckAuthStatus": "0",
|
||||
"accountCheckInfoAuthStatus": "0",
|
||||
"accountCheckUpAuthStatus": "0",
|
||||
"accountStatus": "0",
|
||||
"allTrans": "0",
|
||||
"allotAmount": "0",
|
||||
"allotOil": "0",
|
||||
"allotTaxAmount": "0",
|
||||
"appSendLoadLocation": "0",
|
||||
"appSendUnloadLocation": "0",
|
||||
"applyStatus": "0",
|
||||
"branchCode": _v("branchCode", "branch_code"),
|
||||
"businessTypeCode": _v("businessTypeCode", "business_type_code"),
|
||||
"canWastage": av.get("canWastage", ""),
|
||||
"cancelTrans": "0",
|
||||
"cancelType": "0",
|
||||
"captainSplitCurrentVersion": "0",
|
||||
"captainSplitMode": self.ac.get("captain_split_mode") or av.get("captainSplitMode", ""),
|
||||
"cashDeposit": "0",
|
||||
"coalName": "",
|
||||
"coalPrice": av.get("coalPrice", ""),
|
||||
"coalType": av.get("coalType", ""),
|
||||
"coalTypeName": av.get("coalTypeName", ""),
|
||||
"coalUnit": "0",
|
||||
"coalUnitName": av.get("coalUnitName", ""),
|
||||
"code": av.get("code", ""),
|
||||
"contractStatus": "1",
|
||||
"createdBy": av.get("createdBy", ""),
|
||||
"createdByName": av.get("createdByName", ""),
|
||||
"deleted": "0",
|
||||
"deliveryCashFee": av.get("deliveryCashFee", ""),
|
||||
"deliveryFeeDeserved": av.get("deliveryFeeDeserved", ""),
|
||||
"deliveryFeePractical": av.get("deliveryFeePractical", ""),
|
||||
"deliveryOilFee": "0",
|
||||
"distance": av.get("distance", ""),
|
||||
"driverActualCash": av.get("driverActualCash", ""),
|
||||
"driverActualFreight": av.get("driverActualFreight", ""),
|
||||
"driverCode": av.get("driverCode", ""),
|
||||
"driverName": av.get("driverName", ""),
|
||||
"driverPhone": av.get("driverPhone", ""),
|
||||
"energyCardType": av.get("energyCardType", ""),
|
||||
"etcType": "0",
|
||||
"extraFee": "0",
|
||||
"fillTime": av.get("fillTime", ""),
|
||||
"finishedTrans": "0",
|
||||
"gmtCreated": av.get("gmtCreated", ""),
|
||||
"gmtModified": av.get("gmtModified", ""),
|
||||
"id": av.get("id", ""),
|
||||
"inFee": "0",
|
||||
"infoFee": "0",
|
||||
"insuranceAmount": "0",
|
||||
"insuranceFee": "0",
|
||||
"invoiceIsAll": "0",
|
||||
"invoiceStatus": "0",
|
||||
"invoiceStatusCaptain": "0",
|
||||
"invoiceStatusDriver": "0",
|
||||
"isAbnormal": "0",
|
||||
"isCancel": "0",
|
||||
"isCaptainOrder": "0",
|
||||
"isCash": "0",
|
||||
"isCuikuan": "0",
|
||||
"isDriverComment": "0",
|
||||
"isEqualDriver": "1",
|
||||
"isEqualTeam": "1",
|
||||
"isEqualVehicle": "1",
|
||||
"isEtcHistory": "0",
|
||||
"isFill": "1",
|
||||
"isImport": "0",
|
||||
"isInsure": "0",
|
||||
"isLoadingConfirm": "1",
|
||||
"isLoadingConfirmDate": av.get("isLoadingConfirmDate", ""),
|
||||
"isOriginalOrder": "1",
|
||||
"isPaperReturns": "0",
|
||||
"isReceiptReturn": "0",
|
||||
"isReportOrder": "0",
|
||||
"isSend": "0",
|
||||
"isSendDishonestyVehicle": "0",
|
||||
"isSendFund": "0",
|
||||
"isSendFundZhongLian": "0",
|
||||
"isSendLoadLocation": "0",
|
||||
"isSendTax": "0",
|
||||
"isSendUnloadLocation": "0",
|
||||
"isSendZhongLian": "0",
|
||||
"isSettle": "0",
|
||||
"isShipmentComment": "0",
|
||||
"isSign": "1",
|
||||
"isSplitOrder": "0",
|
||||
"isSupplementary": "0",
|
||||
"isTransfer": "0",
|
||||
"isTrunkName": av.get("isTrunkName", ""),
|
||||
"isUnloadingComfirmDate": av.get("isUnloadingComfirmDate", ""),
|
||||
"isUnloadingConfirm": "1",
|
||||
"isWsShare": "0",
|
||||
"loadImgUrls[]": av.get("ticketImageUrls", ""),
|
||||
"loadLatitude": av.get("loadLatitude", ""),
|
||||
"loadLongitude": av.get("loadLongitude", ""),
|
||||
"loadPlace": av.get("loadPlace", ""),
|
||||
"loadQuantity": av.get("loadQuantity", ""),
|
||||
"loadWeight": av.get("loadWeight", "1"),
|
||||
"mainOrderCode": av.get("mainOrderCode", ""),
|
||||
"modifiedBy": av.get("modifiedBy", ""),
|
||||
"modifiedByName": av.get("modifiedByName", ""),
|
||||
"monthlySettlementStatus": "0",
|
||||
"oilFee": av.get("oilFee", ""),
|
||||
"onTrans": "0",
|
||||
"orderCode": av.get("orderCode", ""),
|
||||
"payAmount": "0",
|
||||
"payOil": "0",
|
||||
"payStatus": "0",
|
||||
"payStep": "10",
|
||||
"payTaxAmount": "0",
|
||||
"payTotalAmount": av.get("payTotalAmount", ""),
|
||||
"payType": "0",
|
||||
"payeeCode": _v("createdBy", "payee_code"), # step06 createdBy = payeeCode
|
||||
"payeeName": av.get("payeeName", ""),
|
||||
"payerReceiver": self.ac.get("payer_receiver", ""),
|
||||
"payerReceiverLinkMan": self.ac.get("payer_receiver_link_man", ""),
|
||||
"platformPrepayment": "0",
|
||||
"prepaidStatus": "0",
|
||||
"price": av.get("price", ""),
|
||||
"proofAmount": "0",
|
||||
"proofStatus": "0",
|
||||
"realPrice": av.get("realPrice", ""),
|
||||
"receiptNum": "",
|
||||
"receiptPayAmount": "0",
|
||||
"refuelProportion": "0",
|
||||
"reportMark": "0",
|
||||
"reportStage": "0",
|
||||
"settlementCash": "0",
|
||||
"settlementOil": "0",
|
||||
"settlementType": self.ac.get("settlement_type", "2"),
|
||||
"settlementWeight": av.get("settlementWeight", "1"),
|
||||
"shipmentActualCash": av.get("shipmentActualCash", ""),
|
||||
"shipmentActualFreight": av.get("shipmentActualFreight", ""),
|
||||
"shipmentCardScale": "10",
|
||||
"shipmentDeliveryFeePractical": av.get("shipmentDeliveryFeePractical", ""),
|
||||
"shipmentName": av.get("shipmentName", ""),
|
||||
"shipmentPayStatus": "0",
|
||||
"shipmentPhone": _v("shipmentPhone", "shipment_phone"),
|
||||
"shipperDeliveryFee": av.get("shipperDeliveryFee", ""),
|
||||
"shippingNoteNumber": av.get("shippingNoteNumber", ""),
|
||||
"signTime": av.get("signTime", ""),
|
||||
"status": "3",
|
||||
"takeLatitude": av.get("takeLatitude", ""),
|
||||
"takeLongitude": av.get("takeLongitude", ""),
|
||||
"takeOrderType": "2",
|
||||
"takePlace": av.get("takePlace", ""),
|
||||
"taxFee": av.get("taxFee", ""),
|
||||
"taxPayFee": av.get("taxPayFee", ""),
|
||||
"taxPrice": av.get("taxPrice", ""),
|
||||
"ticketImageUrls[]": av.get("ticketImageUrls", ""),
|
||||
"txStatus": "0",
|
||||
"unloadLatitude": self.dp.get("latitude", ""),
|
||||
"unloadLongitude": self.dp.get("longitude", ""),
|
||||
"unloadPlace": av.get("unloadPlace", ""),
|
||||
"unloadQuantity": av.get("unloadQuantity", ""),
|
||||
"unloadWeight": av.get("unloadWeight", "1"),
|
||||
"vehicleCode": av.get("vehicleCode", ""),
|
||||
"vehicleLicense": av.get("vehicleLicense", ""),
|
||||
"waitCar": "0",
|
||||
"waitCheckTrans": "0",
|
||||
"waitDriverCheckTrans": "0",
|
||||
"waitPay": "0",
|
||||
"waitSettle": "0",
|
||||
"waitTrans": "0",
|
||||
"wastage": "0",
|
||||
"weight": av.get("weight", "1"),
|
||||
"whetherOverload": "0",
|
||||
"wsShareRatio": "0",
|
||||
"deductionFee": "0",
|
||||
"driverErase": "0",
|
||||
"driverFixedPay": "0",
|
||||
"driverSettlementWeight": av.get("driverSettlementWeight", "1"),
|
||||
"electricityCardScale": "10",
|
||||
"gasCardScale": "10",
|
||||
"isTrunk": "0",
|
||||
"limitWastageAmount": "500",
|
||||
"limitWastageType": "1",
|
||||
"shipmentErase": "0",
|
||||
"shipmentPartyCode": _v("shipmentPartyCode", "shipment_party_code"),
|
||||
"shipmentSettlementWeight": av.get("shipmentSettlementWeight", "1"),
|
||||
"shipperFixedPay": "0",
|
||||
"taxPoint": av.get("taxPoint", ""),
|
||||
"taxPriceFixedPay": "0",
|
||||
"orderExtraFeeListStr": "[]",
|
||||
"BranchCode": _v("branchCode", "branch_code"),
|
||||
"unloadImgaes": av.get("ticketImageUrls", ""),
|
||||
"truckLoadingImgaes": av.get("ticketImageUrls", ""),
|
||||
"IsTaxPoint": "0",
|
||||
"RealPrice": av.get("realPrice", ""),
|
||||
"gmtCreated2": av.get("gmtCreated", ""),
|
||||
"gmtModified2": av.get("gmtModified", ""),
|
||||
}
|
||||
return self._put_with_query_string(self.admin, "/ntocc-tms-api/orderDetail/orderHandleEditSubmitHandle", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 10: 查询运单审核
|
||||
# ==================================================================
|
||||
def step10_query_audit(self, order_code: str,
|
||||
page_size: int = 20, page_num: int = 1) -> dict:
|
||||
"""
|
||||
GET /ntocc-tms-api/orderDetail (status=6 已结算, isSettle=1)
|
||||
Admin 端查询已结算运单用于审核。
|
||||
"""
|
||||
params = {
|
||||
"isAbnormalHelp": "",
|
||||
"gmtCreatedRange": "",
|
||||
"lastLoadingTimeRange": "",
|
||||
"fillTimeRange": "",
|
||||
"lastUnloadingTimeRange": "",
|
||||
"signTimeRange": "",
|
||||
"settleTimeRange": "",
|
||||
"gmtModifiedRange": "",
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"isSettle": "1",
|
||||
"status": "6",
|
||||
"payerReceiverLinkMan": self.ac.get("payer_receiver_link_man", ""),
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"orderCode": order_code,
|
||||
}
|
||||
return self._get(self.admin, "/ntocc-tms-api/orderDetail", params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 11: 核算审核通过
|
||||
# ==================================================================
|
||||
def step11_approve_audit(self, code: str) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/auditOrderDetail?orderDetailCodes={code}&isPass=true
|
||||
"""
|
||||
params = {"orderDetailCodes": code, "isPass": "true"}
|
||||
return self._post_with_query_string(self.admin, "/ntocc-tms-api/orderDetail/auditOrderDetail", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 12: 队长分账预览 (captainSplitAdjustPreview)
|
||||
# ==================================================================
|
||||
def step12_captain_split_preview(self, order_detail_code: str) -> dict:
|
||||
"""
|
||||
GET /ntocc-tms-api/orderDetail/captainSplitAdjustPreview/{code}
|
||||
仅在 payerReceiver 不为空且 captainSplitMode=2 时调用。
|
||||
"""
|
||||
path = f"/ntocc-tms-api/orderDetail/captainSplitAdjustPreview/{order_detail_code}"
|
||||
return self._get(self.admin, path)
|
||||
|
||||
# Step 13: 队长分账调整 (adjustCaptainSplit)
|
||||
# ==================================================================
|
||||
def step13_adjust_captain_split(self, order_detail_code: str,
|
||||
latest_driver_amount: float,
|
||||
delivery_oil_fee: float) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/adjustCaptainSplit
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
driverAmount = ceil(latestDriverAmount * 0.8 * 100) / 100
|
||||
captainAmount = latestDriverAmount - driverAmount
|
||||
oilFee = deliveryOilFee
|
||||
"""
|
||||
driver_amount = math.ceil(latest_driver_amount * 0.8 * 100) / 100.0
|
||||
captain_amount = round(latest_driver_amount - driver_amount, 2)
|
||||
body = {
|
||||
"orderDetailCode": order_detail_code,
|
||||
"driverAmount": str(driver_amount),
|
||||
"captainAmount": str(captain_amount),
|
||||
"oilFee": delivery_oil_fee,
|
||||
}
|
||||
resp = self.admin.post(
|
||||
f"{self.base_url}/ntocc-tms-api/orderDetail/adjustCaptainSplit",
|
||||
json=body,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
# Step 14: 单油卡充值 (singleOil)
|
||||
# ==================================================================
|
||||
def step14_single_oil(self, order_detail_code: str, amount: str = "0.03",
|
||||
pay_type: str = "1", category: str = "1") -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/finance/singleOil
|
||||
Query params: payType, amount, orderDetailCode, category, isAdvancePayment
|
||||
No body (content-length: 0)
|
||||
|
||||
在垫资-财务打款之前调用,处理油卡充值。
|
||||
"""
|
||||
params = {
|
||||
"payType": pay_type,
|
||||
"amount": amount,
|
||||
"orderDetailCode": order_detail_code,
|
||||
"category": category,
|
||||
"isAdvancePayment": "1",
|
||||
}
|
||||
return self._post_with_query_string(
|
||||
self.admin,
|
||||
"/ntocc-tms-api/finance/singleOil",
|
||||
params=params,
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# Step 15: 垫资-财务打款
|
||||
# ==================================================================
|
||||
def step15_pay_to_driver(self, code: str, amount: str = None,
|
||||
delivery_cash_fee: str = None,
|
||||
driver_actual_freight: str = None,
|
||||
plan_driver_payout: str = "0",
|
||||
plan_captain_payout: str = "0") -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/arpaTransfer/payToDriver
|
||||
amount / deliveryCashFee / driverActualFreight 由 pipeline 从 step07 传入
|
||||
planDriverPayout / planCaptainPayout 来自队长分账步骤,无分账时为 "0"
|
||||
"""
|
||||
if amount is None:
|
||||
amount = "0.27"
|
||||
if delivery_cash_fee is None:
|
||||
delivery_cash_fee = "0.27"
|
||||
if driver_actual_freight is None:
|
||||
driver_actual_freight = "0.3"
|
||||
params = {
|
||||
"payTypes": self.pm.get("pay_types", "3,4,5,9,10,11"),
|
||||
"amount": amount,
|
||||
"orderDetailCode": code,
|
||||
"infoFee": "0",
|
||||
"pageNum": "1",
|
||||
"pageSize": "20",
|
||||
"totaltotal": "0",
|
||||
"payerRole": "0",
|
||||
"isAdvancePayment": "1",
|
||||
"planDriverPayout": plan_driver_payout,
|
||||
"planCaptainPayout": plan_captain_payout,
|
||||
"allotAmount": "0",
|
||||
"deliveryCashFee": delivery_cash_fee,
|
||||
"driverActualFreight": driver_actual_freight,
|
||||
"total": "0",
|
||||
"payPlatform": self.pm.get("pay_platform", "3"),
|
||||
"smsCode": self.pm.get("sms_code", "1234"),
|
||||
"phone": self.pm.get("phone", "13888888888"),
|
||||
}
|
||||
return self._post_with_query_string(self.admin, "/ntocc-tms-api/arpaTransfer/payToDriver", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 16: 垫资-货主打款 (申请付款)
|
||||
# ==================================================================
|
||||
def step16_apply_for_payment(self, code: str, total_amount: str = None) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/applicationForPayment
|
||||
totalAmount 应从 pipeline 传入 (step06 的 shipmentActualCash)
|
||||
"""
|
||||
if total_amount is None:
|
||||
total_amount = "0.32"
|
||||
params = {
|
||||
"payType": self.pm.get("pay_type", "arpa_2"),
|
||||
"totalAmount": total_amount,
|
||||
"codes": code,
|
||||
"payPlatform": self.pm.get("pay_platform", "3"),
|
||||
"isAdvancePayment": "1",
|
||||
}
|
||||
return self._post_with_query_string(self.admin, "/ntocc-tms-api/orderDetail/applicationForPayment", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 17: 查询运单上传 → 提取 payNo
|
||||
# ==================================================================
|
||||
def step17_query_upload_for_payno(self, order_code: str,
|
||||
page_size: int = 20, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
var sendMsg = jsonData.data.records[0].sendMsg || "";
|
||||
var match = sendMsg.match(/【([^】]+)】/);
|
||||
var result = match ? match[1] : "";
|
||||
pm.collectionVariables.set("payNo", result);
|
||||
|
||||
Returns: {"payNo": str, "raw_records": list}
|
||||
"""
|
||||
params = {
|
||||
"gmtCreatedRange[]": ["", ""],
|
||||
"lastLoadingTimeRange": "",
|
||||
"fillTimeRange": "",
|
||||
"lastUnloadingTimeRange": "",
|
||||
"signTimeRange": "",
|
||||
"settleTimeRange": "",
|
||||
"gmtModifiedRange": "",
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"status": "5", # <-- 注意:status=5
|
||||
"payerReceiverLinkMan": self.ac.get("payer_receiver_link_man", ""),
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"gmtCreatedStart": "",
|
||||
"gmtCreatedEnd": "",
|
||||
"orderCode": order_code,
|
||||
}
|
||||
data = self._get(self.admin, "/ntocc-basic-api/dytBusiness/orderDetail", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
pay_no = ""
|
||||
if records:
|
||||
send_msg = records[0].get("sendMsg", "")
|
||||
match = re.search(r"【([^】]+)】", send_msg)
|
||||
if match:
|
||||
pay_no = match.group(1)
|
||||
|
||||
return {"payNo": pay_no, "raw_records": records}
|
||||
|
||||
# ==================================================================
|
||||
# Step 18: 查询打款记录 → 提取所有 code
|
||||
# ==================================================================
|
||||
def step18_query_remit_records(self, order_code: str,
|
||||
page_size: int = 20) -> list[dict]:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("remitCode", jsonData.data.records[0].code);
|
||||
|
||||
分页获取全部打款记录。Returns: [{code, ...}, ...]
|
||||
"""
|
||||
all_records = []
|
||||
page_num = 1
|
||||
|
||||
while True:
|
||||
params = {
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"pageForm": "arpa_2",
|
||||
"orderDetailNumber": order_code,
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"isPayAdvance": "",
|
||||
"platPayType": "",
|
||||
"gmtModifiedStart": "",
|
||||
"gmtModifiedEnd": "",
|
||||
}
|
||||
data = self._get(self.admin, "/ntocc-tms-api/remitDefinite", params)
|
||||
# code 可能是 0 (成功) 或 None/null (也是成功)
|
||||
resp_code = data.get("code")
|
||||
if resp_code is not None and resp_code != 0:
|
||||
raise RuntimeError(f"查询打款记录失败: {data.get('msg')}")
|
||||
|
||||
page_data = data.get("data", {})
|
||||
records = page_data.get("records", [])
|
||||
all_records.extend(records)
|
||||
|
||||
total = page_data.get("total", 0)
|
||||
if page_num * page_size >= total:
|
||||
break
|
||||
page_num += 1
|
||||
|
||||
return all_records
|
||||
|
||||
# ==================================================================
|
||||
# Step 19: 查询文件名前缀 → 提取 filePrefix
|
||||
# ==================================================================
|
||||
def step19_query_file_prefix(self, remit_code: str) -> str:
|
||||
"""
|
||||
Postman Test Script:
|
||||
const msg = jsonData.msg || '';
|
||||
const prefix = '文件名前缀:';
|
||||
const index = msg.indexOf(prefix);
|
||||
const filePrefix = index !== -1 ? msg.substring(index + prefix.length).trim() : '';
|
||||
pm.collectionVariables.set('filePrefix', filePrefix);
|
||||
|
||||
Returns: filePrefix string
|
||||
"""
|
||||
params = {"code": remit_code}
|
||||
data = self._get(self.admin,
|
||||
"/ntocc-tms-api/centralizedPayment/downLoadElectronicReceipt",
|
||||
params)
|
||||
msg = data.get("msg", "")
|
||||
prefix_marker = "文件名前缀:"
|
||||
idx = msg.find(prefix_marker)
|
||||
if idx != -1:
|
||||
return msg[idx + len(prefix_marker):].strip()
|
||||
return ""
|
||||
|
||||
# ==================================================================
|
||||
# Step 20: 上报运单 (reportOrderDetail)
|
||||
# ==================================================================
|
||||
def step20_report_order_detail(self, codes: str, report_type: int = 1) -> dict:
|
||||
"""
|
||||
POST /ntocc-basic-api/dytBusiness/reportOrderDetail
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
Body: {"codes": "...", "type": 1}
|
||||
|
||||
在打款完成后上报运单,完成整个流程闭环。
|
||||
"""
|
||||
body = {"codes": codes, "type": report_type}
|
||||
resp = self.admin.post(
|
||||
f"{self.base_url}/ntocc-basic-api/dytBusiness/reportOrderDetail",
|
||||
json=body,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# ==================================================================
|
||||
# Step 21: 滇运通审核 (updateOrderDetailExt)
|
||||
# ==================================================================
|
||||
def step21_owner_review(self, order_detail_code: str) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/updateOrderDetailExt
|
||||
滇运通独立接口,独立 token / host。
|
||||
仅在 config.dianyuntong.enabled=true 时执行。
|
||||
"""
|
||||
if not self.tp_enabled or not self.tp:
|
||||
raise RuntimeError("dianyuntong 未启用,请检查 config.yaml")
|
||||
params = {
|
||||
"orderDetailCode": order_detail_code,
|
||||
"reviewStatus": "1",
|
||||
"reviewMsg": "",
|
||||
"invoiceApplicationStatus": "0",
|
||||
"invoiceApplicationMsg": "",
|
||||
}
|
||||
resp = self.tp.post(
|
||||
f"{self.tp_base}/ntocc-tms-api/orderDetail/updateOrderDetailExt",
|
||||
params=params,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# ==================================================================
|
||||
Reference in New Issue
Block a user