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:
xst
2026-08-17 14:30:40 +08:00
parent 18890c0437
commit 73b93bdf0c
7 changed files with 2357 additions and 0 deletions
+791
View File
@@ -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]}...")