refactor(api): yun8_sync 并入 tests 分层框架(接口层 apis/yun8 + 用例层 api + Allure)
- tests/core: 新增 BaseHttpClient 统一 HTTP 封装(直连绕行/超时/日志/自定义编码); db_helper 追加 Db 长连接类(事务/mogrify/自动重连),既有函数不动 - tests/apis/yun8: 21 个 step 方法按域拆分为 5 个 Mixin(driver/accounting/ audit_payment/receipt/third_party),Yun8Pipeline 24 步编排原样迁移, SQL 全部参数化并集中至 sql_templates - tests/api: 5 阶段有序 P0 E2E 用例(业务失败由 print 改为真实报错)+ README 迁移(删除明文密码段,修正步骤编号漂移) - yun8_sync 目录退役:配置并入 test_config.yaml 的 yun8_api 段; 造数 CLI 保留(python -m tests.apis.yun8.cli,兼容旧 config.yaml 结构) - pytest.ini 注册 api/yun8_sync marker;requirements.txt 补 requests>=2.28 - 静态验证: compileall / 全仓 collect 24 条 / marker 过滤 / CLI --help / 客户端与 pipeline 离线实例化 / SQL 占位符核对 / cross_end 兼容
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""yun8 运输流程 API 客户端包。
|
||||
|
||||
分层:
|
||||
client.py Yun8ApiClient 门面(3 套会话组装 + 鉴权)
|
||||
driver_api.py 司机端 step01-05
|
||||
accounting_api.py 运费核算 step06-09
|
||||
audit_payment_api.py 审核 & 付款 step10-16
|
||||
receipt_api.py 回单/上报 step17-20
|
||||
third_party_api.py 滇运通 step21
|
||||
sql_templates.py 全部 SQL 语句与参数化构造器
|
||||
pipeline.py Yun8Pipeline 24 步编排
|
||||
cli.py 造数 CLI 入口
|
||||
"""
|
||||
@@ -0,0 +1,337 @@
|
||||
"""运费核算接口 Mixin — step06-09(管理端,含 ~190 字段核算提交报文)。
|
||||
|
||||
所有方法签名/返回值/默认值与旧 yun8_sync/client.py 逐字一致,
|
||||
仅将原始 requests.Session 调用替换为 BaseHttpClient(self.admin_http)。
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.accounting_api import AccountingApi
|
||||
class Yun8ApiClient(AccountingApi, ...): ...
|
||||
"""
|
||||
|
||||
|
||||
class AccountingApi:
|
||||
"""运费核算接口(挂载于 Yun8ApiClient,依赖 admin_http / ac / dp 属性)。"""
|
||||
|
||||
# ==================================================================
|
||||
# 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.admin_http.get("/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.admin_http.post(
|
||||
"/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.admin_http.put("/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.admin_http.request_with_encoded_query(
|
||||
"PUT", "/ntocc-tms-api/orderDetail/orderHandleEditSubmitHandle", params=params)
|
||||
@@ -0,0 +1,173 @@
|
||||
"""审核 & 付款接口 Mixin — step10-16(管理端)。
|
||||
|
||||
所有方法签名/返回值/默认值与旧 yun8_sync/client.py 逐字一致,
|
||||
仅将原始 requests.Session 调用替换为 BaseHttpClient(self.admin_http)。
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.audit_payment_api import AuditPaymentApi
|
||||
class Yun8ApiClient(AuditPaymentApi, ...): ...
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class AuditPaymentApi:
|
||||
"""审核 & 付款接口(挂载于 Yun8ApiClient,依赖 admin_http / ac / pm 属性)。"""
|
||||
|
||||
# ==================================================================
|
||||
# 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.admin_http.get("/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.admin_http.request_with_encoded_query(
|
||||
"POST", "/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.admin_http.get(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,
|
||||
}
|
||||
return self.admin_http.post_json(
|
||||
"/ntocc-tms-api/orderDetail/adjustCaptainSplit", body)
|
||||
|
||||
# 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.admin_http.request_with_encoded_query(
|
||||
"POST", "/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.admin_http.request_with_encoded_query(
|
||||
"POST", "/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.admin_http.request_with_encoded_query(
|
||||
"POST", "/ntocc-tms-api/orderDetail/applicationForPayment", params=params)
|
||||
@@ -0,0 +1,204 @@
|
||||
"""yun8 运输流程造数 CLI。
|
||||
|
||||
旧 yun8_sync/main.py 的等价入口,复用 tests/apis/yun8 的 API 层与编排层:
|
||||
python -m tests.apis.yun8.cli --stage all --repeat 10 --delay 5
|
||||
|
||||
注意:
|
||||
- --dry-run 仅跳过回单/ETC 的 SQL 写入;HTTP 步骤全部真实执行,
|
||||
driver 阶段的 reset SQL 也不受 dry-run 影响(与旧工具行为一致)。
|
||||
- 数据库连接读取 tests/config/test_config.yaml 的 database 段。
|
||||
|
||||
阶段选择:
|
||||
all 完整流程 (默认): 司机端 → 核算 → 付款 → 回单SQL → 上报
|
||||
driver 仅司机端 (步骤 1-5)
|
||||
accounting 仅运费核算 (步骤 6-9)
|
||||
payment 仅审核付款 (步骤 10-16)
|
||||
receipt 仅回单文件+SQL+上报 (步骤 14-17)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from tests.core.config_loader import load_config
|
||||
from tests.core.db_helper import Db
|
||||
|
||||
from tests.apis.yun8.pipeline import Yun8Pipeline
|
||||
|
||||
STAGE_HELP = """阶段选择:
|
||||
all 完整流程 (默认): 司机端 → 核算 → 付款 → 回单SQL → 上报
|
||||
driver 仅司机端 (步骤 1-5)
|
||||
accounting 仅运费核算 (步骤 6-9)
|
||||
payment 仅审核付款 (步骤 10-16)
|
||||
receipt 仅回单文件+SQL+上报 (步骤 14-17)
|
||||
"""
|
||||
|
||||
|
||||
def _load_yun8_section(path: str | Path | None) -> dict:
|
||||
"""加载 yun8_api 配置段。
|
||||
|
||||
- path 为空: 读 tests/config/test_config.yaml 的 yun8_api 段
|
||||
- path 指向的 YAML 含 yun8_api 键: 直接取该段
|
||||
- 否则按旧 yun8_sync/config.yaml 结构兼容解析(database 段忽略)
|
||||
"""
|
||||
if path is None:
|
||||
return load_config()["yun8_api"]
|
||||
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
raw = yaml.safe_load(fh)
|
||||
|
||||
if "yun8_api" in raw:
|
||||
return raw["yun8_api"]
|
||||
|
||||
# 旧版结构兼容: api/auth/driver_params/... 平铺
|
||||
api = raw.get("api", {})
|
||||
section = {
|
||||
"base_url": api.get("base_url", ""),
|
||||
"verify_ssl": api.get("verify_ssl", False),
|
||||
"timeout_seconds": api.get("timeout_seconds", 60),
|
||||
"common_headers": api.get("common_headers", {}),
|
||||
"driver_headers": api.get("driver", {}),
|
||||
"admin_headers": api.get("admin", {}),
|
||||
"tokens": dict(raw.get("auth", {})),
|
||||
}
|
||||
for key in ("driver_params", "accounting", "payment", "etc_invoice",
|
||||
"sql", "dianyuntong"):
|
||||
if key in raw:
|
||||
section[key] = raw[key]
|
||||
return section
|
||||
|
||||
|
||||
def run_one_cycle(section: dict, stage: str, driver_token: str,
|
||||
admin_token: str, dry_run: bool) -> bool:
|
||||
"""执行一次完整流水线,返回 True 表示成功(异常直接向上抛)。"""
|
||||
db = Db()
|
||||
pipe = Yun8Pipeline(section, db)
|
||||
try:
|
||||
pipe.set_auth(driver_token, admin_token)
|
||||
|
||||
if stage in ("all", "driver"):
|
||||
pipe.run_driver_flow()
|
||||
|
||||
# reset_order_detail 模式:仅跑司机端,跳过后续全部步骤
|
||||
dp = section.get("driver_params", {})
|
||||
if dp.get("reset_order_detail", False):
|
||||
print(f"\n [stop] reset_order_detail=true,跳过步骤 06-21")
|
||||
return True
|
||||
|
||||
if stage in ("all", "accounting"):
|
||||
pipe.run_accounting_flow()
|
||||
|
||||
if stage in ("all", "payment"):
|
||||
pipe.run_audit_payment_flow()
|
||||
|
||||
ac = section.get("accounting", {})
|
||||
if ac.get("execute_after_audit", True):
|
||||
if stage in ("all", "receipt"):
|
||||
pipe.run_receipt_file_flow(dry_run=dry_run)
|
||||
# ETC 开票 SQL 写入 (步骤 22-23)
|
||||
pipe.run_etc_invoice_flow(dry_run=dry_run)
|
||||
else:
|
||||
if stage in ("all", "receipt"):
|
||||
print(f"\n [stop] execute_after_audit=false,跳过 receipt 阶段")
|
||||
|
||||
return True
|
||||
finally:
|
||||
pipe.close()
|
||||
db.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="yun8 运输流程完整自动化",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=STAGE_HELP,
|
||||
)
|
||||
parser.add_argument("--config", default=None,
|
||||
help="配置文件路径 (默认: tests/config/test_config.yaml 的 yun8_api 段)")
|
||||
parser.add_argument("--auth-driver", default="",
|
||||
help="司机端 Bearer Token (含 'Bearer ' 前缀)")
|
||||
parser.add_argument("--auth-admin", default="",
|
||||
help="管理端 Bearer Token (含 'Bearer ' 前缀)")
|
||||
parser.add_argument("--stage", default="all",
|
||||
choices=["all", "driver", "accounting", "payment", "receipt"],
|
||||
help="运行阶段 (默认: all)")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="仅预览 SQL,不实际写入数据库 (注意: HTTP 步骤仍真实执行)")
|
||||
parser.add_argument("--repeat", type=int, default=1,
|
||||
help="循环执行次数 (默认: 1)")
|
||||
parser.add_argument("--delay", type=int, default=0,
|
||||
help="每次循环之间的等待秒数 (默认: 0)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 加载配置
|
||||
section = _load_yun8_section(args.config)
|
||||
|
||||
# 读取 Token(命令行优先)
|
||||
tokens = section.get("tokens", {})
|
||||
driver_token = args.auth_driver or tokens.get("driver_token", "")
|
||||
admin_token = args.auth_admin or tokens.get("admin_token", "")
|
||||
|
||||
print("=" * 60)
|
||||
print(f" yun8 运输流程自动化")
|
||||
print(f" config: {args.config or 'tests/config/test_config.yaml (yun8_api)'}")
|
||||
print(f" stage: {args.stage}")
|
||||
print(f" dry-run: {args.dry_run}")
|
||||
print(f" repeat: {args.repeat}")
|
||||
print(f" delay: {args.delay}s")
|
||||
print(f" 开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 60)
|
||||
|
||||
ok_count = 0
|
||||
fail_count = 0
|
||||
cycle_start = time.time()
|
||||
|
||||
for i in range(1, args.repeat + 1):
|
||||
if args.repeat > 1:
|
||||
print(f"\n{'#' * 60}")
|
||||
print(f"# 第 {i}/{args.repeat} 次循环")
|
||||
print(f"# 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"{'#' * 60}")
|
||||
|
||||
try:
|
||||
run_one_cycle(section, args.stage, driver_token,
|
||||
admin_token, args.dry_run)
|
||||
ok_count += 1
|
||||
if args.repeat > 1:
|
||||
print(f"\n--- 第 {i}/{args.repeat} 次循环: 成功 ---")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n 用户中断,已执行 {i-1} 次")
|
||||
fail_count += (args.repeat - i + 1)
|
||||
break
|
||||
except Exception as exc:
|
||||
fail_count += 1
|
||||
print(f"\n--- 第 {i}/{args.repeat} 次循环: 失败 ---")
|
||||
print(f" 错误: {exc}")
|
||||
traceback.print_exc()
|
||||
# 失败后继续下一次(不中断循环)
|
||||
|
||||
# 循环间隔
|
||||
if i < args.repeat and args.delay > 0:
|
||||
print(f" 等待 {args.delay}s 后进入下一次循环...")
|
||||
time.sleep(args.delay)
|
||||
|
||||
# 汇总
|
||||
elapsed = time.time() - cycle_start
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" 全部循环完成")
|
||||
print(f" 成功: {ok_count} 失败: {fail_count} 总计: {args.repeat}")
|
||||
print(f" 耗时: {elapsed:.0f}s")
|
||||
print(f" 结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
if fail_count > 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,95 @@
|
||||
"""yun8 API 客户端门面。
|
||||
|
||||
组装司机端 / 管理端 / 第三方(滇运通)三套 BaseHttpClient 会话,
|
||||
并挂载各业务域 Mixin 的 step01-21 方法。
|
||||
|
||||
请求头组装顺序与旧 yun8_sync/client.py._init_session 逐字一致:
|
||||
1. 角色专属请求头(跳过空值)
|
||||
2. common_headers 中的 host / accept-language / accept-encoding 无条件覆盖
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.client import Yun8ApiClient
|
||||
|
||||
client = Yun8ApiClient(config["yun8_api"])
|
||||
client.set_driver_auth("Bearer xxx")
|
||||
client.set_admin_auth("Bearer yyy")
|
||||
data = client.step01_query_goods()
|
||||
"""
|
||||
|
||||
from tests.core.http_client import BaseHttpClient
|
||||
|
||||
from tests.apis.yun8.driver_api import DriverApi
|
||||
from tests.apis.yun8.accounting_api import AccountingApi
|
||||
from tests.apis.yun8.audit_payment_api import AuditPaymentApi
|
||||
from tests.apis.yun8.receipt_api import ReceiptApi
|
||||
from tests.apis.yun8.third_party_api import ThirdPartyApi
|
||||
|
||||
|
||||
class Yun8ApiClient(DriverApi, AccountingApi, AuditPaymentApi, ReceiptApi,
|
||||
ThirdPartyApi):
|
||||
"""封装 yun8 全部 API 请求(21 个 step 方法 + 3 套会话)。"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
# config 为 test_config.yaml 的 yun8_api section
|
||||
self.base_url = config["base_url"].rstrip("/")
|
||||
self.verify_ssl = config.get("verify_ssl", False)
|
||||
self.timeout = config.get("timeout_seconds", 60)
|
||||
common = config.get("common_headers", {})
|
||||
|
||||
# ---- Driver Session ----
|
||||
self.driver_http = BaseHttpClient(
|
||||
self.base_url, headers=config.get("driver_headers", {}),
|
||||
verify_ssl=self.verify_ssl, timeout=self.timeout, name="yun8-driver")
|
||||
|
||||
# ---- Admin Session ----
|
||||
self.admin_http = BaseHttpClient(
|
||||
self.base_url, headers=config.get("admin_headers", {}),
|
||||
verify_ssl=self.verify_ssl, timeout=self.timeout, name="yun8-admin")
|
||||
|
||||
# 通用请求头无条件覆盖(与旧版一致,即使为空也写入)
|
||||
for http in (self.driver_http, self.admin_http):
|
||||
http.set_header("host", common.get("host", ""))
|
||||
http.set_header("accept-language", common.get("accept-language", ""))
|
||||
http.set_header("accept-encoding", common.get("accept-encoding", ""))
|
||||
|
||||
# ---- Third-party Session (滇运通,独立 token/host) ----
|
||||
tp = config.get("dianyuntong", {})
|
||||
self.tp_enabled = tp.get("enabled", False)
|
||||
self.tp_http = None
|
||||
self.tp_base = ""
|
||||
if self.tp_enabled:
|
||||
self.tp_base = tp.get("base_url", "").rstrip("/")
|
||||
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", ""),
|
||||
}
|
||||
self.tp_http = BaseHttpClient(
|
||||
self.tp_base, headers=tp_headers,
|
||||
verify_ssl=self.verify_ssl, timeout=self.timeout, name="yun8-tp")
|
||||
|
||||
# 快捷引用(各域 Mixin 使用)
|
||||
self.dp = config.get("driver_params", {})
|
||||
self.ac = config.get("accounting", {})
|
||||
self.pm = config.get("payment", {})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def set_driver_auth(self, token: str):
|
||||
self.driver_http.set_header("authorization", token)
|
||||
|
||||
def set_admin_auth(self, token: str):
|
||||
self.admin_http.set_header("authorization", token)
|
||||
|
||||
def close(self):
|
||||
"""关闭全部会话连接池。"""
|
||||
self.driver_http.close()
|
||||
self.admin_http.close()
|
||||
if self.tp_http:
|
||||
self.tp_http.close()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""司机端接口 Mixin — step01-05(对应 Postman Collection 司机端 5 个接口)。
|
||||
|
||||
所有方法签名/返回值/默认值与旧 yun8_sync/client.py 逐字一致,
|
||||
仅将原始 requests.Session 调用替换为 BaseHttpClient(self.driver_http)。
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.driver_api import DriverApi
|
||||
class Yun8ApiClient(DriverApi, ...): ...
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from tests.core.http_client import BaseHttpClient
|
||||
|
||||
|
||||
def _current_time_str() -> str:
|
||||
"""Postman pre-request: 生成 'YYYY-MM-DD HH:MM:SS' 格式的当前时间"""
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
class DriverApi:
|
||||
"""司机端接口(挂载于 Yun8ApiClient,依赖 driver_http / dp 属性)。"""
|
||||
|
||||
# ==================================================================
|
||||
# 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.driver_http.get("/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.driver_http.post("/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.driver_http.get("/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.driver_http.put("/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.driver_http.put("/ntocc-tms-api/orderDetail/uploadTicketImage", data=body)
|
||||
@@ -0,0 +1,917 @@
|
||||
"""yun8 运输流程编排器。
|
||||
|
||||
将 Postman Collection 中运输流程 18 个接口 + SQL 写入串联为完整流水线,
|
||||
变量传递模拟 Postman 的 Collection Variables 机制(步骤 N 的输出写入
|
||||
self.vars,供后续步骤读取)。
|
||||
|
||||
与旧 yun8_sync/pipeline.py 的差异(均为迁移计划内的有意行为变化):
|
||||
- _check_response 业务 code != 0 时抛 Yun8StepError(旧版仅 print)
|
||||
- step17 未提取到 payNo 时抛 Yun8StepError(旧版警告后继续,写库 front_seq_no 为空)
|
||||
- step20 上报运单 / step21 滇运通审核失败不再吞异常,向上传播
|
||||
- SQL 全部参数化,reset 序列包进事务,连接复用
|
||||
- print → 统一 logger(output/logs/ 文件 + 控制台)
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.pipeline import Yun8Pipeline
|
||||
pipe = Yun8Pipeline(config["yun8_api"], db)
|
||||
pipe.set_auth(driver_token, admin_token)
|
||||
pipe.run_driver_flow() # 步骤 1-5
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from tests.core.db_helper import Db
|
||||
from tests.core.logger import get_logger
|
||||
|
||||
from tests.apis.yun8.client import Yun8ApiClient
|
||||
from tests.apis.yun8.sql_templates import (
|
||||
RECEIPT_FILE_INSERT_SQL,
|
||||
ETC_INVOICE_INSERT_SQL,
|
||||
ETC_PUSH_INSERT_SQL,
|
||||
etc_invoice_insert_params,
|
||||
etc_status_update,
|
||||
post_unload_reset_update,
|
||||
receipt_file_insert_params,
|
||||
driver_reset_statements,
|
||||
risk_control_update,
|
||||
)
|
||||
|
||||
|
||||
class Yun8StepError(RuntimeError):
|
||||
"""yun8 流程步骤失败(业务 code != 0 或关键变量提取失败)。"""
|
||||
|
||||
def __init__(self, step_name: str, code=None, msg: str = ""):
|
||||
self.step_name = step_name
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
detail = f"{step_name} 失败"
|
||||
if code is not None:
|
||||
detail += f" (code={code}, msg={msg})"
|
||||
elif msg:
|
||||
detail += f" ({msg})"
|
||||
super().__init__(detail)
|
||||
|
||||
|
||||
def _gen_code() -> str:
|
||||
"""按现有数据规则生成 code: 32 位小写十六进制 (UUID4 hex 去横线)"""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
# ETC 发票路段链(广东→河北,按行驶方向依次经过;每条发票取相邻两站作为进/出站)
|
||||
_ETC_STATION_CHAIN = [
|
||||
"广东古镇东站", "湖南郴州西站", "湖南岳阳东站",
|
||||
"湖北孝感东站", "河南信阳站", "河北成安西站",
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Yun8Pipeline — 变量上下文
|
||||
# ============================================================
|
||||
class Yun8Pipeline:
|
||||
"""
|
||||
模拟 Postman Collection Variables 的变量上下文。
|
||||
每个步骤的输出写入 self.vars,后续步骤从 self.vars 读取。
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict, db: Db):
|
||||
# config 为 test_config.yaml 的 yun8_api section
|
||||
self.config = config
|
||||
self.client = Yun8ApiClient(config)
|
||||
self.db = db
|
||||
self.log = get_logger("apis.yun8.pipeline")
|
||||
# 所有步骤间共享的变量池(对应 Postman collectionVariables)
|
||||
self.vars: dict = {}
|
||||
# 回单 file_name 格式模板(旧 config.sql.file_name_template)
|
||||
self.file_name_template = config.get("sql", {}).get(
|
||||
"file_name_template", "{file_prefix}_1_signed.pdf")
|
||||
|
||||
# 动态 weight 模式:每次循环随机生成 1~10 之间的小数,per_weight 和 weight 保持一致
|
||||
dp = config.get("driver_params", {})
|
||||
if dp.get("weight_mode", "fixed") == "random":
|
||||
val = str(round(random.uniform(1.0, 10.0), 1))
|
||||
self.client.dp["per_weight"] = val
|
||||
self.client.dp["weight"] = val
|
||||
self.log.info(f" [random] weight = {val}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 辅助方法
|
||||
# ------------------------------------------------------------------
|
||||
def _set_vars(self, **kwargs):
|
||||
"""批量写入变量"""
|
||||
self.vars.update(kwargs)
|
||||
|
||||
def _get(self, key: str, default: str = "") -> str:
|
||||
"""读取变量"""
|
||||
return self.vars.get(key, default)
|
||||
|
||||
def _log_step(self, num: int, name: str):
|
||||
self.log.info(f"\n{'─' * 50}")
|
||||
self.log.info(f"[Step {num:02d}] {name}")
|
||||
self.log.info(f"{'─' * 50}")
|
||||
|
||||
def _check_response(self, data: dict, step_name: str):
|
||||
"""检查 API 返回 code 是否为 0,非 0 视为步骤失败并中断流程。"""
|
||||
code = data.get("code")
|
||||
msg = data.get("msg", "")
|
||||
if code is not None and code != 0:
|
||||
raise Yun8StepError(step_name, code=code, msg=msg)
|
||||
|
||||
def _load_lat(self) -> str:
|
||||
"""装货纬度: config.load_latitude → 货源 shipperLatitude → config.latitude"""
|
||||
dp = self.config.get("driver_params", {})
|
||||
return dp.get("load_latitude") or self._get("shipperLatitude") or dp.get("latitude", "")
|
||||
|
||||
def _load_lng(self) -> str:
|
||||
"""装货经度: config.load_longitude → 货源 shipperLongitude → config.longitude"""
|
||||
dp = self.config.get("driver_params", {})
|
||||
return dp.get("load_longitude") or self._get("shipperLongitude") or dp.get("longitude", "")
|
||||
|
||||
def _unload_lat(self) -> str:
|
||||
"""卸货纬度: config.unload_latitude → 货源 consigneeLatitude → config.latitude"""
|
||||
dp = self.config.get("driver_params", {})
|
||||
return dp.get("unload_latitude") or self._get("consigneeLatitude") or dp.get("latitude", "")
|
||||
|
||||
def _unload_lng(self) -> str:
|
||||
"""卸货经度: config.unload_longitude → 货源 consigneeLongitude → config.longitude"""
|
||||
dp = self.config.get("driver_params", {})
|
||||
return dp.get("unload_longitude") or self._get("consigneeLongitude") or dp.get("longitude", "")
|
||||
|
||||
# ==================================================================
|
||||
# 步骤 1-5 : 司机端流程
|
||||
# ==================================================================
|
||||
def step01_query_goods(self) -> dict:
|
||||
self._log_step(1, "查询货源")
|
||||
result = self.client.step01_query_goods()
|
||||
self._set_vars(mainOrderCode=result["mainOrderCode"],
|
||||
residueNumber=result["residueNumber"],
|
||||
shipperLatitude=result["shipperLatitude"],
|
||||
shipperLongitude=result["shipperLongitude"],
|
||||
consigneeLatitude=result["consigneeLatitude"],
|
||||
consigneeLongitude=result["consigneeLongitude"])
|
||||
self.log.info(f" mainOrderCode = {result['mainOrderCode']}")
|
||||
self.log.info(f" residueNumber = {result['residueNumber']}")
|
||||
return result
|
||||
|
||||
def step02_accept_order(self) -> dict:
|
||||
self._log_step(2, "接单")
|
||||
data = self.client.step02_accept_order(
|
||||
main_order_code=self._get("mainOrderCode"),
|
||||
residue_number=self._get("residueNumber"),
|
||||
latitude=self._load_lat(),
|
||||
longitude=self._load_lng())
|
||||
self._check_response(data, "接单")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
def step03_query_pending_transport(self) -> dict:
|
||||
self._log_step(3, "查询待运输单")
|
||||
result = self.client.step03_query_pending_transport()
|
||||
self._set_vars(code=result["code"], vehicleCode=result["vehicleCode"],
|
||||
orderCode=result["orderCode"])
|
||||
self.log.info(f" code = {result['code']}")
|
||||
self.log.info(f" vehicleCode = {result['vehicleCode']}")
|
||||
self.log.info(f" orderCode = {result['orderCode']}")
|
||||
return result
|
||||
|
||||
def step04_load_goods(self) -> dict:
|
||||
self._log_step(4, "装货")
|
||||
data = self.client.step04_load_goods(
|
||||
code=self._get("code"),
|
||||
vehicle_code=self._get("vehicleCode"),
|
||||
latitude=self._load_lat(),
|
||||
longitude=self._load_lng())
|
||||
self._check_response(data, "装货")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
def step05_unload_goods(self) -> dict:
|
||||
self._log_step(5, "卸货")
|
||||
data = self.client.step05_unload_goods(
|
||||
code=self._get("code"),
|
||||
latitude=self._unload_lat(),
|
||||
longitude=self._unload_lng())
|
||||
self._check_response(data, "卸货")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
# ==================================================================
|
||||
# 步骤 6-9 : 运费核算流程
|
||||
# ==================================================================
|
||||
def step06_query_for_accounting(self) -> dict:
|
||||
self._log_step(6, "运费核算查询运单")
|
||||
record = self.client.step06_query_for_accounting(
|
||||
order_code=self._get("orderCode"))
|
||||
for k, v in record.items():
|
||||
self.vars[k] = str(v) if v is not None else ""
|
||||
self.log.info(f" 提取字段 ({len(record)}): {', '.join(list(record.keys())[:10])}...")
|
||||
return record
|
||||
|
||||
def step07_query_order_expenses(self) -> dict:
|
||||
self._log_step(7, "查询运单详情")
|
||||
result = self.client.step07_query_order_expenses(code=self._get("code"))
|
||||
self._set_vars(**result)
|
||||
for k, v in result.items():
|
||||
self.log.info(f" {k} = {v}")
|
||||
return result
|
||||
|
||||
def step08_calculate_order_detail(self) -> dict:
|
||||
self._log_step(8, "核算试算 (calculateOrderDetail)")
|
||||
result = self.client.step08_calculate_order_detail(code=self._get("code"))
|
||||
self._set_vars(**result)
|
||||
for k, v in result.items():
|
||||
self.log.info(f" {k} = {v[:80] if len(str(v)) > 80 else v}")
|
||||
return result
|
||||
|
||||
def step09_submit_accounting(self) -> dict:
|
||||
self._log_step(9, "核算提交 (orderHandleEditSubmitHandle)")
|
||||
data = self.client.step09_submit_accounting(self.vars)
|
||||
self._check_response(data, "核算提交")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
# ==================================================================
|
||||
# 步骤 10-16 : 审核 & 付款流程
|
||||
# ==================================================================
|
||||
def step10_query_audit(self) -> dict:
|
||||
self._log_step(10, "查询运单审核")
|
||||
data = self.client.step10_query_audit(order_code=self._get("orderCode"))
|
||||
self._check_response(data, "查询运单审核")
|
||||
records = data.get("data", {}).get("records", [])
|
||||
self.log.info(f" 记录数: {len(records)}")
|
||||
return data
|
||||
|
||||
def step11_approve_audit(self) -> dict:
|
||||
self._log_step(11, "核算审核通过")
|
||||
data = self.client.step11_approve_audit(code=self._get("code"))
|
||||
self._check_response(data, "核算审核通过")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
# Step 12-14 wrappers 不单独定义(已内联在 run_audit_payment_flow 中)
|
||||
|
||||
def step15_pay_to_driver(self) -> dict:
|
||||
self._log_step(15, "垫资-财务打款")
|
||||
data = self.client.step15_pay_to_driver(
|
||||
code=self._get("code"),
|
||||
delivery_cash_fee=self._get("deliveryCashFee"),
|
||||
driver_actual_freight=self._get("driverActualFreight"),
|
||||
plan_driver_payout=self._get("planDriverPayout", "0"),
|
||||
plan_captain_payout=self._get("planCaptainPayout", "0"))
|
||||
self._check_response(data, "垫资-财务打款")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
def step16_apply_for_payment(self) -> dict:
|
||||
self._log_step(16, "垫资-货主打款 (申请付款)")
|
||||
data = self.client.step16_apply_for_payment(
|
||||
code=self._get("code"),
|
||||
total_amount=self._get("shipmentActualCash"))
|
||||
self._check_response(data, "垫资-货主打款")
|
||||
self.log.info(f" 响应: code={data.get('code')}, msg={data.get('msg')}")
|
||||
return data
|
||||
|
||||
# ==================================================================
|
||||
# 步骤 17-21 : 回单/文件名前缀
|
||||
# ==================================================================
|
||||
def step17_query_upload_for_payno(self) -> dict:
|
||||
"""
|
||||
14. 查询运单上传 → payNo
|
||||
─────────────────
|
||||
GET /ntocc-basic-api/dytBusiness/orderDetail (status=5)
|
||||
Test: records[0].sendMsg → regex【(payNo)】
|
||||
Input: orderCode
|
||||
"""
|
||||
self._log_step(17, "查询运单上传 (提取 payNo)")
|
||||
result = self.client.step17_query_upload_for_payno(
|
||||
order_code=self._get("orderCode"),
|
||||
)
|
||||
pay_no = result["payNo"]
|
||||
self._set_vars(payNo=pay_no)
|
||||
self.log.info(f" payNo = {pay_no}")
|
||||
if not pay_no:
|
||||
# 迁移行为变化:旧版仅警告并继续(INSERT 空 front_seq_no),现视为失败
|
||||
raise Yun8StepError("查询运单上传", msg="未提取到 payNo(sendMsg 中无【】包裹内容)")
|
||||
return result
|
||||
|
||||
def step18_query_remit_records(self) -> list[dict]:
|
||||
"""
|
||||
15. 查询打款记录
|
||||
─────────────────
|
||||
GET /ntocc-tms-api/remitDefinite (分页)
|
||||
Test: records[0].code → remitCode
|
||||
Input: orderCode
|
||||
"""
|
||||
self._log_step(18, "查询打款记录")
|
||||
records = self.client.step18_query_remit_records(
|
||||
order_code=self._get("orderCode"),
|
||||
)
|
||||
self.log.info(f" 共 {len(records)} 条打款记录")
|
||||
if records:
|
||||
self.log.info(f" 首条字段: {list(records[0].keys())}")
|
||||
return records
|
||||
|
||||
def step19_query_file_prefix(self, remit_code: str) -> str:
|
||||
"""
|
||||
16. 查询文件名前缀
|
||||
─────────────────
|
||||
GET /ntocc-tms-api/centralizedPayment/downLoadElectronicReceipt?code={remitCode}
|
||||
Test: msg → '文件名前缀:{filePrefix}'
|
||||
"""
|
||||
file_prefix = self.client.step19_query_file_prefix(remit_code)
|
||||
return file_prefix
|
||||
|
||||
# ==================================================================
|
||||
# 组装流程
|
||||
# ==================================================================
|
||||
|
||||
def run_driver_flow(self) -> dict:
|
||||
"""仅跑司机端(步骤 1-5)"""
|
||||
self.log.info("\n" + "=" * 60)
|
||||
self.log.info(" 司机端流程 (Step 1-5)")
|
||||
self.log.info("=" * 60)
|
||||
|
||||
self.step01_query_goods()
|
||||
time.sleep(0.3)
|
||||
|
||||
self.step02_accept_order()
|
||||
time.sleep(0.3)
|
||||
|
||||
self.step03_query_pending_transport()
|
||||
|
||||
# ---- 重置运单 (条件触发) ----
|
||||
dp = self.config.get("driver_params", {})
|
||||
if dp.get("reset_order_detail", False):
|
||||
code = self._get("code")
|
||||
order_code = self._get("orderCode")
|
||||
self.log.info(f"\n [reset] 重置运单 orderCode={order_code}")
|
||||
reset_code = dp.get("reset_order_detail_code", "")
|
||||
with self.db.transaction():
|
||||
for sql, params in driver_reset_statements(reset_code, order_code):
|
||||
rows = self.db.execute(sql, params)
|
||||
self.log.info(f" SQL: {self.db.mogrify(sql, params)[:80]}... → {rows} rows")
|
||||
time.sleep(0.3)
|
||||
# 重新查询待运输单,刷新变量
|
||||
self.step03_query_pending_transport()
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
self.step04_load_goods()
|
||||
time.sleep(0.3)
|
||||
|
||||
self.step05_unload_goods()
|
||||
|
||||
# 卸货后 SQL
|
||||
if dp.get("reset_order_detail", False):
|
||||
reset_code = dp.get("reset_order_detail_code", "")
|
||||
sql, params = post_unload_reset_update(reset_code, self._get("orderCode"))
|
||||
rows = self.db.execute(sql, params)
|
||||
self.log.info(f" [reset] 卸货后 UPDATE: {rows} rows")
|
||||
else:
|
||||
self.log.info(" [...] 等待 10s ...")
|
||||
time.sleep(10)
|
||||
sql, params = risk_control_update(self._get("code"))
|
||||
rows = self.db.execute(sql, params)
|
||||
self.log.info(f" 卸货后 risk_control_status UPDATE: {rows} rows")
|
||||
|
||||
return self.vars
|
||||
|
||||
def run_accounting_flow(self) -> dict:
|
||||
"""运费核算流程(步骤 6-9)"""
|
||||
self.log.info("\n" + "=" * 60)
|
||||
self.log.info(" 运费核算流程 (Step 6-9)")
|
||||
self.log.info("=" * 60)
|
||||
|
||||
self.step06_query_for_accounting()
|
||||
time.sleep(0.5)
|
||||
|
||||
self.step07_query_order_expenses()
|
||||
time.sleep(0.3)
|
||||
|
||||
self.step08_calculate_order_detail()
|
||||
time.sleep(0.3)
|
||||
|
||||
self.step09_submit_accounting()
|
||||
|
||||
return self.vars
|
||||
|
||||
def _retry_on_processing(self, step_fn, step_name: str, max_retries: int = 5):
|
||||
"""
|
||||
遇到 "处理中" 错误时自动重试,每次间隔递增。
|
||||
服务端审核通过后需要异步处理,可能持续数秒到十几秒。
|
||||
"""
|
||||
for attempt in range(1, max_retries + 1):
|
||||
data = step_fn()
|
||||
msg = str(data.get("msg", ""))
|
||||
code = data.get("code")
|
||||
if code == 0 or (msg and ("成功" in msg or "success" in msg.lower())):
|
||||
return data
|
||||
if "处理中" in msg or "稍后" in msg:
|
||||
wait = 3 + attempt * 2 # 5, 7, 9, 11, 13 秒
|
||||
self.log.info(f" [...] {step_name} 服务端正处理中,第 {attempt}/{max_retries} 次重试(等待 {wait}s)...")
|
||||
time.sleep(wait)
|
||||
else:
|
||||
# 非 "处理中" 错误,直接返回
|
||||
return data
|
||||
return data
|
||||
|
||||
def run_audit_payment_flow(self) -> dict:
|
||||
"""审核 & 付款流程(步骤 10-16)"""
|
||||
self.log.info("\n" + "=" * 60)
|
||||
self.log.info(" 审核 & 付款流程 (Step 10-16)")
|
||||
self.log.info("=" * 60)
|
||||
|
||||
# 确保前置依赖已获取(独立运行 --stage payment 时 vars 可能为空)
|
||||
self._ensure_code_and_order_code()
|
||||
|
||||
self.step10_query_audit()
|
||||
time.sleep(0.5)
|
||||
|
||||
self.step11_approve_audit()
|
||||
self.log.info(" [...] 等待服务端处理审核结果...")
|
||||
|
||||
ac = self.config.get("accounting", {})
|
||||
if not ac.get("execute_after_audit", True):
|
||||
self.log.info(f"\n [stop] execute_after_audit=false,跳过步骤 12-21")
|
||||
return self.vars
|
||||
|
||||
# ---- 队长分账 (条件触发:payerReceiverPair 非空 + captainSplitMode=2) ----
|
||||
payer_receiver = ac.get("payer_receiver", "")
|
||||
payer_link_man = ac.get("payer_receiver_link_man", "")
|
||||
captain_split_mode = ac.get("captain_split_mode") or self._get("captainSplitMode")
|
||||
if (payer_receiver or payer_link_man) and captain_split_mode == "2":
|
||||
self._log_step(12, "队长分账预览 (captainSplitAdjustPreview)")
|
||||
preview = self.client.step12_captain_split_preview(
|
||||
order_detail_code=self._get("code"),
|
||||
)
|
||||
self._check_response(preview, "队长分账预览")
|
||||
latest_driver = float(preview.get("data", {}).get("latestDriverAmount", 0))
|
||||
oil_fee = float(preview.get("data", {}).get("deliveryOilFee", 0))
|
||||
self.log.info(f" latestDriverAmount = {latest_driver}")
|
||||
self.log.info(f" deliveryOilFee = {oil_fee}")
|
||||
time.sleep(0.5)
|
||||
|
||||
self._log_step(13, "队长分账调整 (adjustCaptainSplit)")
|
||||
# 计算 driverAmount/captainAmount (与 client 中计算逻辑一致)
|
||||
import math as _m
|
||||
_driver_amt = _m.ceil(latest_driver * 0.8 * 100) / 100.0
|
||||
_captain_amt = round(latest_driver - _driver_amt, 2)
|
||||
adjust = self.client.step13_adjust_captain_split(
|
||||
order_detail_code=self._get("code"),
|
||||
latest_driver_amount=latest_driver,
|
||||
delivery_oil_fee=oil_fee,
|
||||
)
|
||||
self._check_response(adjust, "队长分账调整")
|
||||
self.log.info(f" 响应: code={adjust.get('code')}, msg={adjust.get('msg')}")
|
||||
# 存入 vars,供 step13 财务打款使用
|
||||
self._set_vars(planDriverPayout=str(_driver_amt),
|
||||
planCaptainPayout=str(_captain_amt))
|
||||
self.log.info(f" planDriverPayout = {_driver_amt}")
|
||||
self.log.info(f" planCaptainPayout = {_captain_amt}")
|
||||
else:
|
||||
self.log.info(f" [skip] 队长分账 (payerReceiver='{payer_receiver}', captainSplitMode={captain_split_mode})")
|
||||
time.sleep(0.5)
|
||||
|
||||
# ---- Step 14: singleOil 带重试 ----
|
||||
self._log_step(14, "单油卡充值 (singleOil)")
|
||||
oil_amount = self._get("oilFee")
|
||||
data_12 = self._retry_on_processing(
|
||||
lambda: self.client.step14_single_oil(
|
||||
order_detail_code=self._get("code"),
|
||||
amount=oil_amount if oil_amount else "0.03",
|
||||
),
|
||||
"单油卡充值",
|
||||
)
|
||||
self._check_response(data_12, "singleOil")
|
||||
self.log.info(f" 响应: code={data_12.get('code')}, msg={data_12.get('msg')}")
|
||||
|
||||
# Step 13 带重试
|
||||
self._log_step(15, "垫资-财务打款")
|
||||
data_12 = self._retry_on_processing(
|
||||
lambda: self.client.step15_pay_to_driver(
|
||||
code=self._get("code"),
|
||||
delivery_cash_fee=self._get("deliveryCashFee"),
|
||||
driver_actual_freight=self._get("driverActualFreight"),
|
||||
plan_driver_payout=self._get("planDriverPayout", "0"),
|
||||
plan_captain_payout=self._get("planCaptainPayout", "0"),
|
||||
),
|
||||
"垫资-财务打款",
|
||||
)
|
||||
self._check_response(data_12, "垫资-财务打款")
|
||||
self.log.info(f" 响应: code={data_12.get('code')}, msg={data_12.get('msg')}")
|
||||
|
||||
# Step 14 带重试 (totalAmount 取 step06 的 shipmentActualCash)
|
||||
self._log_step(16, "垫资-货主打款 (申请付款)")
|
||||
data_13 = self._retry_on_processing(
|
||||
lambda: self.client.step16_apply_for_payment(
|
||||
code=self._get("code"),
|
||||
total_amount=self._get("shipmentActualCash"),
|
||||
),
|
||||
"垫资-货主打款",
|
||||
)
|
||||
self._check_response(data_13, "垫资-货主打款")
|
||||
self.log.info(f" 响应: code={data_13.get('code')}, msg={data_13.get('msg')}")
|
||||
|
||||
return self.vars
|
||||
|
||||
def run_receipt_file_flow(self, dry_run: bool = False) -> list[dict]:
|
||||
"""回单文件流程(步骤 15-18)+ SQL INSERT"""
|
||||
self.log.info("\n" + "=" * 60)
|
||||
self.log.info(" 回单文件流程 (Step 14-16 + SQL)")
|
||||
self.log.info("=" * 60)
|
||||
|
||||
# 确保 orderCode 已就绪(独立运行 --stage receipt 时需要)
|
||||
self._ensure_code_and_order_code()
|
||||
|
||||
# 14. 获取 payNo
|
||||
self.step17_query_upload_for_payno()
|
||||
time.sleep(0.3)
|
||||
|
||||
# 15. 获取所有打款记录
|
||||
remit_records = self.step18_query_remit_records()
|
||||
|
||||
if not remit_records:
|
||||
self.log.info(" [EXIT] 无打款记录")
|
||||
return []
|
||||
|
||||
# 16 + SQL: 遍历每条打款记录
|
||||
pay_no = self._get("payNo")
|
||||
results = []
|
||||
ok = 0
|
||||
ng = 0
|
||||
|
||||
for i, rec in enumerate(remit_records):
|
||||
remit_code = rec.get("code", "")
|
||||
if not remit_code:
|
||||
self.log.info(f"\n [{i+1}/{len(remit_records)}] 跳过:缺 code 字段")
|
||||
ng += 1
|
||||
results.append({"remit_code": remit_code, "status": "skip_no_code"})
|
||||
continue
|
||||
|
||||
self.log.info(f"\n [{i+1}/{len(remit_records)}] remitCode = {remit_code}")
|
||||
|
||||
# 16. 查询文件名前缀
|
||||
file_prefix = self.step19_query_file_prefix(remit_code)
|
||||
self.log.info(f" filePrefix = {file_prefix}")
|
||||
|
||||
if not file_prefix:
|
||||
self.log.info(f" [!] filePrefix 为空,跳过 SQL INSERT")
|
||||
ng += 1
|
||||
results.append({"remit_code": remit_code, "status": "skip_no_prefix"})
|
||||
continue
|
||||
|
||||
file_name = self.file_name_template.format(file_prefix=file_prefix)
|
||||
self.log.info(f" file_name = {file_name}")
|
||||
self.log.info(f" payNo = {pay_no}")
|
||||
|
||||
# SQL INSERT(参数化;dry-run 用 mogrify 渲染预览)
|
||||
sql = RECEIPT_FILE_INSERT_SQL
|
||||
params = receipt_file_insert_params(file_prefix, pay_no,
|
||||
self.file_name_template)
|
||||
if dry_run:
|
||||
self.log.info(f" [DRY RUN] SQL:\n{self.db.mogrify(sql, params)}")
|
||||
ok += 1
|
||||
results.append({"remit_code": remit_code, "file_prefix": file_prefix,
|
||||
"status": "dry_run"})
|
||||
else:
|
||||
try:
|
||||
rowcount = self.db.execute(sql, params)
|
||||
self.log.info(f" [OK] INSERT 成功 (rows: {rowcount})")
|
||||
ok += 1
|
||||
results.append({"remit_code": remit_code, "file_prefix": file_prefix,
|
||||
"status": "inserted", "rows": rowcount})
|
||||
except Exception as exc:
|
||||
self.log.info(f" [FAIL] INSERT 失败: {exc}")
|
||||
ng += 1
|
||||
results.append({"remit_code": remit_code, "file_prefix": file_prefix,
|
||||
"status": "error", "error": str(exc)})
|
||||
|
||||
# 汇总
|
||||
self.log.info(f"\n ───────────────────────")
|
||||
self.log.info(f" 回单文件流程完成: 成功 {ok}, 失败 {ng}, 总计 {len(remit_records)}")
|
||||
|
||||
# Step 18: 上报运单(迁移行为变化:失败不再吞异常,向上传播)
|
||||
self._log_step(20, "上报运单 (reportOrderDetail)")
|
||||
data_17 = self.client.step20_report_order_detail(
|
||||
codes=self._get("code"),
|
||||
)
|
||||
self._check_response(data_17, "上报运单")
|
||||
self.log.info(f" 响应: code={data_17.get('code')}, msg={data_17.get('msg')}")
|
||||
|
||||
# Step 19: 滇运通审核 (独立 token/host,config 控制开关)
|
||||
# (迁移行为变化:失败不再吞异常,向上传播)
|
||||
if self.client.tp_enabled:
|
||||
self._log_step(21, "滇运通审核 (updateOrderDetailExt)")
|
||||
data_19 = self.client.step21_owner_review(
|
||||
order_detail_code=self._get("code"),
|
||||
)
|
||||
self._check_response(data_19, "滇运通审核")
|
||||
self.log.info(f" 响应: code={data_19.get('code')}, msg={data_19.get('msg')}")
|
||||
else:
|
||||
self.log.info(f"\n [skip] Step 19 滇运通审核 (dianyuntong.enabled=false)")
|
||||
|
||||
return results
|
||||
|
||||
# ==================================================================
|
||||
# 步骤 22-24 : ETC 开票 SQL 写入
|
||||
# ==================================================================
|
||||
def _gen_etc_invoice(self, idx: int, total: int, amount: float) -> dict:
|
||||
"""
|
||||
生成第 idx 条发票的差异字段(发票号码/金额/进出站/时间)。
|
||||
金额由调用方传入(已按"所有发票合计 ≤ 运费×50%"约束分配)。
|
||||
时间规则: transaction_time(=ex_time) 在运单窗口
|
||||
2026-05-13 15:35:38 ~ 2026-05-15 13:35:43 内按条数均分、逐条推进,
|
||||
en_date=ex_date=该时间日期;invoice_time 在 2026-06-04 ~ 06-06 内随机。
|
||||
"""
|
||||
invoice_number = "264" + "".join(random.choices("0123456789", k=17))
|
||||
pair = idx % (len(_ETC_STATION_CHAIN) - 1)
|
||||
w_start = datetime(2026, 5, 13, 15, 35, 38)
|
||||
w_end = datetime(2026, 5, 15, 13, 35, 43)
|
||||
seg = (w_end - w_start) / total
|
||||
trans_time = w_start + seg * idx + timedelta(
|
||||
seconds=random.uniform(0, seg.total_seconds()))
|
||||
invoice_time = datetime(2026, 6, 4) + timedelta(
|
||||
seconds=random.uniform(0, 3 * 86400))
|
||||
return {
|
||||
"code": _gen_code(),
|
||||
"invoice_number": invoice_number,
|
||||
"total_price": amount,
|
||||
"amount": amount,
|
||||
"import_station": _ETC_STATION_CHAIN[pair],
|
||||
"export_station": _ETC_STATION_CHAIN[pair + 1],
|
||||
"transaction_time": trans_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"ex_time": trans_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"en_date": trans_time.strftime("%Y-%m-%d"),
|
||||
"ex_date": trans_time.strftime("%Y-%m-%d"),
|
||||
"invoice_time": invoice_time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
|
||||
def step22_insert_etc_invoices(self, dry_run: bool = False) -> list[dict]:
|
||||
"""
|
||||
22. 写入多条 ETC 开票信息 (base_etc_invoice)
|
||||
─────────────────
|
||||
每个运单插入 etc_invoice.count 条发票(config.yaml 配置,默认 3),
|
||||
每条的 发票号码/金额/进站/出站/时间 各不相同,其余字段按原固定值:
|
||||
order_code ← 步骤03的 orderCode
|
||||
push_order_no ← orderCode + "-1"
|
||||
order_detail_code ← 步骤03的 code
|
||||
code 按现有数据规则自动生成 (32 位十六进制),每条独立
|
||||
金额约束: 所有发票金额合计 ≤ 运费(优先 driverActualFreight,其次
|
||||
shipmentActualFreight) × 50%;运费未知或上限不足时跳过发票写入。
|
||||
"""
|
||||
self._log_step(22, "ETC 开票信息写入 (base_etc_invoice × N)")
|
||||
order_code = self._get("orderCode")
|
||||
detail_code = self._get("code")
|
||||
if not order_code or not detail_code:
|
||||
self.log.info(" [!] orderCode/code 为空,无法写入 base_etc_invoice")
|
||||
return [{"table": "base_etc_invoice", "status": "skip_no_vars"}]
|
||||
|
||||
raw_count = self.config.get("etc_invoice", {}).get("count", 3)
|
||||
n = int(raw_count) if raw_count is not None else 3
|
||||
push_order_no = f"{order_code}-1"
|
||||
self.log.info(f" order_code = {order_code}")
|
||||
self.log.info(f" push_order_no = {push_order_no}")
|
||||
self.log.info(f" order_detail_code = {detail_code}")
|
||||
self.log.info(f" 发票条数 = {n}")
|
||||
if n <= 0:
|
||||
self.log.info(" [skip] etc_invoice.count <= 0,跳过")
|
||||
return []
|
||||
|
||||
# ---- 金额约束: 所有发票金额合计 ≤ 运费 × 50% ----
|
||||
freight_raw = self._get("driverActualFreight") or self._get("shipmentActualFreight")
|
||||
try:
|
||||
freight = float(freight_raw)
|
||||
except (TypeError, ValueError):
|
||||
freight = 0.0
|
||||
freight_src = ("driverActualFreight" if self._get("driverActualFreight")
|
||||
else "shipmentActualFreight")
|
||||
cap = freight * 0.5
|
||||
cap_floor = int(cap * 100) / 100 # 向下取整到分,确保不超上限
|
||||
self.log.info(f" 运费({freight_src}) = {freight:.2f},发票金额上限(运费×50%) = {cap_floor:.2f}")
|
||||
if cap_floor < 0.01 * n:
|
||||
self.log.info(f" [!] 上限 {cap_floor:.2f} 不足以分配 {n} 张发票(每张至少 0.01),跳过发票写入")
|
||||
return [{"table": "base_etc_invoice", "status": "skip_amount_cap"}]
|
||||
|
||||
# 随机总额: 上限的 30%~100%,且保证每张至少 0.01;按整数分分配,合计精确
|
||||
total_amt = min(cap_floor, max(0.01 * n,
|
||||
round(cap_floor * random.uniform(0.3, 1.0), 2)))
|
||||
total_cents = int(round(total_amt * 100))
|
||||
alloc = [0] * n # 每张保底 1 分
|
||||
pool = total_cents - n
|
||||
if pool > 0:
|
||||
weights = [random.uniform(0.5, 1.5) for _ in range(n)]
|
||||
wsum = sum(weights)
|
||||
alloc = [int(pool * w / wsum) for w in weights]
|
||||
for _ in range(pool - sum(alloc)):
|
||||
alloc[random.randrange(n)] += 1
|
||||
amounts = [(1 + a) / 100 for a in alloc]
|
||||
# 写入前显式判断(按构造不应触发,作兜底保护)
|
||||
if round(sum(amounts), 2) > round(cap, 2):
|
||||
self.log.info(f" [!] 发票合计 {sum(amounts):.2f} 超过运费×50% {cap:.2f},跳过发票写入")
|
||||
return [{"table": "base_etc_invoice", "status": "skip_amount_cap"}]
|
||||
self.log.info(f" 发票金额合计 = {sum(amounts):.2f}({n} 张,各 {['%.2f' % a for a in amounts]})≤ 上限,校验通过")
|
||||
|
||||
results = []
|
||||
for i in range(n):
|
||||
inv = self._gen_etc_invoice(i, n, amounts[i])
|
||||
sql = ETC_INVOICE_INSERT_SQL
|
||||
params = etc_invoice_insert_params(inv, order_code, push_order_no,
|
||||
detail_code)
|
||||
self.log.info(f" [{i+1}/{n}] 发票号={inv['invoice_number']} 金额={inv['total_price']:.2f} "
|
||||
f"{inv['import_station']}→{inv['export_station']} "
|
||||
f"交易时间={inv['transaction_time']} 开票时间={inv['invoice_time']}")
|
||||
|
||||
if dry_run:
|
||||
self.log.info(f" [DRY RUN] SQL:\n{self.db.mogrify(sql, params)}")
|
||||
results.append({"table": "base_etc_invoice",
|
||||
"invoice_number": inv["invoice_number"],
|
||||
"status": "dry_run"})
|
||||
continue
|
||||
try:
|
||||
rows = self.db.execute(sql, params)
|
||||
self.log.info(f" [OK] INSERT 成功 (rows: {rows})")
|
||||
results.append({"table": "base_etc_invoice",
|
||||
"invoice_number": inv["invoice_number"],
|
||||
"status": "inserted", "rows": rows})
|
||||
except Exception as exc:
|
||||
self.log.info(f" [FAIL] INSERT 失败: {exc}")
|
||||
results.append({"table": "base_etc_invoice",
|
||||
"invoice_number": inv["invoice_number"],
|
||||
"status": "error", "error": str(exc)})
|
||||
|
||||
ok = sum(1 for r in results if r["status"] in ("inserted", "dry_run"))
|
||||
ng = len(results) - ok
|
||||
self.log.info(f" base_etc_invoice 完成: 成功 {ok}, 失败 {ng}, 总计 {n}")
|
||||
return results
|
||||
|
||||
def step23_insert_etc_push(self, dry_run: bool = False) -> dict:
|
||||
"""
|
||||
23. 写入 ETC 推送记录 (tms_order_etc_push)
|
||||
─────────────────
|
||||
INSERT INTO ntocc.tms_order_etc_push
|
||||
order_no ← 步骤03的 orderCode
|
||||
push_order_no ← orderCode + "-1"
|
||||
order_detail_code ← 步骤03的 code
|
||||
code 按现有数据规则自动生成 (32 位十六进制)
|
||||
"""
|
||||
self._log_step(23, "ETC 推送记录写入 (tms_order_etc_push)")
|
||||
order_code = self._get("orderCode")
|
||||
detail_code = self._get("code")
|
||||
if not order_code or not detail_code:
|
||||
self.log.info(" [!] orderCode/code 为空,无法写入 tms_order_etc_push")
|
||||
return {"table": "tms_order_etc_push", "status": "skip_no_vars"}
|
||||
|
||||
etc_code = _gen_code()
|
||||
push_order_no = f"{order_code}-1"
|
||||
sql = ETC_PUSH_INSERT_SQL
|
||||
params = (etc_code, push_order_no, order_code, detail_code)
|
||||
self.log.info(f" order_no = {order_code}")
|
||||
self.log.info(f" push_order_no = {push_order_no}")
|
||||
self.log.info(f" order_detail_code = {detail_code}")
|
||||
self.log.info(f" code = {etc_code}")
|
||||
|
||||
if dry_run:
|
||||
self.log.info(f" [DRY RUN] SQL:\n{self.db.mogrify(sql, params)}")
|
||||
return {"table": "tms_order_etc_push", "status": "dry_run"}
|
||||
try:
|
||||
rows = self.db.execute(sql, params)
|
||||
self.log.info(f" [OK] INSERT 成功 (rows: {rows})")
|
||||
return {"table": "tms_order_etc_push", "status": "inserted", "rows": rows}
|
||||
except Exception as exc:
|
||||
self.log.info(f" [FAIL] INSERT 失败: {exc}")
|
||||
return {"table": "tms_order_etc_push", "status": "error", "error": str(exc)}
|
||||
|
||||
def step24_update_order_etc_status(self, dry_run: bool = False) -> dict:
|
||||
"""
|
||||
24. 更新运单 ETC 开票状态 (tms_order_detail)
|
||||
─────────────────
|
||||
UPDATE ntocc.tms_order_detail SET etc_invoice_status = 3
|
||||
WHERE order_code = 步骤03的 orderCode
|
||||
"""
|
||||
self._log_step(24, "更新运单 ETC 开票状态 (tms_order_detail.etc_invoice_status=3)")
|
||||
order_code = self._get("orderCode")
|
||||
if not order_code:
|
||||
self.log.info(" [!] orderCode 为空,无法更新 tms_order_detail")
|
||||
return {"table": "tms_order_detail", "status": "skip_no_vars"}
|
||||
|
||||
sql, params = etc_status_update(order_code)
|
||||
self.log.info(f" order_code = {order_code}")
|
||||
|
||||
if dry_run:
|
||||
self.log.info(f" [DRY RUN] SQL:\n{self.db.mogrify(sql, params)}")
|
||||
return {"table": "tms_order_detail", "status": "dry_run"}
|
||||
try:
|
||||
rows = self.db.execute(sql, params)
|
||||
self.log.info(f" [OK] UPDATE 成功 (rows: {rows})")
|
||||
return {"table": "tms_order_detail", "status": "updated", "rows": rows}
|
||||
except Exception as exc:
|
||||
self.log.info(f" [FAIL] UPDATE 失败: {exc}")
|
||||
return {"table": "tms_order_detail", "status": "error", "error": str(exc)}
|
||||
|
||||
def run_etc_invoice_flow(self, dry_run: bool = False) -> list[dict]:
|
||||
"""ETC 开票 SQL 写入(步骤 22-24)"""
|
||||
self.log.info("\n" + "=" * 60)
|
||||
self.log.info(" ETC 开票 SQL 写入 (Step 22-24)")
|
||||
self.log.info("=" * 60)
|
||||
|
||||
# 确保 orderCode / code 已就绪(独立运行 --stage receipt 时需要)
|
||||
self._ensure_code_and_order_code()
|
||||
|
||||
results = []
|
||||
results.extend(self.step22_insert_etc_invoices(dry_run=dry_run))
|
||||
results.append(self.step23_insert_etc_push(dry_run=dry_run))
|
||||
results.append(self.step24_update_order_etc_status(dry_run=dry_run))
|
||||
return results
|
||||
|
||||
def _ensure_code_and_order_code(self):
|
||||
"""
|
||||
确保 code / orderCode 等关键变量已就绪。
|
||||
|
||||
独立运行 --stage payment/receipt 时 vars 为空,
|
||||
需自动补充。优先尝试 driver 端 step03(waitTrans 状态),
|
||||
若订单已过 driver 阶段则降级为 admin 端查询(step10 接口)。
|
||||
"""
|
||||
if self._get("code") and self._get("orderCode"):
|
||||
self.log.info(f" (复用已有 code={self._get('code')}, orderCode={self._get('orderCode')})")
|
||||
return
|
||||
|
||||
self.log.info(" [*] 自动补充前置数据...")
|
||||
|
||||
# 方式 A: 通过 driver 端 step03(status=waitTrans)
|
||||
driver_auth = self.client.driver_http.get_header("authorization")
|
||||
if driver_auth:
|
||||
try:
|
||||
result = self.client.step03_query_pending_transport()
|
||||
self._set_vars(
|
||||
code=result["code"],
|
||||
vehicleCode=result["vehicleCode"],
|
||||
orderCode=result["orderCode"],
|
||||
)
|
||||
self.log.info(f" [driver] code={self._get('code')}, orderCode={self._get('orderCode')}")
|
||||
except Exception:
|
||||
self.log.info(" driver 端无待运输单,切换 admin 端查询...")
|
||||
driver_auth = "" # 标记失败,走降级
|
||||
|
||||
# 方式 B: 通过 admin 端(status=6 已结算 / status=5 已打款)
|
||||
if not self._get("code") or not self._get("orderCode"):
|
||||
for status_val in ["6", "5"]:
|
||||
try:
|
||||
# status=6 用 step10(/ntocc-tms-api/orderDetail?status=6&isSettle=1)
|
||||
if status_val == "6":
|
||||
data = self.client.step10_query_audit(
|
||||
order_code="",
|
||||
page_size=5,
|
||||
)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
else:
|
||||
# status=5 用 step17(/ntocc-basic-api/dytBusiness/orderDetail?status=5)
|
||||
data = self.client.step17_query_upload_for_payno(
|
||||
order_code="", page_size=5,
|
||||
)
|
||||
records = data.get("raw_records", [])
|
||||
|
||||
if records:
|
||||
r = records[0]
|
||||
self._set_vars(
|
||||
code=r.get("code", ""),
|
||||
orderCode=r.get("orderCode", ""),
|
||||
)
|
||||
if self._get("code"):
|
||||
self.log.info(f" [admin status={status_val}] code={self._get('code')}, orderCode={self._get('orderCode')}")
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.debug(f" [admin status={status_val}] 查询失败: {e}")
|
||||
continue
|
||||
|
||||
if not self._get("code"):
|
||||
self.log.info(" [!] 无法自动获取 code,请使用 --stage all 完整运行或手动指定")
|
||||
return
|
||||
|
||||
# 补充核算数据(payment 阶段需要 deliveryCashFee/driverActualFreight)
|
||||
if not self._get("deliveryCashFee"):
|
||||
try:
|
||||
record = self.client.step06_query_for_accounting(
|
||||
order_code=self._get("orderCode"),
|
||||
)
|
||||
for k, v in record.items():
|
||||
self.vars[k] = str(v) if v is not None else ""
|
||||
expenses = self.client.step07_query_order_expenses(
|
||||
code=self._get("code"),
|
||||
)
|
||||
self._set_vars(**expenses)
|
||||
self.log.info(f" deliveryCashFee={self._get('deliveryCashFee')}, driverActualFreight={self._get('driverActualFreight')}")
|
||||
except Exception as e:
|
||||
self.log.info(f" [!] 补充核算字段失败: {e}")
|
||||
|
||||
def set_auth(self, driver_token: str, admin_token: str):
|
||||
"""设置授权 Token"""
|
||||
self.client.set_driver_auth(driver_token)
|
||||
self.client.set_admin_auth(admin_token)
|
||||
self.log.info(f" Driver auth: {driver_token[:30]}...")
|
||||
self.log.info(f" Admin auth: {admin_token[:30]}...")
|
||||
|
||||
def close(self):
|
||||
"""关闭 HTTP 会话连接池(数据库连接由调用方管理)。"""
|
||||
self.client.close()
|
||||
@@ -0,0 +1,142 @@
|
||||
"""回单/上报接口 Mixin — step17-20(管理端)。
|
||||
|
||||
所有方法签名/返回值/默认值与旧 yun8_sync/client.py 逐字一致,
|
||||
仅将原始 requests.Session 调用替换为 BaseHttpClient(self.admin_http)。
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.receipt_api import ReceiptApi
|
||||
class Yun8ApiClient(ReceiptApi, ...): ...
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class ReceiptApi:
|
||||
"""回单/上报接口(挂载于 Yun8ApiClient,依赖 admin_http / ac 属性)。"""
|
||||
|
||||
# ==================================================================
|
||||
# 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.admin_http.get("/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.admin_http.get("/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.admin_http.get(
|
||||
"/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}
|
||||
return self.admin_http.post_json(
|
||||
"/ntocc-basic-api/dytBusiness/reportOrderDetail", body)
|
||||
@@ -0,0 +1,130 @@
|
||||
"""yun8 全部 SQL 语句与参数化构造器。
|
||||
|
||||
所有语句统一使用 %s 参数占位(由 Db.execute 交给 pymysql 转义),
|
||||
字面量列值(固定 code/时间戳/URL 等)从旧 yun8_sync 实现逐字保留。
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.sql_templates import (
|
||||
RECEIPT_FILE_INSERT_SQL, receipt_file_insert_params,
|
||||
ETC_INVOICE_INSERT_SQL, ETC_PUSH_INSERT_SQL,
|
||||
driver_reset_statements, post_unload_reset_update, risk_control_update,
|
||||
)
|
||||
"""
|
||||
|
||||
# ── 回单文件 INSERT (base_gd_mft_file) ────────────────────────────
|
||||
# 仅 file_name / front_seq_no(=payNo) 为参数;其余列值固定。
|
||||
RECEIPT_FILE_INSERT_SQL = """
|
||||
INSERT INTO `ntocc`.`base_gd_mft_file`
|
||||
(`id`, `code`, `file_name`, `remark`, `is_deleted`,
|
||||
`created_by`, `gmt_created`, `modified_by`, `gmt_modified`,
|
||||
`modified_by_name`, `is_directory`, `parent_directory`,
|
||||
`local_path`, `url_path`, `front_seq_no`)
|
||||
VALUES
|
||||
(NULL, '10cd8a53b921463ab8079ea61376574e', %s,
|
||||
'批量写入回单编号20260608', 0,
|
||||
'', '2025-12-21 16:24:11', '', '2025-12-21 16:24:11',
|
||||
'', 0, '415612025121420251215274330494862543.zip',
|
||||
'/data/cebBankService/tempFile/415612025121420251215274330494862543/41561202512141330494250466_1_signed.pdf',
|
||||
'https://public-yun8.eos-chongqing-1.cmecloud.cn/d83a1298991040a4a125a84acadd88b9.pdf',
|
||||
%s)
|
||||
"""
|
||||
|
||||
|
||||
def receipt_file_insert_params(file_prefix: str, pay_no: str,
|
||||
file_name_template: str) -> tuple:
|
||||
"""回单 INSERT 参数: (file_name, pay_no)。"""
|
||||
file_name = file_name_template.format(file_prefix=file_prefix)
|
||||
return (file_name, pay_no)
|
||||
|
||||
|
||||
# ── ETC 发票 INSERT (base_etc_invoice) ────────────────────────────
|
||||
# 参数: code, order_code, push_order_no, order_detail_code, invoice_time,
|
||||
# import_station, export_station, transaction_time, total_price,
|
||||
# amount, invoice_number, en_date, ex_date, ex_time
|
||||
ETC_INVOICE_INSERT_SQL = (
|
||||
"INSERT INTO `ntocc`.`base_etc_invoice` ("
|
||||
"`id`, `code`, `order_code`, `push_order_no`, `order_detail_code`, "
|
||||
"`invoice_time`, `sale_tax_code`, `sale_name`, `import_station`, `export_station`, "
|
||||
"`transaction_time`, `total_price`, `amount`, `tax_rate`, `tax_amount`, "
|
||||
"`invoice_type`, `invoice_code`, `invoice_number`, `waybill_start_time`, `waybill_end_time`, "
|
||||
"`invoice_url`, `invoice_html_url`, `en_date`, `ex_date`, `ex_time`, "
|
||||
"`is_valid`, `task_name`, `plate_num`, `etc_invoice_status`, `tax_deduction_status`, "
|
||||
"`sign`, `is_deleted`, `created_by`, `modified_by`, `gmt_created`, `gmt_modified`) "
|
||||
"VALUES ("
|
||||
"NULL, %s, %s, %s, %s, "
|
||||
"%s, '9141010055833160XE', '河南省视博电子股份有限公司', "
|
||||
"%s, %s, "
|
||||
"%s, %s, %s, 0.03, 0.03, "
|
||||
"'征税发票', '*', %s, '2026-05-13 15:35:38', '2026-05-15 13:35:43', "
|
||||
"'https://hdfs.txffp.com/e/1102e1en66nPPP1ChAeC161e1C6Pei16.zip?Expires=1812122633&OSSAccessKeyId=LTAIYAJbICRhyCD8&Signature=TLjlHgKtCxwWpPliPRQ0xQ6HdKY%3D', "
|
||||
"'https://gateway.txffp.com/alle/common/htmlurl/getinvoice/41012526221804003093/1102e1en66nPPP1ChAeC161e1C6Pei16', "
|
||||
"%s, %s, %s, "
|
||||
"0, 'etc开票申请2026070604', '冀D8E016', 3, 0, "
|
||||
"NULL, 0, '', '04b7e539c77d4bdbb4fe4e87eaaf41e4', '2026-07-06 09:55:16', '2026-07-06 10:03:26')"
|
||||
)
|
||||
|
||||
|
||||
def etc_invoice_insert_params(inv: dict, order_code: str, push_order_no: str,
|
||||
order_detail_code: str) -> tuple:
|
||||
"""ETC 发票 INSERT 参数(inv 为 pipeline._gen_etc_invoice 的产出)。"""
|
||||
return (
|
||||
inv["code"], order_code, push_order_no, order_detail_code,
|
||||
inv["invoice_time"],
|
||||
inv["import_station"], inv["export_station"],
|
||||
inv["transaction_time"], inv["total_price"], inv["amount"],
|
||||
inv["invoice_number"],
|
||||
inv["en_date"], inv["ex_date"], inv["ex_time"],
|
||||
)
|
||||
|
||||
|
||||
# ── ETC 推送记录 INSERT (tms_order_etc_push) ──────────────────────
|
||||
# 参数: code, push_order_no, order_no, order_detail_code;其余固定。
|
||||
ETC_PUSH_INSERT_SQL = (
|
||||
"INSERT INTO `ntocc`.`tms_order_etc_push` ("
|
||||
"`id`, `code`, `push_order_no`, `order_no`, `order_detail_code`, "
|
||||
"`start_time`, `end_time`, `year`, `month`, `vendor`, "
|
||||
"`request`, `start_request`, `end_request`, `invoice_pull_status`, `invoice_pull_time`, "
|
||||
"`start_status`, `start_push_time`, `start_result`, `end_status`, `end_push_time`, `end_result`, "
|
||||
"`is_deleted`, `created_by`, `modified_by`, `gmt_created`, `gmt_modified`) "
|
||||
"VALUES ("
|
||||
"NULL, %s, %s, %s, %s, "
|
||||
"'2026-07-07 15:08:25', '2026-07-07 15:08:28', 2026, 6, 1, "
|
||||
"NULL, '11111', '11111', 2, '2026-07-07 11:40:21', "
|
||||
"1, '2026-07-07 15:17:02', '成功', 1, '2026-07-07 15:11:04', '成功', "
|
||||
"0, NULL, NULL, '2026-07-07 11:09:17', '2026-07-07 15:15:18')"
|
||||
)
|
||||
|
||||
|
||||
# ── 司机流程 reset / 风控状态 UPDATE ──────────────────────────────
|
||||
def driver_reset_statements(reset_code: str, order_code: str) -> list[tuple]:
|
||||
"""step03 后的重置序列(3 DELETE + 1 UPDATE),需在一个事务内执行。"""
|
||||
return [
|
||||
("DELETE FROM tms_order_detail WHERE code = %s", (reset_code,)),
|
||||
("DELETE FROM tms_order_detail_ext WHERE order_detail_code = %s", (reset_code,)),
|
||||
("DELETE FROM tms_order_detail_abnormal_status WHERE order_detail_code = %s", (reset_code,)),
|
||||
("UPDATE tms_order_detail SET code = %s WHERE order_code = %s",
|
||||
(reset_code, order_code)),
|
||||
]
|
||||
|
||||
|
||||
def post_unload_reset_update(reset_code: str, order_code: str) -> tuple:
|
||||
"""reset 模式卸货后的运单号/装货签收时间回写。"""
|
||||
sql = ("UPDATE tms_order_detail SET code = %s, "
|
||||
"fill_time = '2026-07-05 19:30:56', "
|
||||
"sign_time = '2026-07-05 19:52:30' "
|
||||
"WHERE order_code = %s")
|
||||
return (sql, (reset_code, order_code))
|
||||
|
||||
|
||||
def risk_control_update(order_detail_code: str, status: int = 1) -> tuple:
|
||||
"""卸货后风控状态回写(按 order_detail_code 直更)。"""
|
||||
sql = ("UPDATE tms_order_detail_ext SET risk_control_status = %s "
|
||||
"WHERE order_detail_code = %s")
|
||||
return (sql, (status, order_detail_code))
|
||||
|
||||
|
||||
def etc_status_update(order_code: str, status: int = 3) -> tuple:
|
||||
"""运单 ETC 开票状态更新 (tms_order_detail.etc_invoice_status)。"""
|
||||
sql = ("UPDATE `ntocc`.`tms_order_detail` SET etc_invoice_status = %s "
|
||||
"WHERE order_code = %s")
|
||||
return (sql, (status, order_code))
|
||||
@@ -0,0 +1,34 @@
|
||||
"""第三方(滇运通)接口 Mixin — step21。
|
||||
|
||||
签名/返回值与旧 yun8_sync/client.py 逐字一致;
|
||||
滇运通为独立 host/token 会话(self.tp_http,base_url 为滇运通域名)。
|
||||
|
||||
Usage:
|
||||
from tests.apis.yun8.third_party_api import ThirdPartyApi
|
||||
class Yun8ApiClient(ThirdPartyApi, ...): ...
|
||||
"""
|
||||
|
||||
|
||||
class ThirdPartyApi:
|
||||
"""滇运通接口(挂载于 Yun8ApiClient,依赖 tp_http / tp_enabled 属性)。"""
|
||||
|
||||
# ==================================================================
|
||||
# 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_http:
|
||||
raise RuntimeError("dianyuntong 未启用,请检查配置 yun8_api.dianyuntong.enabled")
|
||||
params = {
|
||||
"orderDetailCode": order_detail_code,
|
||||
"reviewStatus": "1",
|
||||
"reviewMsg": "",
|
||||
"invoiceApplicationStatus": "0",
|
||||
"invoiceApplicationMsg": "",
|
||||
}
|
||||
return self.tp_http.post(
|
||||
"/ntocc-tms-api/orderDetail/updateOrderDetailExt", params=params)
|
||||
Reference in New Issue
Block a user