Files
Yb-QaAutomationHub/yun8_sync/pipeline.py
T
xst 73b93bdf0c feat(yun8_sync): add API pipeline automating full freight flow (18 interfaces + SQL)
Implements the "运输流程" collection from yun8.postman_collection.json as a
Python pipeline: driver-side order accept/load/unload -> freight accounting
-> payment, with SQL writes, driven by yun8_sync/config.yaml
(api/auth/accounting/payment/database/sql/dianyuntong sections).
2026-08-17 14:30:40 +08:00

792 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
流程编排器
==========
将 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]}...")