Files
Yb-QaAutomationHub/yun8_sync/client.py
T
xst 73b93bdf0c 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).
2026-08-17 14:30:40 +08:00

966 lines
41 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.
"""
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()
# ==================================================================