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,240 @@
|
||||
# yun8 运输流程完整自动化
|
||||
|
||||
将 `yun8.postman_collection.json` 中 **"运输流程"** 全部 18 个接口 + SQL 写入实现为 Python 流水线。
|
||||
|
||||
---
|
||||
|
||||
## 流程概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 阶段 A: 司机端 (Step 01-05) │
|
||||
│ 查询货源 → 接单 → 查询待运输单 → 装货 → 卸货 │
|
||||
│ 变量: mainOrderCode → code / vehicleCode / orderCode │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 阶段 B: 运费核算 (Step 06-09) │
|
||||
│ 查询运单 → 运单详情 → 核算试算 → 核算提交 │
|
||||
│ 变量: 30+ 核算字段 + ticketImageUrls │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 阶段 C: 审核 & 付款 (Step 10-14) │
|
||||
│ 查询审核 → 审核通过 → 油卡充值 → 司机打款 → 货主打款 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 阶段 D: 回单文件 + SQL (Step 15-18) │
|
||||
│ 运单上传(payNo) → 打款记录 → 文件名前缀 → INSERT → 上报运单 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 阶段 E: ETC 开票 SQL (Step 22-23) │
|
||||
│ 写入 base_etc_invoice → 写入 tms_order_etc_push │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 上下游变量传递
|
||||
|
||||
| 步骤 | 产出变量 | 消费步骤 |
|
||||
|------|---------|---------|
|
||||
| 01 查询货源 | `mainOrderCode`, `residueNumber` | 02, 09 |
|
||||
| 03 查询待运输单 | `code`, `vehicleCode`, `orderCode` | 04-18 |
|
||||
| 06 运费核算 | 30+ 字段 (branchCode, oilFee, ...) | 09, 12 |
|
||||
| 07 运单详情 | `deliveryCashFee`, `driverActualFreight`, ... | 09, 13 |
|
||||
| 08 核算试算 | `ticketImageUrls` | 09 |
|
||||
| 15 运单上传 | `payNo` (sendMsg 正则提取) | SQL |
|
||||
| 16 打款记录 | 所有 `code` | 17 |
|
||||
| 17 文件名前缀 | `filePrefix` | SQL |
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
cd yun8_sync
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
编辑 `config.yaml`:
|
||||
|
||||
### Token(二选一)
|
||||
```yaml
|
||||
auth:
|
||||
driver_token: "Bearer xxx" # 写在配置文件
|
||||
admin_token: "Bearer yyy"
|
||||
```
|
||||
或命令行传入 `--auth-driver` / `--auth-admin`(命令行优先)。
|
||||
|
||||
### 重量模式
|
||||
```yaml
|
||||
driver_params:
|
||||
weight_mode: "fixed" # fixed=固定值 / random=每次随机 1.0~10.0
|
||||
per_weight: "10" # fixed 模式:接单单位重量
|
||||
weight: "10" # fixed 模式:装货/卸货重量
|
||||
```
|
||||
- **fixed**: 使用配置的固定值
|
||||
- **random**: 每次循环随机生成 1.0~10.0 小数(一位小数),`per_weight` 和 `weight` 自动保持一致
|
||||
|
||||
### 核算/付款参数
|
||||
```yaml
|
||||
accounting:
|
||||
branch_code: "49adb23841asw3118yunnanchenggang"
|
||||
business_type_code: "1003999"
|
||||
payee_code: "16d0c260f7e34967b36113c770e03486"
|
||||
shipment_phone: "15198908093"
|
||||
shipment_party_code: "ce8b73b38cf6450d84740cf23aa4ed52"
|
||||
settlement_type: "2"
|
||||
|
||||
payment:
|
||||
pay_types: "3,4,5,9,10,11"
|
||||
pay_platform: "3"
|
||||
sms_code: "1234"
|
||||
phone: "13888888888"
|
||||
pay_type: "arpa_2"
|
||||
```
|
||||
> 注:`branchCode` / `businessTypeCode` / `payeeCode` / `shipmentPhone` / `shipmentPartyCode` 会优先从 step06 返回值中读取,配置仅作兜底。
|
||||
|
||||
### 数据库
|
||||
```yaml
|
||||
database:
|
||||
host: "36.137.133.24"
|
||||
port: 3306
|
||||
user: "root"
|
||||
password: "Rgxi3!kD9jjXpgzx"
|
||||
database: "ntocc"
|
||||
charset: "utf8mb4"
|
||||
```
|
||||
|
||||
### SQL 模板
|
||||
```yaml
|
||||
sql:
|
||||
file_name_template: "{file_prefix}_1_signed.pdf"
|
||||
template: |
|
||||
INSERT INTO `ntocc`.`base_gd_mft_file` (...) VALUES (...)
|
||||
```
|
||||
占位符 `{file_prefix}` 和 `{pay_no}` 由流水线自动填充。
|
||||
|
||||
---
|
||||
|
||||
## 使用
|
||||
|
||||
### 命令行参数
|
||||
|
||||
| 参数 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| `--config` | 配置文件路径 | `./config.yaml` |
|
||||
| `--auth-driver` | 司机端 Bearer Token | 读取 config |
|
||||
| `--auth-admin` | 管理端 Bearer Token | 读取 config |
|
||||
| `--stage` | 运行阶段: `all` / `driver` / `accounting` / `payment` / `receipt` | `all` |
|
||||
| `--dry-run` | 仅预览 SQL,不写库 | `False` |
|
||||
| `--repeat` | 循环执行次数 | `1` |
|
||||
| `--delay` | 每次循环间隔秒数 | `0` |
|
||||
|
||||
### 分阶段运行
|
||||
|
||||
```bash
|
||||
# 完整流程(18步)
|
||||
python main.py --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
|
||||
# 仅跑司机端 (Step 01-05)
|
||||
python main.py --stage driver --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
|
||||
# 仅跑运费核算 (Step 06-09)
|
||||
python main.py --stage accounting --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
|
||||
# 仅跑审核付款 (Step 10-14)
|
||||
python main.py --stage payment --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
|
||||
# 仅跑回单+SQL+上报 (Step 15-18)
|
||||
python main.py --stage receipt --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
```
|
||||
|
||||
> 独立运行 `payment` 或 `receipt` 阶段时,会自动从 admin 端补全前置数据(code/orderCode),无需手动指定。
|
||||
|
||||
### 循环执行
|
||||
|
||||
```bash
|
||||
# 完整流程循环 10 次,每次间隔 5 秒
|
||||
python main.py --repeat 10 --delay 5
|
||||
|
||||
# 仅 receipt 阶段循环 100 次
|
||||
python main.py --stage receipt --repeat 100 --delay 2
|
||||
|
||||
# 配合 random 重量模式,每次自动换新重量
|
||||
python main.py --repeat 50 --delay 3
|
||||
```
|
||||
|
||||
每次循环独立创建 Pipeline,变量上下文重置。某次失败不中断后续循环,`Ctrl+C` 随时停止。
|
||||
|
||||
### 预览模式
|
||||
|
||||
```bash
|
||||
# 不写数据库,仅打印 SQL
|
||||
python main.py --dry-run
|
||||
|
||||
# 循环预览
|
||||
python main.py --repeat 5 --dry-run
|
||||
```
|
||||
|
||||
### Token 从配置文件读取(无需命令行)
|
||||
|
||||
在 `config.yaml` 中填写 `auth.driver_token` 和 `auth.admin_token`,然后:
|
||||
|
||||
```bash
|
||||
python main.py --repeat 100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障处理
|
||||
|
||||
| 现象 | 处理方式 |
|
||||
|------|---------|
|
||||
| WAF 拦截 (SQL注入误判) | 已内置:直连服务器 + 手工 query string 编码 |
|
||||
| "运单正被处理中" | 已内置:自动重试 5 次,间隔递增 5~13s |
|
||||
| "货运单不存在" | Token 过期或运单状态不对,检查 admin token |
|
||||
| 独立阶段提示 code 为空 | 已内置:自动从 admin 端补全 |
|
||||
| GBK 编码报错 | 已修复:全部输出使用纯 ASCII 字符 |
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
yun8_sync/
|
||||
├── config.yaml # 配置文件 (API / Auth / 重量 / 核算 / 付款 / DB / SQL)
|
||||
├── client.py # HTTP 客户端 (18 个接口 + Pre/Post 脚本逻辑)
|
||||
├── pipeline.py # 流程编排 (Postman 变量上下文 + 步骤串联 + 重试)
|
||||
├── db.py # 数据库操作 (INSERT)
|
||||
├── main.py # CLI 入口 (分阶段 + 循环 + dry-run)
|
||||
├── requirements.txt # Python 依赖
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口对照表
|
||||
|
||||
| Step | 名称 | 方法 | 路径 | Auth |
|
||||
|------|------|------|------|------|
|
||||
| 01 | 查询货源 | GET | `/ntocc-tms-api/order/orderHallList` | Driver |
|
||||
| 02 | 接单 | POST | `/ntocc-tms-api/orderDetail/receive` | Driver |
|
||||
| 03 | 查询待运输单 | GET | `/ntocc-tms-api/orderDetail` | Driver |
|
||||
| 04 | 装货 | PUT | `/ntocc-tms-api/orderDetail/fill` | Driver |
|
||||
| 05 | 卸货 | PUT | `/ntocc-tms-api/orderDetail/uploadTicketImage` | Driver |
|
||||
| 06 | 运费核算查询运单 | GET | `/ntocc-tms-api/orderDetail/orderDetailHandlePageList` | Admin |
|
||||
| 07 | 查询运单详情 | POST | `/ntocc-tms-api/orderDetail/getOrderDetailExpensesParamList` | Admin |
|
||||
| 08 | 核算试算 | PUT | `/ntocc-tms-api/orderDetail/calculateOrderDetail` | Admin |
|
||||
| 09 | 核算提交 | PUT | `/ntocc-tms-api/orderDetail/orderHandleEditSubmitHandle` | Admin |
|
||||
| 10 | 查询运单审核 | GET | `/ntocc-basic-api/dytBusiness/orderDetail` | Admin |
|
||||
| 11 | 核算审核通过 | POST | `/ntocc-tms-api/orderDetail/auditOrderDetail` | Admin |
|
||||
| 12 | 单油卡充值 | POST | `/ntocc-tms-api/finance/singleOil` | Admin |
|
||||
| 13 | 垫资-财务打款 | POST | `/ntocc-tms-api/arpaTransfer/payToDriver` | Admin |
|
||||
| 14 | 垫资-货主打款 | POST | `/ntocc-tms-api/orderDetail/applicationForPayment` | Admin |
|
||||
| 15 | 查询运单上传 | GET | `/ntocc-basic-api/dytBusiness/orderDetail?status=5` | Admin |
|
||||
| 16 | 查询打款记录 | GET | `/ntocc-tms-api/remitDefinite` | Admin |
|
||||
| 17 | 查询文件名前缀 | GET | `/ntocc-tms-api/centralizedPayment/downLoadElectronicReceipt` | Admin |
|
||||
| 18 | 上报运单 | POST | `/ntocc-basic-api/dytBusiness/reportOrderDetail` | Admin |
|
||||
| — | SQL INSERT | — | `ntocc.base_gd_mft_file` | — |
|
||||
| 22 | ETC 开票信息写入 | SQL INSERT | — | `ntocc.base_etc_invoice` | — |
|
||||
| 23 | ETC 推送记录写入 | SQL INSERT | — | `ntocc.tms_order_etc_push` | — |
|
||||
|
||||
> 步骤 22/23 的 `order_code`/`order_no` 取步骤 03 的 `orderCode`,`push_order_no` 为其后追加 `-1`,`order_detail_code` 取步骤 03 的 `code`,两个表的 `code` 按现有数据规则(32 位十六进制)自动生成,随 receipt 阶段执行(`--dry-run` 仅预览不写库)。
|
||||
@@ -0,0 +1,965 @@
|
||||
"""
|
||||
yun8 API 客户端
|
||||
===============
|
||||
封装 Postman Collection 中运输流程的全部 16 个接口。
|
||||
Header 构造、Pre-request Script 逻辑、Test Script 变量提取全部在此实现。
|
||||
|
||||
接口列表:
|
||||
[Driver 端 — 5个]
|
||||
01. 查询货源 GET /ntocc-tms-api/order/orderHallList
|
||||
02. 接单 POST /ntocc-tms-api/orderDetail/receive
|
||||
03. 查询待运输单 GET /ntocc-tms-api/orderDetail
|
||||
04. 装货 PUT /ntocc-tms-api/orderDetail/fill
|
||||
05. 卸货 PUT /ntocc-tms-api/orderDetail/uploadTicketImage
|
||||
|
||||
[Admin 端 — 11个]
|
||||
06. 运费核算查询运单 GET /ntocc-tms-api/orderDetail/orderDetailHandlePageList
|
||||
07. 查询运单详情 POST /ntocc-tms-api/orderDetail/getOrderDetailExpensesParamList
|
||||
08. 核算试算 PUT /ntocc-tms-api/orderDetail/calculateOrderDetail
|
||||
09. 核算提交 PUT /ntocc-tms-api/orderDetail/orderHandleEditSubmitHandle
|
||||
10. 查询运单审核 GET /ntocc-tms-api/orderDetail?status=6&isSettle=1
|
||||
11. 核算审核通过 POST /ntocc-tms-api/orderDetail/auditOrderDetail
|
||||
12. 垫资-财务打款 POST /ntocc-tms-api/arpaTransfer/payToDriver
|
||||
13. 垫资-货主打款 POST /ntocc-tms-api/orderDetail/applicationForPayment
|
||||
14. 查询运单上传 GET /ntocc-basic-api/dytBusiness/orderDetail?status=5
|
||||
15. 查询打款记录 GET /ntocc-tms-api/remitDefinite
|
||||
16. 查询文件名前缀 GET /ntocc-tms-api/centralizedPayment/downLoadElectronicReceipt
|
||||
"""
|
||||
|
||||
import math
|
||||
import re
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Pre-request Script 逻辑:生成当前时间戳
|
||||
# ============================================================
|
||||
def _current_time_str() -> str:
|
||||
"""Postman pre-request: 生成 'YYYY-MM-DD HH:MM:SS' 格式的当前时间"""
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _debug_full_url(url: str):
|
||||
"""调试:打印 URL 中 ticketImageUrls / unloadImgaes 参数片段"""
|
||||
import re as _re
|
||||
# 找 ticketImageUrls[]= 后面的值(到下一个 & 或行尾)
|
||||
m = _re.search(r'ticketImageUrls%5B%5D=([^&]{0,300})', url)
|
||||
if m:
|
||||
print(f" [DEBUG] ticketImageUrls[] = {m.group(1)}")
|
||||
m2 = _re.search(r'unloadImgaes=([^&]{0,300})', url)
|
||||
if m2:
|
||||
print(f" [DEBUG] unloadImgaes = {m2.group(1)}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Yun8Client
|
||||
# ============================================================
|
||||
class Yun8Client:
|
||||
"""封装 yun8 全部 API 请求"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
api = config["api"]
|
||||
self.base_url = api["base_url"].rstrip("/")
|
||||
self.verify_ssl = api.get("verify_ssl", False)
|
||||
self.common = api.get("common_headers", {})
|
||||
|
||||
# ---- Driver Session ----
|
||||
self.driver = requests.Session()
|
||||
drv = api.get("driver", {})
|
||||
self._init_session(self.driver, drv)
|
||||
self.driver.headers["host"] = self.common.get("host", "")
|
||||
self.driver.headers["accept-language"] = self.common.get("accept-language", "")
|
||||
self.driver.headers["accept-encoding"] = self.common.get("accept-encoding", "")
|
||||
|
||||
# ---- Admin Session ----
|
||||
self.admin = requests.Session()
|
||||
adm = api.get("admin", {})
|
||||
self._init_session(self.admin, adm)
|
||||
self.admin.headers["host"] = self.common.get("host", "")
|
||||
self.admin.headers["accept-language"] = self.common.get("accept-language", "")
|
||||
self.admin.headers["accept-encoding"] = self.common.get("accept-encoding", "")
|
||||
|
||||
# ---- Third-party Session (滇运通,独立 token/host) ----
|
||||
tp = config.get("dianyuntong", {})
|
||||
self.tp_enabled = tp.get("enabled", False)
|
||||
self.tp = None
|
||||
if self.tp_enabled:
|
||||
self.tp = requests.Session()
|
||||
tp_headers = {
|
||||
"host": "yndyt.cxtech.vip",
|
||||
"authorization": tp.get("token", ""),
|
||||
"deviceid": "0000000000",
|
||||
"secret": "GouldAMap=d58bb5f7da1aa1d6b78741ded2450f2d",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0",
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"origin": tp.get("base_url", ""),
|
||||
"referer": f"{tp.get('base_url', '')}/admin/",
|
||||
"cookie": tp.get("cookie", ""),
|
||||
}
|
||||
for k, v in tp_headers.items():
|
||||
if v:
|
||||
self.tp.headers[k] = v
|
||||
self.tp.trust_env = False
|
||||
self.tp.proxies = {"http": None, "https": None}
|
||||
self.tp_base = tp.get("base_url", "").rstrip("/")
|
||||
|
||||
# 快捷引用
|
||||
self.dp = config.get("driver_params", {})
|
||||
self.ac = config.get("accounting", {})
|
||||
self.pm = config.get("payment", {})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _init_session(self, sess: requests.Session, cfg: dict):
|
||||
for k, v in cfg.items():
|
||||
if v:
|
||||
sess.headers[k] = v
|
||||
# 禁用系统代理,直连服务器(避免本地代理修改请求导致 WAF 误判 SQL 注入)
|
||||
sess.trust_env = False
|
||||
sess.proxies = {"http": None, "https": None}
|
||||
|
||||
def set_driver_auth(self, token: str):
|
||||
self.driver.headers["authorization"] = token
|
||||
|
||||
def set_admin_auth(self, token: str):
|
||||
self.admin.headers["authorization"] = token
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _get(self, sess: requests.Session, path: str, params: dict = None) -> dict:
|
||||
resp = sess.get(f"{self.base_url}{path}", params=params, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _post(self, sess: requests.Session, path: str, params: dict = None, data: dict = None) -> dict:
|
||||
resp = sess.post(f"{self.base_url}{path}", params=params, data=data, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _put(self, sess: requests.Session, path: str, params: dict = None, data: dict = None) -> dict:
|
||||
resp = sess.put(f"{self.base_url}{path}", params=params, data=data, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _put_with_query_string(self, sess: requests.Session, path: str,
|
||||
params: dict) -> dict:
|
||||
"""
|
||||
PUT 请求,手工构建 query string。
|
||||
|
||||
与 _put(params=...) 的区别:query string 由 urlencode 一次性编码后
|
||||
拼接为完整 URL 再发给 requests,避免 requests 内部再次编码导致
|
||||
URL 值中已有的 %XX(如 AWS Signature 的 %2B)被二次转义为 %25XX,
|
||||
触发服务端 WAF 的 SQL 注入误判。
|
||||
"""
|
||||
qs = urllib.parse.urlencode(params, doseq=True,
|
||||
quote_via=urllib.parse.quote)
|
||||
full_url = f"{self.base_url}{path}?{qs}"
|
||||
|
||||
# DEBUG: 打印实际发出的 URL(截取 ticketImageUrls 参数附近供对比)
|
||||
_debug_full_url(full_url)
|
||||
|
||||
resp = sess.put(full_url, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _post_with_query_string(self, sess: requests.Session, path: str,
|
||||
params: dict) -> dict:
|
||||
"""
|
||||
POST 请求,手工构建 query string(与 _put_with_query_string 同理)。
|
||||
避免 requests 对逗号、URL 等字符做不必要的编码。
|
||||
"""
|
||||
qs = urllib.parse.urlencode(params, doseq=True,
|
||||
quote_via=urllib.parse.quote)
|
||||
full_url = f"{self.base_url}{path}?{qs}"
|
||||
resp = sess.post(full_url, verify=self.verify_ssl)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# ==================================================================
|
||||
# Step 01: 查询货源
|
||||
# ==================================================================
|
||||
def step01_query_goods(self, page_size: int = 10, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("mainOrderCode", jsonData.data.records[0].code);
|
||||
pm.collectionVariables.set("residueNumber", jsonData.data.records[0].lastWeight);
|
||||
|
||||
Returns: {"mainOrderCode": str, "residueNumber": str}
|
||||
"""
|
||||
params = {
|
||||
"isFromOrderHall": "1",
|
||||
"sortType": "desc",
|
||||
"sortField": "gmt_created",
|
||||
"pageSize": page_size,
|
||||
"pageNum": page_num,
|
||||
}
|
||||
data = self._get(self.driver, "/ntocc-tms-api/order/orderHallList", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
if not records:
|
||||
raise RuntimeError("查询货源: 无记录返回")
|
||||
|
||||
r = records[0]
|
||||
return {
|
||||
"mainOrderCode": r.get("code", ""),
|
||||
"residueNumber": str(r.get("lastWeight", "")),
|
||||
"shipperLatitude": str(r.get("shipperLatitude", "")),
|
||||
"shipperLongitude": str(r.get("shipperLongitude", "")),
|
||||
"consigneeLatitude": str(r.get("consigneeLatitude", "")),
|
||||
"consigneeLongitude": str(r.get("consigneeLongitude", "")),
|
||||
}
|
||||
|
||||
# ==================================================================
|
||||
# Step 02: 接单
|
||||
# ==================================================================
|
||||
def step02_accept_order(self, main_order_code: str, residue_number: str,
|
||||
message_code: str = "",
|
||||
latitude: str = None, longitude: str = None) -> dict:
|
||||
"""
|
||||
Postman Body (urlencoded):
|
||||
residueNumber / perWeight / latitude / messageCode / mainOrderCode / longitude
|
||||
latitude/longitude 优先取货源接口的 shipperLat/shipperLng,没有则用 config
|
||||
"""
|
||||
data = {
|
||||
"residueNumber": residue_number,
|
||||
"perWeight": self.dp.get("per_weight", "1"),
|
||||
"latitude": latitude or self.dp.get("latitude", ""),
|
||||
"messageCode": message_code,
|
||||
"mainOrderCode": main_order_code,
|
||||
"longitude": longitude or self.dp.get("longitude", ""),
|
||||
}
|
||||
return self._post(self.driver, "/ntocc-tms-api/orderDetail/receive", data=data)
|
||||
|
||||
# ==================================================================
|
||||
# Step 03: 查询待运输单
|
||||
# ==================================================================
|
||||
def step03_query_pending_transport(self, page_size: int = 10, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("code", jsonData.data.records[0].code);
|
||||
pm.collectionVariables.set("vehicleCode", jsonData.data.records[0].vehicleCode);
|
||||
pm.collectionVariables.set("orderCode", jsonData.data.records[0].orderCode);
|
||||
|
||||
Returns: {"code": str, "vehicleCode": str, "orderCode": str}
|
||||
"""
|
||||
params = {
|
||||
"assignmentStatus": "waitTrans",
|
||||
"placeOfReceipt": "",
|
||||
"gmtCreatedEnd": "",
|
||||
"gmtCreatedStart": "",
|
||||
"pageSize": page_size,
|
||||
"placeOfShipment": "",
|
||||
"pageNum": page_num,
|
||||
"shipmentName": "",
|
||||
"status": "1",
|
||||
"vehicleLicense": "",
|
||||
}
|
||||
data = self._get(self.driver, "/ntocc-tms-api/orderDetail", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
if not records:
|
||||
raise RuntimeError("查询待运输单: 无记录返回")
|
||||
|
||||
r = records[0]
|
||||
return {
|
||||
"code": r.get("code", ""),
|
||||
"vehicleCode": r.get("vehicleCode", ""),
|
||||
"orderCode": r.get("orderCode", ""),
|
||||
}
|
||||
|
||||
# ==================================================================
|
||||
# Step 04: 装货
|
||||
# ==================================================================
|
||||
def step04_load_goods(self, code: str, vehicle_code: str,
|
||||
img_url: str = "", quantity: str = "",
|
||||
weight: str = None,
|
||||
latitude: str = None, longitude: str = None) -> dict:
|
||||
"""
|
||||
Postman Pre-request Script: 生成 currentTime
|
||||
Body: imgUrl / orderType / quantity / code / latitude / weight / vehicleCode / time / longitude
|
||||
latitude/longitude 优先取货源接口的 shipperLat/shipperLng
|
||||
"""
|
||||
if weight is None:
|
||||
weight = self.dp.get("weight", "1")
|
||||
body = {
|
||||
"imgUrl": img_url or self.dp.get("load_img_url", ""),
|
||||
"orderType": "1",
|
||||
"quantity": quantity,
|
||||
"code": code,
|
||||
"latitude": latitude or self.dp.get("latitude", ""),
|
||||
"weight": weight,
|
||||
"vehicleCode": vehicle_code,
|
||||
"time": _current_time_str(),
|
||||
"longitude": longitude or self.dp.get("longitude", ""),
|
||||
}
|
||||
return self._put(self.driver, "/ntocc-tms-api/orderDetail/fill", data=body)
|
||||
|
||||
# ==================================================================
|
||||
# Step 05: 卸货
|
||||
# ==================================================================
|
||||
def step05_unload_goods(self, code: str, img_url: str = "",
|
||||
quantity: str = "", weight: str = None,
|
||||
latitude: str = None, longitude: str = None) -> dict:
|
||||
"""
|
||||
Postman Pre-request Script: 生成 currentTime
|
||||
Body: imgUrl / code / quantity / latitude / weight / time / longitude
|
||||
latitude/longitude 优先取货源接口的 consigneeLat/consigneeLng
|
||||
"""
|
||||
if weight is None:
|
||||
weight = self.dp.get("weight", "1")
|
||||
body = {
|
||||
"imgUrl": img_url or self.dp.get("unload_img_url", ""),
|
||||
"code": code,
|
||||
"quantity": quantity,
|
||||
"latitude": latitude or self.dp.get("latitude", ""),
|
||||
"weight": weight,
|
||||
"time": _current_time_str(),
|
||||
"longitude": longitude or self.dp.get("longitude", ""),
|
||||
}
|
||||
return self._put(self.driver, "/ntocc-tms-api/orderDetail/uploadTicketImage", data=body)
|
||||
|
||||
# ==================================================================
|
||||
# Step 06: 运费核算查询运单
|
||||
# ==================================================================
|
||||
def step06_query_for_accounting(self, order_code: str,
|
||||
page_size: int = 20, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script: 设置 30+ 个 collection variables
|
||||
从 data.records[0] 中提取所有字段
|
||||
|
||||
Returns: dict with all fields from records[0]
|
||||
"""
|
||||
params = {
|
||||
"status": "3",
|
||||
"lastLoadingTimeRange": "",
|
||||
"fillTimeRange": "",
|
||||
"lastUnloadingTimeRange": "",
|
||||
"signTimeRange": "",
|
||||
"gmtModifiedRange": "",
|
||||
"rangeValue_start_1": "",
|
||||
"rangeValue_end_1": "",
|
||||
"rangeValue_start_2": "",
|
||||
"rangeValue_end_2": "",
|
||||
"rangeValue_start_3": "",
|
||||
"rangeValue_end_3": "",
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"gmtCreatedStart": "",
|
||||
"gmtCreatedEnd": "",
|
||||
"lastUnloadingTimeStart": "",
|
||||
"lastUnloadingTimeEnd": "",
|
||||
"lastLoadingTimeStart": "",
|
||||
"lastLoadingTimeEnd": "",
|
||||
"gmtModifiedStart": "",
|
||||
"gmtModifiedEnd": "",
|
||||
"fillTimeStart": "",
|
||||
"fillTimeEnd": "",
|
||||
"signTimeStart": "",
|
||||
"signTimeEnd": "",
|
||||
"ordCreatedByName": "",
|
||||
"isUnloadingConfirm": "1",
|
||||
"isCannotAccounting": "0",
|
||||
"riskControlStatus": "",
|
||||
"riskControlReason": "",
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"orderCode": order_code,
|
||||
}
|
||||
data = self._get(self.admin, "/ntocc-tms-api/orderDetail/orderDetailHandlePageList", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
if not records:
|
||||
raise RuntimeError("运费核算查询运单: 无记录返回")
|
||||
return records[0]
|
||||
|
||||
# ==================================================================
|
||||
# Step 07: 查询运单详情
|
||||
# ==================================================================
|
||||
def step07_query_order_expenses(self, code: str) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("canWastage", ...);
|
||||
pm.collectionVariables.set("deliveryCashFee", ...);
|
||||
pm.collectionVariables.set("deliveryFeeDeserved", ...);
|
||||
pm.collectionVariables.set("deliveryFeePractical", ...);
|
||||
pm.collectionVariables.set("driverActualCash", ...);
|
||||
pm.collectionVariables.set("driverActualFreight", ...);
|
||||
|
||||
Returns: {canWastage, deliveryCashFee, deliveryFeeDeserved,
|
||||
deliveryFeePractical, driverActualCash, driverActualFreight}
|
||||
"""
|
||||
params = {"codes": code}
|
||||
data = self._post(self.admin,
|
||||
"/ntocc-tms-api/orderDetail/getOrderDetailExpensesParamList",
|
||||
params=params)
|
||||
items = data.get("data", [])
|
||||
if not items:
|
||||
raise RuntimeError("查询运单详情: 无数据返回")
|
||||
r = items[0]
|
||||
return {
|
||||
"canWastage": str(r.get("canWastage", "")),
|
||||
"deliveryCashFee": str(r.get("deliveryCashFee", "")),
|
||||
"deliveryFeeDeserved": str(r.get("deliveryFeeDeserved", "")),
|
||||
"deliveryFeePractical": str(r.get("deliveryFeePractical", "")),
|
||||
"driverActualCash": str(r.get("driverActualCash", "")),
|
||||
"driverActualFreight": str(r.get("driverActualFreight", "")),
|
||||
}
|
||||
|
||||
# ==================================================================
|
||||
# Step 08: 核算试算 (calculateOrderDetail)
|
||||
# ==================================================================
|
||||
def step08_calculate_order_detail(self, code: str) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("ticketImageUrls[]", jsonData.data.ticketImageUrls);
|
||||
|
||||
Returns: dict with ticketImageUrls + 其他 step08 返回的核算数据 (taxPrice 等)
|
||||
"""
|
||||
params = {"code": code, "isFirst": "1"}
|
||||
data = self._put(self.admin, "/ntocc-tms-api/orderDetail/calculateOrderDetail", params=params)
|
||||
inner = data.get("data", {}) or {}
|
||||
ticket_urls = inner.get("ticketImageUrls", "")
|
||||
if isinstance(ticket_urls, list):
|
||||
ticket_urls = ticket_urls[0] if ticket_urls else ""
|
||||
# 返回所有有用字段,供 pipeline 存入 vars
|
||||
result = {"ticketImageUrls": str(ticket_urls)}
|
||||
for key in ("taxPrice", "taxFee", "taxPayFee", "taxPoint",
|
||||
"price", "realPrice", "shipmentActualCash",
|
||||
"shipmentActualFreight"):
|
||||
if key in inner:
|
||||
result[key] = str(inner[key])
|
||||
return result
|
||||
|
||||
# ==================================================================
|
||||
# Step 09: 核算提交
|
||||
# ==================================================================
|
||||
def step09_submit_accounting(self, accounting_vars: dict) -> dict:
|
||||
"""
|
||||
提交核算,参数来自前面步骤累积的所有变量。
|
||||
accounting_vars 应包含 steps 03/06/07/08 的全部字段。
|
||||
优先从 step06 返回值取值,取不到再用 config.yaml 默认值。
|
||||
"""
|
||||
av = accounting_vars
|
||||
# 便捷取值:vars 优先 → config 兜底
|
||||
def _v(key, cfg_key, default=""):
|
||||
return av.get(key) or self.ac.get(cfg_key, default)
|
||||
|
||||
# Build the massive query string
|
||||
params = {
|
||||
"abnormalFee": "0",
|
||||
"accountCheckAuthStatus": "0",
|
||||
"accountCheckInfoAuthStatus": "0",
|
||||
"accountCheckUpAuthStatus": "0",
|
||||
"accountStatus": "0",
|
||||
"allTrans": "0",
|
||||
"allotAmount": "0",
|
||||
"allotOil": "0",
|
||||
"allotTaxAmount": "0",
|
||||
"appSendLoadLocation": "0",
|
||||
"appSendUnloadLocation": "0",
|
||||
"applyStatus": "0",
|
||||
"branchCode": _v("branchCode", "branch_code"),
|
||||
"businessTypeCode": _v("businessTypeCode", "business_type_code"),
|
||||
"canWastage": av.get("canWastage", ""),
|
||||
"cancelTrans": "0",
|
||||
"cancelType": "0",
|
||||
"captainSplitCurrentVersion": "0",
|
||||
"captainSplitMode": self.ac.get("captain_split_mode") or av.get("captainSplitMode", ""),
|
||||
"cashDeposit": "0",
|
||||
"coalName": "",
|
||||
"coalPrice": av.get("coalPrice", ""),
|
||||
"coalType": av.get("coalType", ""),
|
||||
"coalTypeName": av.get("coalTypeName", ""),
|
||||
"coalUnit": "0",
|
||||
"coalUnitName": av.get("coalUnitName", ""),
|
||||
"code": av.get("code", ""),
|
||||
"contractStatus": "1",
|
||||
"createdBy": av.get("createdBy", ""),
|
||||
"createdByName": av.get("createdByName", ""),
|
||||
"deleted": "0",
|
||||
"deliveryCashFee": av.get("deliveryCashFee", ""),
|
||||
"deliveryFeeDeserved": av.get("deliveryFeeDeserved", ""),
|
||||
"deliveryFeePractical": av.get("deliveryFeePractical", ""),
|
||||
"deliveryOilFee": "0",
|
||||
"distance": av.get("distance", ""),
|
||||
"driverActualCash": av.get("driverActualCash", ""),
|
||||
"driverActualFreight": av.get("driverActualFreight", ""),
|
||||
"driverCode": av.get("driverCode", ""),
|
||||
"driverName": av.get("driverName", ""),
|
||||
"driverPhone": av.get("driverPhone", ""),
|
||||
"energyCardType": av.get("energyCardType", ""),
|
||||
"etcType": "0",
|
||||
"extraFee": "0",
|
||||
"fillTime": av.get("fillTime", ""),
|
||||
"finishedTrans": "0",
|
||||
"gmtCreated": av.get("gmtCreated", ""),
|
||||
"gmtModified": av.get("gmtModified", ""),
|
||||
"id": av.get("id", ""),
|
||||
"inFee": "0",
|
||||
"infoFee": "0",
|
||||
"insuranceAmount": "0",
|
||||
"insuranceFee": "0",
|
||||
"invoiceIsAll": "0",
|
||||
"invoiceStatus": "0",
|
||||
"invoiceStatusCaptain": "0",
|
||||
"invoiceStatusDriver": "0",
|
||||
"isAbnormal": "0",
|
||||
"isCancel": "0",
|
||||
"isCaptainOrder": "0",
|
||||
"isCash": "0",
|
||||
"isCuikuan": "0",
|
||||
"isDriverComment": "0",
|
||||
"isEqualDriver": "1",
|
||||
"isEqualTeam": "1",
|
||||
"isEqualVehicle": "1",
|
||||
"isEtcHistory": "0",
|
||||
"isFill": "1",
|
||||
"isImport": "0",
|
||||
"isInsure": "0",
|
||||
"isLoadingConfirm": "1",
|
||||
"isLoadingConfirmDate": av.get("isLoadingConfirmDate", ""),
|
||||
"isOriginalOrder": "1",
|
||||
"isPaperReturns": "0",
|
||||
"isReceiptReturn": "0",
|
||||
"isReportOrder": "0",
|
||||
"isSend": "0",
|
||||
"isSendDishonestyVehicle": "0",
|
||||
"isSendFund": "0",
|
||||
"isSendFundZhongLian": "0",
|
||||
"isSendLoadLocation": "0",
|
||||
"isSendTax": "0",
|
||||
"isSendUnloadLocation": "0",
|
||||
"isSendZhongLian": "0",
|
||||
"isSettle": "0",
|
||||
"isShipmentComment": "0",
|
||||
"isSign": "1",
|
||||
"isSplitOrder": "0",
|
||||
"isSupplementary": "0",
|
||||
"isTransfer": "0",
|
||||
"isTrunkName": av.get("isTrunkName", ""),
|
||||
"isUnloadingComfirmDate": av.get("isUnloadingComfirmDate", ""),
|
||||
"isUnloadingConfirm": "1",
|
||||
"isWsShare": "0",
|
||||
"loadImgUrls[]": av.get("ticketImageUrls", ""),
|
||||
"loadLatitude": av.get("loadLatitude", ""),
|
||||
"loadLongitude": av.get("loadLongitude", ""),
|
||||
"loadPlace": av.get("loadPlace", ""),
|
||||
"loadQuantity": av.get("loadQuantity", ""),
|
||||
"loadWeight": av.get("loadWeight", "1"),
|
||||
"mainOrderCode": av.get("mainOrderCode", ""),
|
||||
"modifiedBy": av.get("modifiedBy", ""),
|
||||
"modifiedByName": av.get("modifiedByName", ""),
|
||||
"monthlySettlementStatus": "0",
|
||||
"oilFee": av.get("oilFee", ""),
|
||||
"onTrans": "0",
|
||||
"orderCode": av.get("orderCode", ""),
|
||||
"payAmount": "0",
|
||||
"payOil": "0",
|
||||
"payStatus": "0",
|
||||
"payStep": "10",
|
||||
"payTaxAmount": "0",
|
||||
"payTotalAmount": av.get("payTotalAmount", ""),
|
||||
"payType": "0",
|
||||
"payeeCode": _v("createdBy", "payee_code"), # step06 createdBy = payeeCode
|
||||
"payeeName": av.get("payeeName", ""),
|
||||
"payerReceiver": self.ac.get("payer_receiver", ""),
|
||||
"payerReceiverLinkMan": self.ac.get("payer_receiver_link_man", ""),
|
||||
"platformPrepayment": "0",
|
||||
"prepaidStatus": "0",
|
||||
"price": av.get("price", ""),
|
||||
"proofAmount": "0",
|
||||
"proofStatus": "0",
|
||||
"realPrice": av.get("realPrice", ""),
|
||||
"receiptNum": "",
|
||||
"receiptPayAmount": "0",
|
||||
"refuelProportion": "0",
|
||||
"reportMark": "0",
|
||||
"reportStage": "0",
|
||||
"settlementCash": "0",
|
||||
"settlementOil": "0",
|
||||
"settlementType": self.ac.get("settlement_type", "2"),
|
||||
"settlementWeight": av.get("settlementWeight", "1"),
|
||||
"shipmentActualCash": av.get("shipmentActualCash", ""),
|
||||
"shipmentActualFreight": av.get("shipmentActualFreight", ""),
|
||||
"shipmentCardScale": "10",
|
||||
"shipmentDeliveryFeePractical": av.get("shipmentDeliveryFeePractical", ""),
|
||||
"shipmentName": av.get("shipmentName", ""),
|
||||
"shipmentPayStatus": "0",
|
||||
"shipmentPhone": _v("shipmentPhone", "shipment_phone"),
|
||||
"shipperDeliveryFee": av.get("shipperDeliveryFee", ""),
|
||||
"shippingNoteNumber": av.get("shippingNoteNumber", ""),
|
||||
"signTime": av.get("signTime", ""),
|
||||
"status": "3",
|
||||
"takeLatitude": av.get("takeLatitude", ""),
|
||||
"takeLongitude": av.get("takeLongitude", ""),
|
||||
"takeOrderType": "2",
|
||||
"takePlace": av.get("takePlace", ""),
|
||||
"taxFee": av.get("taxFee", ""),
|
||||
"taxPayFee": av.get("taxPayFee", ""),
|
||||
"taxPrice": av.get("taxPrice", ""),
|
||||
"ticketImageUrls[]": av.get("ticketImageUrls", ""),
|
||||
"txStatus": "0",
|
||||
"unloadLatitude": self.dp.get("latitude", ""),
|
||||
"unloadLongitude": self.dp.get("longitude", ""),
|
||||
"unloadPlace": av.get("unloadPlace", ""),
|
||||
"unloadQuantity": av.get("unloadQuantity", ""),
|
||||
"unloadWeight": av.get("unloadWeight", "1"),
|
||||
"vehicleCode": av.get("vehicleCode", ""),
|
||||
"vehicleLicense": av.get("vehicleLicense", ""),
|
||||
"waitCar": "0",
|
||||
"waitCheckTrans": "0",
|
||||
"waitDriverCheckTrans": "0",
|
||||
"waitPay": "0",
|
||||
"waitSettle": "0",
|
||||
"waitTrans": "0",
|
||||
"wastage": "0",
|
||||
"weight": av.get("weight", "1"),
|
||||
"whetherOverload": "0",
|
||||
"wsShareRatio": "0",
|
||||
"deductionFee": "0",
|
||||
"driverErase": "0",
|
||||
"driverFixedPay": "0",
|
||||
"driverSettlementWeight": av.get("driverSettlementWeight", "1"),
|
||||
"electricityCardScale": "10",
|
||||
"gasCardScale": "10",
|
||||
"isTrunk": "0",
|
||||
"limitWastageAmount": "500",
|
||||
"limitWastageType": "1",
|
||||
"shipmentErase": "0",
|
||||
"shipmentPartyCode": _v("shipmentPartyCode", "shipment_party_code"),
|
||||
"shipmentSettlementWeight": av.get("shipmentSettlementWeight", "1"),
|
||||
"shipperFixedPay": "0",
|
||||
"taxPoint": av.get("taxPoint", ""),
|
||||
"taxPriceFixedPay": "0",
|
||||
"orderExtraFeeListStr": "[]",
|
||||
"BranchCode": _v("branchCode", "branch_code"),
|
||||
"unloadImgaes": av.get("ticketImageUrls", ""),
|
||||
"truckLoadingImgaes": av.get("ticketImageUrls", ""),
|
||||
"IsTaxPoint": "0",
|
||||
"RealPrice": av.get("realPrice", ""),
|
||||
"gmtCreated2": av.get("gmtCreated", ""),
|
||||
"gmtModified2": av.get("gmtModified", ""),
|
||||
}
|
||||
return self._put_with_query_string(self.admin, "/ntocc-tms-api/orderDetail/orderHandleEditSubmitHandle", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 10: 查询运单审核
|
||||
# ==================================================================
|
||||
def step10_query_audit(self, order_code: str,
|
||||
page_size: int = 20, page_num: int = 1) -> dict:
|
||||
"""
|
||||
GET /ntocc-tms-api/orderDetail (status=6 已结算, isSettle=1)
|
||||
Admin 端查询已结算运单用于审核。
|
||||
"""
|
||||
params = {
|
||||
"isAbnormalHelp": "",
|
||||
"gmtCreatedRange": "",
|
||||
"lastLoadingTimeRange": "",
|
||||
"fillTimeRange": "",
|
||||
"lastUnloadingTimeRange": "",
|
||||
"signTimeRange": "",
|
||||
"settleTimeRange": "",
|
||||
"gmtModifiedRange": "",
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"isSettle": "1",
|
||||
"status": "6",
|
||||
"payerReceiverLinkMan": self.ac.get("payer_receiver_link_man", ""),
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"orderCode": order_code,
|
||||
}
|
||||
return self._get(self.admin, "/ntocc-tms-api/orderDetail", params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 11: 核算审核通过
|
||||
# ==================================================================
|
||||
def step11_approve_audit(self, code: str) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/auditOrderDetail?orderDetailCodes={code}&isPass=true
|
||||
"""
|
||||
params = {"orderDetailCodes": code, "isPass": "true"}
|
||||
return self._post_with_query_string(self.admin, "/ntocc-tms-api/orderDetail/auditOrderDetail", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 12: 队长分账预览 (captainSplitAdjustPreview)
|
||||
# ==================================================================
|
||||
def step12_captain_split_preview(self, order_detail_code: str) -> dict:
|
||||
"""
|
||||
GET /ntocc-tms-api/orderDetail/captainSplitAdjustPreview/{code}
|
||||
仅在 payerReceiver 不为空且 captainSplitMode=2 时调用。
|
||||
"""
|
||||
path = f"/ntocc-tms-api/orderDetail/captainSplitAdjustPreview/{order_detail_code}"
|
||||
return self._get(self.admin, path)
|
||||
|
||||
# Step 13: 队长分账调整 (adjustCaptainSplit)
|
||||
# ==================================================================
|
||||
def step13_adjust_captain_split(self, order_detail_code: str,
|
||||
latest_driver_amount: float,
|
||||
delivery_oil_fee: float) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/adjustCaptainSplit
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
|
||||
driverAmount = ceil(latestDriverAmount * 0.8 * 100) / 100
|
||||
captainAmount = latestDriverAmount - driverAmount
|
||||
oilFee = deliveryOilFee
|
||||
"""
|
||||
driver_amount = math.ceil(latest_driver_amount * 0.8 * 100) / 100.0
|
||||
captain_amount = round(latest_driver_amount - driver_amount, 2)
|
||||
body = {
|
||||
"orderDetailCode": order_detail_code,
|
||||
"driverAmount": str(driver_amount),
|
||||
"captainAmount": str(captain_amount),
|
||||
"oilFee": delivery_oil_fee,
|
||||
}
|
||||
resp = self.admin.post(
|
||||
f"{self.base_url}/ntocc-tms-api/orderDetail/adjustCaptainSplit",
|
||||
json=body,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
# Step 14: 单油卡充值 (singleOil)
|
||||
# ==================================================================
|
||||
def step14_single_oil(self, order_detail_code: str, amount: str = "0.03",
|
||||
pay_type: str = "1", category: str = "1") -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/finance/singleOil
|
||||
Query params: payType, amount, orderDetailCode, category, isAdvancePayment
|
||||
No body (content-length: 0)
|
||||
|
||||
在垫资-财务打款之前调用,处理油卡充值。
|
||||
"""
|
||||
params = {
|
||||
"payType": pay_type,
|
||||
"amount": amount,
|
||||
"orderDetailCode": order_detail_code,
|
||||
"category": category,
|
||||
"isAdvancePayment": "1",
|
||||
}
|
||||
return self._post_with_query_string(
|
||||
self.admin,
|
||||
"/ntocc-tms-api/finance/singleOil",
|
||||
params=params,
|
||||
)
|
||||
|
||||
# ==================================================================
|
||||
# Step 15: 垫资-财务打款
|
||||
# ==================================================================
|
||||
def step15_pay_to_driver(self, code: str, amount: str = None,
|
||||
delivery_cash_fee: str = None,
|
||||
driver_actual_freight: str = None,
|
||||
plan_driver_payout: str = "0",
|
||||
plan_captain_payout: str = "0") -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/arpaTransfer/payToDriver
|
||||
amount / deliveryCashFee / driverActualFreight 由 pipeline 从 step07 传入
|
||||
planDriverPayout / planCaptainPayout 来自队长分账步骤,无分账时为 "0"
|
||||
"""
|
||||
if amount is None:
|
||||
amount = "0.27"
|
||||
if delivery_cash_fee is None:
|
||||
delivery_cash_fee = "0.27"
|
||||
if driver_actual_freight is None:
|
||||
driver_actual_freight = "0.3"
|
||||
params = {
|
||||
"payTypes": self.pm.get("pay_types", "3,4,5,9,10,11"),
|
||||
"amount": amount,
|
||||
"orderDetailCode": code,
|
||||
"infoFee": "0",
|
||||
"pageNum": "1",
|
||||
"pageSize": "20",
|
||||
"totaltotal": "0",
|
||||
"payerRole": "0",
|
||||
"isAdvancePayment": "1",
|
||||
"planDriverPayout": plan_driver_payout,
|
||||
"planCaptainPayout": plan_captain_payout,
|
||||
"allotAmount": "0",
|
||||
"deliveryCashFee": delivery_cash_fee,
|
||||
"driverActualFreight": driver_actual_freight,
|
||||
"total": "0",
|
||||
"payPlatform": self.pm.get("pay_platform", "3"),
|
||||
"smsCode": self.pm.get("sms_code", "1234"),
|
||||
"phone": self.pm.get("phone", "13888888888"),
|
||||
}
|
||||
return self._post_with_query_string(self.admin, "/ntocc-tms-api/arpaTransfer/payToDriver", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 16: 垫资-货主打款 (申请付款)
|
||||
# ==================================================================
|
||||
def step16_apply_for_payment(self, code: str, total_amount: str = None) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/applicationForPayment
|
||||
totalAmount 应从 pipeline 传入 (step06 的 shipmentActualCash)
|
||||
"""
|
||||
if total_amount is None:
|
||||
total_amount = "0.32"
|
||||
params = {
|
||||
"payType": self.pm.get("pay_type", "arpa_2"),
|
||||
"totalAmount": total_amount,
|
||||
"codes": code,
|
||||
"payPlatform": self.pm.get("pay_platform", "3"),
|
||||
"isAdvancePayment": "1",
|
||||
}
|
||||
return self._post_with_query_string(self.admin, "/ntocc-tms-api/orderDetail/applicationForPayment", params=params)
|
||||
|
||||
# ==================================================================
|
||||
# Step 17: 查询运单上传 → 提取 payNo
|
||||
# ==================================================================
|
||||
def step17_query_upload_for_payno(self, order_code: str,
|
||||
page_size: int = 20, page_num: int = 1) -> dict:
|
||||
"""
|
||||
Postman Test Script:
|
||||
var sendMsg = jsonData.data.records[0].sendMsg || "";
|
||||
var match = sendMsg.match(/【([^】]+)】/);
|
||||
var result = match ? match[1] : "";
|
||||
pm.collectionVariables.set("payNo", result);
|
||||
|
||||
Returns: {"payNo": str, "raw_records": list}
|
||||
"""
|
||||
params = {
|
||||
"gmtCreatedRange[]": ["", ""],
|
||||
"lastLoadingTimeRange": "",
|
||||
"fillTimeRange": "",
|
||||
"lastUnloadingTimeRange": "",
|
||||
"signTimeRange": "",
|
||||
"settleTimeRange": "",
|
||||
"gmtModifiedRange": "",
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"status": "5", # <-- 注意:status=5
|
||||
"payerReceiverLinkMan": self.ac.get("payer_receiver_link_man", ""),
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"gmtCreatedStart": "",
|
||||
"gmtCreatedEnd": "",
|
||||
"orderCode": order_code,
|
||||
}
|
||||
data = self._get(self.admin, "/ntocc-basic-api/dytBusiness/orderDetail", params)
|
||||
records = data.get("data", {}).get("records", [])
|
||||
pay_no = ""
|
||||
if records:
|
||||
send_msg = records[0].get("sendMsg", "")
|
||||
match = re.search(r"【([^】]+)】", send_msg)
|
||||
if match:
|
||||
pay_no = match.group(1)
|
||||
|
||||
return {"payNo": pay_no, "raw_records": records}
|
||||
|
||||
# ==================================================================
|
||||
# Step 18: 查询打款记录 → 提取所有 code
|
||||
# ==================================================================
|
||||
def step18_query_remit_records(self, order_code: str,
|
||||
page_size: int = 20) -> list[dict]:
|
||||
"""
|
||||
Postman Test Script:
|
||||
pm.collectionVariables.set("remitCode", jsonData.data.records[0].code);
|
||||
|
||||
分页获取全部打款记录。Returns: [{code, ...}, ...]
|
||||
"""
|
||||
all_records = []
|
||||
page_num = 1
|
||||
|
||||
while True:
|
||||
params = {
|
||||
"pageNum": page_num,
|
||||
"pageSize": page_size,
|
||||
"pageForm": "arpa_2",
|
||||
"orderDetailNumber": order_code,
|
||||
"sortField": "",
|
||||
"sortType": "",
|
||||
"isPayAdvance": "",
|
||||
"platPayType": "",
|
||||
"gmtModifiedStart": "",
|
||||
"gmtModifiedEnd": "",
|
||||
}
|
||||
data = self._get(self.admin, "/ntocc-tms-api/remitDefinite", params)
|
||||
# code 可能是 0 (成功) 或 None/null (也是成功)
|
||||
resp_code = data.get("code")
|
||||
if resp_code is not None and resp_code != 0:
|
||||
raise RuntimeError(f"查询打款记录失败: {data.get('msg')}")
|
||||
|
||||
page_data = data.get("data", {})
|
||||
records = page_data.get("records", [])
|
||||
all_records.extend(records)
|
||||
|
||||
total = page_data.get("total", 0)
|
||||
if page_num * page_size >= total:
|
||||
break
|
||||
page_num += 1
|
||||
|
||||
return all_records
|
||||
|
||||
# ==================================================================
|
||||
# Step 19: 查询文件名前缀 → 提取 filePrefix
|
||||
# ==================================================================
|
||||
def step19_query_file_prefix(self, remit_code: str) -> str:
|
||||
"""
|
||||
Postman Test Script:
|
||||
const msg = jsonData.msg || '';
|
||||
const prefix = '文件名前缀:';
|
||||
const index = msg.indexOf(prefix);
|
||||
const filePrefix = index !== -1 ? msg.substring(index + prefix.length).trim() : '';
|
||||
pm.collectionVariables.set('filePrefix', filePrefix);
|
||||
|
||||
Returns: filePrefix string
|
||||
"""
|
||||
params = {"code": remit_code}
|
||||
data = self._get(self.admin,
|
||||
"/ntocc-tms-api/centralizedPayment/downLoadElectronicReceipt",
|
||||
params)
|
||||
msg = data.get("msg", "")
|
||||
prefix_marker = "文件名前缀:"
|
||||
idx = msg.find(prefix_marker)
|
||||
if idx != -1:
|
||||
return msg[idx + len(prefix_marker):].strip()
|
||||
return ""
|
||||
|
||||
# ==================================================================
|
||||
# Step 20: 上报运单 (reportOrderDetail)
|
||||
# ==================================================================
|
||||
def step20_report_order_detail(self, codes: str, report_type: int = 1) -> dict:
|
||||
"""
|
||||
POST /ntocc-basic-api/dytBusiness/reportOrderDetail
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
Body: {"codes": "...", "type": 1}
|
||||
|
||||
在打款完成后上报运单,完成整个流程闭环。
|
||||
"""
|
||||
body = {"codes": codes, "type": report_type}
|
||||
resp = self.admin.post(
|
||||
f"{self.base_url}/ntocc-basic-api/dytBusiness/reportOrderDetail",
|
||||
json=body,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# ==================================================================
|
||||
# Step 21: 滇运通审核 (updateOrderDetailExt)
|
||||
# ==================================================================
|
||||
def step21_owner_review(self, order_detail_code: str) -> dict:
|
||||
"""
|
||||
POST /ntocc-tms-api/orderDetail/updateOrderDetailExt
|
||||
滇运通独立接口,独立 token / host。
|
||||
仅在 config.dianyuntong.enabled=true 时执行。
|
||||
"""
|
||||
if not self.tp_enabled or not self.tp:
|
||||
raise RuntimeError("dianyuntong 未启用,请检查 config.yaml")
|
||||
params = {
|
||||
"orderDetailCode": order_detail_code,
|
||||
"reviewStatus": "1",
|
||||
"reviewMsg": "",
|
||||
"invoiceApplicationStatus": "0",
|
||||
"invoiceApplicationMsg": "",
|
||||
}
|
||||
resp = self.tp.post(
|
||||
f"{self.tp_base}/ntocc-tms-api/orderDetail/updateOrderDetailExt",
|
||||
params=params,
|
||||
verify=self.verify_ssl,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
# ==================================================================
|
||||
@@ -0,0 +1,127 @@
|
||||
# ============================================================
|
||||
# yun8 运输流程完整自动化 — 配置文件
|
||||
# 对应 Postman Collection: yun8.postman_collection.json
|
||||
# ============================================================
|
||||
|
||||
# ----- API 配置 -----
|
||||
api:
|
||||
base_url: "https://ybxcx.ynyun8.com:8000"
|
||||
verify_ssl: false
|
||||
|
||||
# 通用请求头(与 Postman Collection 一致)
|
||||
common_headers:
|
||||
host: "ybxcx.ynyun8.com:8000"
|
||||
accept-language: "zh-CN,zh;q=0.9,en;q=0.8"
|
||||
accept-encoding: "gzip"
|
||||
|
||||
# 司机端 Header(步骤1-5)
|
||||
driver:
|
||||
deviceid: "A58FBAB7161EA82309491FB06CDE9687"
|
||||
user-agent: "okhttp-okgo/jeasonlzy"
|
||||
|
||||
# 管理端 Header(步骤6-16)
|
||||
admin:
|
||||
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, */*"
|
||||
referer: "https://ybxcx.ynyun8.com:8000/admin/"
|
||||
|
||||
# ----- 授权 Token -----
|
||||
# 通过命令行 --auth-driver / --auth-admin 传入,或在此写默认值
|
||||
auth:
|
||||
# driver_token: "Bearer NRWt5TGNYgZOMYWimJ21xTzYYgEZMYD45Dz3lZEOYcTOZJG4jTT55QMZYFDj" # 司机端 Bearer token (含 Bearer 前缀)
|
||||
driver_token: "Bearer mJ25zW3NYIYYZQcYMVQZ2QFNmTj5wkzMYMFZlj2NzDWk2mj12mT33QMMMIjM" # 司机端 Bearer token (含 Bearer 前缀)
|
||||
admin_token: "Bearer Z=AZ3I3YlTTzhTGNOFDO5DDx1YhYMYz5hTTOOQUZkFTl5kFOMYdY3QWM2Em=" # 管理端 Bearer token (含 Bearer 前缀)
|
||||
|
||||
# ----- 司机端固定参数(装货/卸货) -----
|
||||
driver_params:
|
||||
# 重量模式: "fixed"(固定值) / "random"(每次随机 1-10 小数)
|
||||
weight_mode: "random"
|
||||
per_weight: "10" # fixed 模式下的接单单位重量
|
||||
weight: "10" # fixed 模式下的装货/卸货重量
|
||||
# 装货/卸货经纬度(不为空时覆盖货源接口返回值)
|
||||
# load_latitude: "38.5166" # 接单+装货纬度
|
||||
# load_longitude: "102.308617" # 接单+装货经度
|
||||
# unload_latitude: "38.529333" # 卸货纬度
|
||||
# unload_longitude: "102.25395" # 卸货经度
|
||||
# load_latitude: "28.116007" # 接单+装货纬度
|
||||
# load_longitude: "112.982951" # 接单+装货经度
|
||||
# unload_latitude: "30.663179" # 卸货纬度
|
||||
# unload_longitude: "104.039285" # 卸货经度
|
||||
load_latitude: "" # 接单+装货纬度
|
||||
load_longitude: "" # 接单+装货经度
|
||||
unload_latitude: "" # 卸货纬度
|
||||
unload_longitude: "" # 卸货经度
|
||||
# 待运输单重置(清掉旧记录后重新查询,仅影响司机端流程)
|
||||
reset_order_detail: false # true=step03后执行DELETE+UPDATE再重新查询
|
||||
reset_order_detail_code: "f234118ee56d4b3db4ae1eeb97b0b194" # DELETE+UPDATE SET 使用的固定 code
|
||||
# 通用经纬度兜底(当上述配置和接口返回值都为空时使用)
|
||||
latitude: "25.005731558883365"
|
||||
longitude: "102.652599407955"
|
||||
# 装货照片 URL
|
||||
load_img_url: "https://ynyun8.eos-chongqing-1.cmecloud.cn/2026-06-30/88a6ff85d8ad4e26bf1571d59ae4f60c.jpg?AWSAccessKeyId=AOS5YGU0MP2Y88ELIFFH&Expires=4102329600&Signature=s68ODdXZ%2BI9s09IbaPwLy6Hefxw%3D"
|
||||
# 卸货照片 URL
|
||||
unload_img_url: "https://ynyun8.eos-chongqing-1.cmecloud.cn/2026-06-30/66426dc5be6242968dd9a3d0fd6e8b38.jpg?AWSAccessKeyId=AOS5YGU0MP2Y88ELIFFH&Expires=4102329600&Signature=UVKmwyFEnzA4fh%2BmnlGzmDOmuJE%3D"
|
||||
|
||||
# ----- 核算固定参数 -----
|
||||
accounting:
|
||||
branch_code: "49adb23841asw3118yunnanchenggang"
|
||||
business_type_code: "1003999"
|
||||
payee_code: "16d0c260f7e34967b36113c770e03486"
|
||||
shipment_phone: "15198908093"
|
||||
shipment_party_code: "ce8b73b38cf6450d84740cf23aa4ed52"
|
||||
settlement_type: "2"
|
||||
# 队长分账:payer_receiver/link_man 不为空 且 captainSplitMode=2 时触发
|
||||
# payer_receiver: "bd759732e4ac442b8fabbf65a405dae1" # 收款人
|
||||
# payer_receiver_link_man: "车队一" # 收款联系人
|
||||
# captain_split_mode: "2" # 队长分账模式 (0=不分账, 2=分账)
|
||||
# execute_after_audit: true # 审核通过后是否执行分账/付款/回单等后续步骤(12-21)
|
||||
payer_receiver: "" # 收款人
|
||||
payer_receiver_link_man: "" # 收款联系人
|
||||
captain_split_mode: "0" # 队长分账模式 (0=不分账, 2=分账)
|
||||
execute_after_audit: true # 审核通过后是否执行分账/付款/回单等后续步骤(12-21)
|
||||
|
||||
# ----- 垫资/付款固定参数 -----
|
||||
payment:
|
||||
pay_types: "3,4,5,9,10,11"
|
||||
pay_platform: "3"
|
||||
sms_code: "1234"
|
||||
phone: "13888888888"
|
||||
pay_type: "arpa_2"
|
||||
|
||||
# ----- 数据库配置 -----
|
||||
database:
|
||||
host: "36.137.133.24"
|
||||
port: 3306
|
||||
user: "ntocc"
|
||||
password: "Rgxi3!kD9jjXpgzx"
|
||||
database: "ntocc"
|
||||
charset: "utf8mb4"
|
||||
|
||||
# ----- SQL 模板 -----
|
||||
sql:
|
||||
# file_name 格式: {file_prefix}_1_signed.pdf
|
||||
file_name_template: "{file_prefix}_1_signed.pdf"
|
||||
# INSERT 模板, 占位符 {file_name} / {pay_no}
|
||||
template: |
|
||||
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', '{file_name}',
|
||||
'批量写入回单编号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',
|
||||
'{pay_no}')
|
||||
|
||||
# ----- 第三方端(滇运通审核,独立 token / host,可选执行) -----
|
||||
dianyuntong:
|
||||
enabled: false # true=执行, false=跳过
|
||||
base_url: "https://yndyt.cxtech.vip"
|
||||
token: "Bearer NTmlyQYMYYEMwDTy1DDyxWjwykNZyQGO0jDOYYAMMQcNMgMN3jTy0jTYZlZh" # 独立 Bearer Token
|
||||
cookie: "" # Cookie 字符串 (Admin-Token=xxx; sl-session=xxx)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
数据库操作
|
||||
==========
|
||||
封装 ntocc.base_gd_mft_file 表的 INSERT 操作。
|
||||
"""
|
||||
|
||||
import pymysql
|
||||
|
||||
|
||||
class Database:
|
||||
"""MySQL 数据库写入"""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
db = config["database"]
|
||||
self.conn_kwargs = {
|
||||
"host": db["host"],
|
||||
"port": db.get("port", 3306),
|
||||
"user": db["user"],
|
||||
"password": db["password"],
|
||||
"database": db["database"],
|
||||
"charset": db.get("charset", "utf8mb4"),
|
||||
}
|
||||
sql_cfg = config["sql"]
|
||||
self.file_name_template = sql_cfg["file_name_template"]
|
||||
self.sql_template = sql_cfg["template"]
|
||||
|
||||
def build_sql(self, file_prefix: str, pay_no: str) -> str:
|
||||
"""拼接完整 SQL(dry-run / 日志用)"""
|
||||
file_name = self.file_name_template.format(file_prefix=file_prefix)
|
||||
return self.sql_template.format(file_name=file_name, pay_no=pay_no)
|
||||
|
||||
def insert(self, file_prefix: str, pay_no: str) -> int:
|
||||
"""执行 INSERT,返回 affected rows"""
|
||||
sql = self.build_sql(file_prefix, pay_no)
|
||||
conn = pymysql.connect(**self.conn_kwargs)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
return cur.rowcount
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def execute(self, sql: str) -> int:
|
||||
"""执行任意 SQL,返回 affected rows"""
|
||||
conn = pymysql.connect(**self.conn_kwargs)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
return cur.rowcount
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
yun8 运输流程完整自动化
|
||||
=======================
|
||||
将 Postman Collection (yun8.postman_collection.json) 中
|
||||
"运输流程" 全部 17 个接口 + SQL 写入实现为 Python 流水线。
|
||||
|
||||
阶段 A — 司机端 (步骤 1-5) 查询货源→接单→待运输→装货→卸货
|
||||
阶段 B — 运费核算 (步骤 6-9) 查询→详情→试算→提交核算
|
||||
阶段 C — 审核 & 付款 (步骤 10-16) 查询审核→审核通过→司机打款→货主打款
|
||||
阶段 D — 回单文件 + SQL (步骤 14-16) payNo→打款记录→文件名前缀→INSERT
|
||||
阶段 E — 上报运单 (步骤 17) reportOrderDetail
|
||||
|
||||
用法:
|
||||
# 完整流程
|
||||
python main.py --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
|
||||
# 仅跑特定阶段
|
||||
python main.py --stage driver --auth-driver "Bearer xxx" --auth-admin "Bearer yyy"
|
||||
|
||||
# 循环执行 10 次,间隔 3 秒
|
||||
python main.py --repeat 10 --delay 3
|
||||
|
||||
# 预览 SQL 不写库
|
||||
python main.py --dry-run
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from pipeline import Pipeline
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return yaml.safe_load(fh)
|
||||
|
||||
|
||||
STAGE_HELP = """阶段选择:
|
||||
all 完整流程 (默认): 司机端 → 核算 → 付款 → 回单SQL → 上报
|
||||
driver 仅司机端 (步骤 1-5)
|
||||
accounting 仅运费核算 (步骤 6-9)
|
||||
payment 仅审核付款 (步骤 10-16)
|
||||
receipt 仅回单文件+SQL+上报 (步骤 14-17)
|
||||
"""
|
||||
|
||||
|
||||
def run_one_cycle(config: dict, stage: str, driver_token: str,
|
||||
admin_token: str, dry_run: bool) -> bool:
|
||||
"""执行一次完整流水线,返回 True 表示成功"""
|
||||
pipe = Pipeline(config)
|
||||
pipe.set_auth(driver_token, admin_token)
|
||||
|
||||
if stage in ("all", "driver"):
|
||||
pipe.run_driver_flow()
|
||||
|
||||
# reset_order_detail 模式:仅跑司机端,跳过后续全部步骤
|
||||
dp = config.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 = config.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)
|
||||
if stage in ("all", "receipt"):
|
||||
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
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="yun8 运输流程完整自动化",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=STAGE_HELP,
|
||||
)
|
||||
parser.add_argument("--config", default=None,
|
||||
help="配置文件路径 (默认: 脚本同目录下 config.yaml)")
|
||||
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,不实际写入数据库")
|
||||
parser.add_argument("--repeat", type=int, default=1,
|
||||
help="循环执行次数 (默认: 1)")
|
||||
parser.add_argument("--delay", type=int, default=0,
|
||||
help="每次循环之间的等待秒数 (默认: 0)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 加载配置
|
||||
config_path = args.config or (Path(__file__).parent / "config.yaml")
|
||||
config = load_config(config_path)
|
||||
|
||||
# 读取 Token
|
||||
driver_token = args.auth_driver or config.get("auth", {}).get("driver_token", "")
|
||||
admin_token = args.auth_admin or config.get("auth", {}).get("admin_token", "")
|
||||
|
||||
print("=" * 60)
|
||||
print(f" yun8 运输流程自动化")
|
||||
print(f" config: {config_path}")
|
||||
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(config, 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,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]}...")
|
||||
@@ -0,0 +1,3 @@
|
||||
requests>=2.28
|
||||
pymysql>=1.0
|
||||
PyYAML>=6.0
|
||||
Reference in New Issue
Block a user