feat(yun8_sync): add API pipeline automating full freight flow (18 interfaces + SQL)
Implements the "运输流程" collection from yun8.postman_collection.json as a Python pipeline: driver-side order accept/load/unload -> freight accounting -> payment, with SQL writes, driven by yun8_sync/config.yaml (api/auth/accounting/payment/database/sql/dianyuntong sections).
This commit is contained in:
@@ -0,0 +1,791 @@
|
||||
"""
|
||||
流程编排器
|
||||
==========
|
||||
将 Postman Collection 中 16 步运输流程 + SQL 写入串联为完整流水线。
|
||||
|
||||
变量传递完全模拟 Postman 的 Collection Variables 机制:
|
||||
步骤 N 的 output 传递给后续步骤作为 input。
|
||||
|
||||
用法:
|
||||
from pipeline import Pipeline
|
||||
pipe = Pipeline(config)
|
||||
# 仅跑司机端 (步骤 1-5)
|
||||
result = pipe.run_driver_flow()
|
||||
# 完整流程 (步骤 1-16 + SQL)
|
||||
result = pipe.run_full_flow(dry_run=False)
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from client import Yun8Client
|
||||
from db import Database
|
||||
|
||||
|
||||
def _gen_code() -> str:
|
||||
"""按现有数据规则生成 code: 32 位小写十六进制 (UUID4 hex 去横线)"""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Pipeline — 变量上下文
|
||||
# ============================================================
|
||||
class Pipeline:
|
||||
"""
|
||||
模拟 Postman Collection Variables 的变量上下文。
|
||||
每个步骤的输出写入 self.vars,后续步骤从 self.vars 读取。
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.client = Yun8Client(config)
|
||||
self.db = Database(config)
|
||||
# 所有步骤间共享的变量池(对应 Postman collectionVariables)
|
||||
self.vars: dict = {}
|
||||
|
||||
# 动态 weight 模式:每次循环随机生成 1~10 之间的小数,per_weight 和 weight 保持一致
|
||||
dp = config.get("driver_params", {})
|
||||
if dp.get("weight_mode", "fixed") == "random":
|
||||
import random as _r
|
||||
val = str(round(_r.uniform(1.0, 10.0), 1))
|
||||
self.client.dp["per_weight"] = val
|
||||
self.client.dp["weight"] = val
|
||||
print(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):
|
||||
print(f"\n{'─' * 50}")
|
||||
print(f"[Step {num:02d}] {name}")
|
||||
print(f"{'─' * 50}")
|
||||
|
||||
def _check_response(self, data: dict, step_name: str):
|
||||
"""检查 API 返回 code 是否为 0"""
|
||||
code = data.get("code")
|
||||
msg = data.get("msg", "")
|
||||
if code is not None and code != 0:
|
||||
print(f" [!] {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"])
|
||||
print(f" mainOrderCode = {result['mainOrderCode']}")
|
||||
print(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, "接单")
|
||||
print(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"])
|
||||
print(f" code = {result['code']}")
|
||||
print(f" vehicleCode = {result['vehicleCode']}")
|
||||
print(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, "装货")
|
||||
print(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, "卸货")
|
||||
print(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 ""
|
||||
print(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():
|
||||
print(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():
|
||||
print(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, "核算提交")
|
||||
print(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", [])
|
||||
print(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, "核算审核通过")
|
||||
print(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, "垫资-财务打款")
|
||||
print(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, "垫资-货主打款")
|
||||
print(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)
|
||||
print(f" payNo = {pay_no}")
|
||||
if not pay_no:
|
||||
print(" [!] 未提取到 payNo,后续 SQL 中 front_seq_no 将为空")
|
||||
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"),
|
||||
)
|
||||
print(f" 共 {len(records)} 条打款记录")
|
||||
if records:
|
||||
print(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)"""
|
||||
print("\n" + "=" * 60)
|
||||
print(" 司机端流程 (Step 1-5)")
|
||||
print("=" * 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")
|
||||
print(f"\n [reset] 重置运单 orderCode={order_code}")
|
||||
reset_code = dp.get("reset_order_detail_code", "")
|
||||
for sql in [
|
||||
f"DELETE FROM tms_order_detail WHERE code = '{reset_code}'",
|
||||
f"DELETE FROM tms_order_detail_ext WHERE order_detail_code = '{reset_code}'",
|
||||
f"DELETE FROM tms_order_detail_abnormal_status WHERE order_detail_code = '{reset_code}'",
|
||||
f"UPDATE tms_order_detail SET code = '{reset_code}' WHERE order_code = '{order_code}'",
|
||||
]:
|
||||
rows = self.db.execute(sql)
|
||||
print(f" SQL: {sql[: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 = (f"UPDATE tms_order_detail SET code = '{reset_code}', "
|
||||
f"fill_time = '2026-07-05 19:30:56', "
|
||||
f"sign_time = '2026-07-05 19:52:30' "
|
||||
f"WHERE order_code = '{self._get('orderCode')}'")
|
||||
rows = self.db.execute(sql)
|
||||
print(f" [reset] 卸货后 UPDATE: {rows} rows")
|
||||
else:
|
||||
print(" [...] 等待 10s ...")
|
||||
time.sleep(10)
|
||||
sql = (f"UPDATE tms_order_detail_ext SET risk_control_status = 1 "
|
||||
f"WHERE order_detail_code = '{self._get('code')}'")
|
||||
rows = self.db.execute(sql)
|
||||
print(f" 卸货后 risk_control_status UPDATE: {rows} rows")
|
||||
|
||||
return self.vars
|
||||
|
||||
def run_accounting_flow(self) -> dict:
|
||||
"""运费核算流程(步骤 6-9)"""
|
||||
print("\n" + "=" * 60)
|
||||
print(" 运费核算流程 (Step 6-9)")
|
||||
print("=" * 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 秒
|
||||
print(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)"""
|
||||
print("\n" + "=" * 60)
|
||||
print(" 审核 & 付款流程 (Step 10-16)")
|
||||
print("=" * 60)
|
||||
|
||||
# 确保前置依赖已获取(独立运行 --stage payment 时 vars 可能为空)
|
||||
self._ensure_code_and_order_code()
|
||||
|
||||
self.step10_query_audit()
|
||||
time.sleep(0.5)
|
||||
|
||||
self.step11_approve_audit()
|
||||
print(" [...] 等待服务端处理审核结果...")
|
||||
|
||||
ac = self.config.get("accounting", {})
|
||||
if not ac.get("execute_after_audit", True):
|
||||
print(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))
|
||||
print(f" latestDriverAmount = {latest_driver}")
|
||||
print(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, "队长分账调整")
|
||||
print(f" 响应: code={adjust.get('code')}, msg={adjust.get('msg')}")
|
||||
# 存入 vars,供 step13 财务打款使用
|
||||
self._set_vars(planDriverPayout=str(_driver_amt),
|
||||
planCaptainPayout=str(_captain_amt))
|
||||
print(f" planDriverPayout = {_driver_amt}")
|
||||
print(f" planCaptainPayout = {_captain_amt}")
|
||||
else:
|
||||
print(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")
|
||||
print(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, "垫资-财务打款")
|
||||
print(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, "垫资-货主打款")
|
||||
print(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"""
|
||||
print("\n" + "=" * 60)
|
||||
print(" 回单文件流程 (Step 14-16 + SQL)")
|
||||
print("=" * 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:
|
||||
print(" [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:
|
||||
print(f"\n [{i+1}/{len(remit_records)}] 跳过:缺 code 字段")
|
||||
ng += 1
|
||||
results.append({"remit_code": remit_code, "status": "skip_no_code"})
|
||||
continue
|
||||
|
||||
print(f"\n [{i+1}/{len(remit_records)}] remitCode = {remit_code}")
|
||||
|
||||
# 16. 查询文件名前缀
|
||||
file_prefix = self.step19_query_file_prefix(remit_code)
|
||||
print(f" filePrefix = {file_prefix}")
|
||||
|
||||
if not file_prefix:
|
||||
print(f" [!] filePrefix 为空,跳过 SQL INSERT")
|
||||
ng += 1
|
||||
results.append({"remit_code": remit_code, "status": "skip_no_prefix"})
|
||||
continue
|
||||
|
||||
file_name = self.db.file_name_template.format(file_prefix=file_prefix)
|
||||
print(f" file_name = {file_name}")
|
||||
print(f" payNo = {pay_no}")
|
||||
|
||||
# SQL INSERT
|
||||
sql = self.db.build_sql(file_prefix, pay_no)
|
||||
if dry_run:
|
||||
print(f" [DRY RUN] SQL:\n{sql}")
|
||||
ok += 1
|
||||
results.append({"remit_code": remit_code, "file_prefix": file_prefix,
|
||||
"status": "dry_run"})
|
||||
else:
|
||||
try:
|
||||
rowcount = self.db.insert(file_prefix, pay_no)
|
||||
print(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:
|
||||
print(f" [FAIL] INSERT 失败: {exc}")
|
||||
ng += 1
|
||||
results.append({"remit_code": remit_code, "file_prefix": file_prefix,
|
||||
"status": "error", "error": str(exc)})
|
||||
|
||||
# 汇总
|
||||
print(f"\n ───────────────────────")
|
||||
print(f" 回单文件流程完成: 成功 {ok}, 失败 {ng}, 总计 {len(remit_records)}")
|
||||
|
||||
# Step 18: 上报运单
|
||||
self._log_step(20, "上报运单 (reportOrderDetail)")
|
||||
try:
|
||||
data_17 = self.client.step20_report_order_detail(
|
||||
codes=self._get("code"),
|
||||
)
|
||||
self._check_response(data_17, "上报运单")
|
||||
print(f" 响应: code={data_17.get('code')}, msg={data_17.get('msg')}")
|
||||
except Exception as exc:
|
||||
print(f" [!] 上报运单失败: {exc}")
|
||||
|
||||
# Step 19: 滇运通审核 (独立 token/host,config 控制开关)
|
||||
if self.client.tp_enabled:
|
||||
self._log_step(21, "滇运通审核 (updateOrderDetailExt)")
|
||||
try:
|
||||
data_19 = self.client.step21_owner_review(
|
||||
order_detail_code=self._get("code"),
|
||||
)
|
||||
self._check_response(data_19, "滇运通审核")
|
||||
print(f" 响应: code={data_19.get('code')}, msg={data_19.get('msg')}")
|
||||
except Exception as exc:
|
||||
print(f" [!] 滇运通审核失败: {exc}")
|
||||
else:
|
||||
print(f"\n [skip] Step 19 滇运通审核 (dianyuntong.enabled=false)")
|
||||
|
||||
return results
|
||||
|
||||
# ==================================================================
|
||||
# 步骤 22-23 : ETC 开票 SQL 写入
|
||||
# ==================================================================
|
||||
def step22_insert_etc_invoice(self, dry_run: bool = False) -> dict:
|
||||
"""
|
||||
22. 写入 ETC 开票信息 (base_etc_invoice)
|
||||
─────────────────
|
||||
INSERT INTO ntocc.base_etc_invoice
|
||||
order_code ← 步骤03的 orderCode
|
||||
push_order_no ← orderCode + "-1"
|
||||
order_detail_code ← 步骤03的 code
|
||||
code 按现有数据规则自动生成 (32 位十六进制)
|
||||
"""
|
||||
self._log_step(22, "ETC 开票信息写入 (base_etc_invoice)")
|
||||
order_code = self._get("orderCode")
|
||||
detail_code = self._get("code")
|
||||
if not order_code or not detail_code:
|
||||
print(" [!] orderCode/code 为空,无法写入 base_etc_invoice")
|
||||
return {"table": "base_etc_invoice", "status": "skip_no_vars"}
|
||||
|
||||
etc_code = _gen_code()
|
||||
push_order_no = f"{order_code}-1"
|
||||
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 ("
|
||||
f"NULL, '{etc_code}', '{order_code}', '{push_order_no}', '{detail_code}', "
|
||||
"'2026-06-04 23:23:51', '9141010055833160XE', '河南省视博电子股份有限公司', '广东古镇东站', '河北成安西站', "
|
||||
"'2026-05-14 19:03:42', 0.02, 0.02, 0.03, 0.03, "
|
||||
"'征税发票', '*', '26417910230600041165', '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', "
|
||||
"'2026-05-14', '2026-05-14', '2026-05-14 19:03:42', "
|
||||
"0, 'etc开票申请2026070604', '冀D8E016', 3, 0, "
|
||||
"NULL, 0, '', '04b7e539c77d4bdbb4fe4e87eaaf41e4', '2026-07-06 09:55:16', '2026-07-06 10:03:26')"
|
||||
)
|
||||
print(f" order_code = {order_code}")
|
||||
print(f" push_order_no = {push_order_no}")
|
||||
print(f" order_detail_code = {detail_code}")
|
||||
print(f" code = {etc_code}")
|
||||
|
||||
if dry_run:
|
||||
print(f" [DRY RUN] SQL:\n{sql}")
|
||||
return {"table": "base_etc_invoice", "status": "dry_run"}
|
||||
try:
|
||||
rows = self.db.execute(sql)
|
||||
print(f" [OK] INSERT 成功 (rows: {rows})")
|
||||
return {"table": "base_etc_invoice", "status": "inserted", "rows": rows}
|
||||
except Exception as exc:
|
||||
print(f" [FAIL] INSERT 失败: {exc}")
|
||||
return {"table": "base_etc_invoice", "status": "error", "error": str(exc)}
|
||||
|
||||
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:
|
||||
print(" [!] 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 = (
|
||||
"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 ("
|
||||
f"NULL, '{etc_code}', '{push_order_no}', '{order_code}', '{detail_code}', "
|
||||
"'2026-07-07 15:08:25', '2026-07-07 15:08:28', 2026, 6, 1, "
|
||||
"NULL, '11111', '11111', 0, '2026-07-07 11:40:21', "
|
||||
"2, '2026-07-07 15:17:02', '2', 2, '2026-07-07 15:11:04', 'dsdsad', "
|
||||
"0, NULL, NULL, '2026-07-07 11:09:17', '2026-07-07 15:15:18')"
|
||||
)
|
||||
print(f" order_no = {order_code}")
|
||||
print(f" push_order_no = {push_order_no}")
|
||||
print(f" order_detail_code = {detail_code}")
|
||||
print(f" code = {etc_code}")
|
||||
|
||||
if dry_run:
|
||||
print(f" [DRY RUN] SQL:\n{sql}")
|
||||
return {"table": "tms_order_etc_push", "status": "dry_run"}
|
||||
try:
|
||||
rows = self.db.execute(sql)
|
||||
print(f" [OK] INSERT 成功 (rows: {rows})")
|
||||
return {"table": "tms_order_etc_push", "status": "inserted", "rows": rows}
|
||||
except Exception as exc:
|
||||
print(f" [FAIL] INSERT 失败: {exc}")
|
||||
return {"table": "tms_order_etc_push", "status": "error", "error": str(exc)}
|
||||
|
||||
def run_etc_invoice_flow(self, dry_run: bool = False) -> list[dict]:
|
||||
"""ETC 开票 SQL 写入(步骤 22-23)"""
|
||||
print("\n" + "=" * 60)
|
||||
print(" ETC 开票 SQL 写入 (Step 22-23)")
|
||||
print("=" * 60)
|
||||
|
||||
# 确保 orderCode / code 已就绪(独立运行 --stage receipt 时需要)
|
||||
self._ensure_code_and_order_code()
|
||||
|
||||
results = [
|
||||
self.step22_insert_etc_invoice(dry_run=dry_run),
|
||||
self.step23_insert_etc_push(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"):
|
||||
print(f" (复用已有 code={self._get('code')}, orderCode={self._get('orderCode')})")
|
||||
return
|
||||
|
||||
print(" [*] 自动补充前置数据...")
|
||||
|
||||
# 方式 A: 通过 driver 端 step03(status=waitTrans)
|
||||
driver_auth = self.client.driver.headers.get("authorization", "")
|
||||
if driver_auth:
|
||||
try:
|
||||
result = self.client.step03_query_pending_transport()
|
||||
self._set_vars(
|
||||
code=result["code"],
|
||||
vehicleCode=result["vehicleCode"],
|
||||
orderCode=result["orderCode"],
|
||||
)
|
||||
print(f" [driver] code={self._get('code')}, orderCode={self._get('orderCode')}")
|
||||
except Exception:
|
||||
print(" 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"):
|
||||
print(f" [admin status={status_val}] code={self._get('code')}, orderCode={self._get('orderCode')}")
|
||||
break
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
if not self._get("code"):
|
||||
print(" [!] 无法自动获取 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)
|
||||
print(f" deliveryCashFee={self._get('deliveryCashFee')}, driverActualFreight={self._get('driverActualFreight')}")
|
||||
except Exception as e:
|
||||
print(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)
|
||||
print(f" Driver auth: {driver_token[:30]}...")
|
||||
print(f" Admin auth: {admin_token[:30]}...")
|
||||
Reference in New Issue
Block a user