feat(yun8_sync): 重写 ETC 开票步骤 (Step 22-23) 及配套配置文档

- pipeline.py: ETC 开票信息写入 base_etc_invoice + 推送记录写入
  tms_order_etc_push, 随 receipt 阶段执行; 每单发票条数可配
  (etc_invoice.count), 发票号/金额/进出站/时间自动变化,
  金额合计受运费×50%约束; order_code/order_no 取步骤 03 的
  orderCode, push_order_no 追加 -1, dry-run 仅预览不写库
- config.yaml: 新增 etc_invoice 配置节 + 刷新登录 token
- README.md: 阶段 E 流程图、接口表 Step 22/23 及取值说明
This commit is contained in:
xst
2026-09-02 17:44:52 +08:00
parent 625a589bfc
commit 224fe88ea6
3 changed files with 190 additions and 53 deletions
+6 -2
View File
@@ -22,8 +22,9 @@
│ 阶段 D: 回单文件 + SQL (Step 15-18) │ │ 阶段 D: 回单文件 + SQL (Step 15-18) │
│ 运单上传(payNo) → 打款记录 → 文件名前缀 → INSERT → 上报运单 │ │ 运单上传(payNo) → 打款记录 → 文件名前缀 → INSERT → 上报运单 │
├─────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────┤
│ 阶段 E: ETC 开票 SQL (Step 22-23) │ │ 阶段 E: ETC 开票 SQL (Step 22-24) │
│ 写入 base_etc_invoice → 写入 tms_order_etc_push │ │ 写入 base_etc_invoice (多条) → 写入 tms_order_etc_push │
│ → 更新 tms_order_detail 开票状态 │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
``` ```
@@ -236,5 +237,8 @@ yun8_sync/
| — | SQL INSERT | — | `ntocc.base_gd_mft_file` | — | | — | SQL INSERT | — | `ntocc.base_gd_mft_file` | — |
| 22 | ETC 开票信息写入 | SQL INSERT | — | `ntocc.base_etc_invoice` | — | | 22 | ETC 开票信息写入 | SQL INSERT | — | `ntocc.base_etc_invoice` | — |
| 23 | ETC 推送记录写入 | SQL INSERT | — | `ntocc.tms_order_etc_push` | — | | 23 | ETC 推送记录写入 | SQL INSERT | — | `ntocc.tms_order_etc_push` | — |
| 24 | 运单 ETC 开票状态更新 | SQL UPDATE | — | `ntocc.tms_order_detail` | — |
> 步骤 22 每个运单插入 `etc_invoice.count` 条发票(config.yaml 配置,默认 3),每条的**发票号码**(随机 20 位数字)、**金额**(随机分配,**所有发票合计 ≤ 运费×50%**,运费优先取 `driverActualFreight`、缺失时取 `shipmentActualFreight`,超限或运费未知则跳过发票写入)、**进站/出站**(按广东→河北路段链取相邻两站)、**时间**(交易时间在运单窗口内逐条推进)各不相同;税额、开票方、发票类型、URL 等其余字段用固定值。
> 步骤 22/23 的 `order_code`/`order_no` 取步骤 03 的 `orderCode`,`push_order_no` 为其后追加 `-1`,`order_detail_code` 取步骤 03 的 `code`,两个表的 `code` 按现有数据规则(32 位十六进制)自动生成,随 receipt 阶段执行(`--dry-run` 仅预览不写库)。 > 步骤 22/23 的 `order_code`/`order_no` 取步骤 03 的 `orderCode`,`push_order_no` 为其后追加 `-1`,`order_detail_code` 取步骤 03 的 `code`,两个表的 `code` 按现有数据规则(32 位十六进制)自动生成,随 receipt 阶段执行(`--dry-run` 仅预览不写库)。
> 步骤 23 的推送记录固定写入 `invoice_pull_status=2`、`start_status=1`、`end_status=1`、`start_result='成功'`、`end_result='成功'`;步骤 24 将步骤 03 `orderCode` 对应运单的 `etc_invoice_status` 置为 3。
+6 -2
View File
@@ -31,8 +31,8 @@ api:
# 通过命令行 --auth-driver / --auth-admin 传入,或在此写默认值 # 通过命令行 --auth-driver / --auth-admin 传入,或在此写默认值
auth: auth:
# driver_token: "Bearer NRWt5TGNYgZOMYWimJ21xTzYYgEZMYD45Dz3lZEOYcTOZJG4jTT55QMZYFDj" # 司机端 Bearer token (含 Bearer 前缀) # driver_token: "Bearer NRWt5TGNYgZOMYWimJ21xTzYYgEZMYD45Dz3lZEOYcTOZJG4jTT55QMZYFDj" # 司机端 Bearer token (含 Bearer 前缀)
driver_token: "Bearer mJ25zW3NYIYYZQcYMVQZ2QFNmTj5wkzMYMFZlj2NzDWk2mj12mT33QMMMIjM" # 司机端 Bearer token (含 Bearer 前缀) driver_token: "Bearer YQZOMJWOYljNZJGN0XT23kFONQJZmDRNlm20lTTYYAUY4YjhiZTj0cDlhDjw" # 司机端 Bearer token (含 Bearer 前缀)
admin_token: "Bearer Z=AZ3I3YlTTzhTGNOFDO5DDx1YhYMYz5hTTOOQUZkFTl5kFOMYdY3QWM2Em=" # 管理端 Bearer token (含 Bearer 前缀) admin_token: "Bearer Z=BZ1gdZYkDN2Ij4kzTw3EGN0GnN2TYYhJcMzQmYNUZNNEWZjNWwmTTOhQD=" # 管理端 Bearer token (含 Bearer 前缀)
# ----- 司机端固定参数(装货/卸货) ----- # ----- 司机端固定参数(装货/卸货) -----
driver_params: driver_params:
@@ -90,6 +90,10 @@ payment:
phone: "13888888888" phone: "13888888888"
pay_type: "arpa_2" pay_type: "arpa_2"
# ----- ETC 开票 (步骤 22) -----
etc_invoice:
count: 3 # 每个运单插入的发票条数; 每条发票号码/金额/进出站/时间自动变化, 其余字段用固定值; 金额合计受运费×50%约束
# ----- 数据库配置 ----- # ----- 数据库配置 -----
database: database:
host: "36.137.133.24" host: "36.137.133.24"
+178 -49
View File
@@ -16,8 +16,10 @@
""" """
import json import json
import random
import time import time
import uuid import uuid
from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from client import Yun8Client from client import Yun8Client
@@ -29,6 +31,13 @@ def _gen_code() -> str:
return uuid.uuid4().hex return uuid.uuid4().hex
# ETC 发票路段链(广东→河北,按行驶方向依次经过;每条发票取相邻两站作为进/出站)
_ETC_STATION_CHAIN = [
"广东古镇东站", "湖南郴州西站", "湖南岳阳东站",
"湖北孝感东站", "河南信阳站", "河北成安西站",
]
# ============================================================ # ============================================================
# Pipeline — 变量上下文 # Pipeline — 变量上下文
# ============================================================ # ============================================================
@@ -583,62 +592,154 @@ class Pipeline:
return results return results
# ================================================================== # ==================================================================
# 步骤 22-23 : ETC 开票 SQL 写入 # 步骤 22-24 : ETC 开票 SQL 写入
# ================================================================== # ==================================================================
def step22_insert_etc_invoice(self, dry_run: bool = False) -> dict: def _gen_etc_invoice(self, idx: int, total: int, amount: float) -> dict:
""" """
22. 写入 ETC 开票信息 (base_etc_invoice) 生成第 idx 条发票的差异字段(发票号码/金额/进出站/时间)。
金额由调用方传入(已按"所有发票合计 ≤ 运费×50%"约束分配)。
时间规则: transaction_time(=ex_time) 在运单窗口
2026-05-13 15:35:38 ~ 2026-05-15 13:35:43 内按条数均分、逐条推进,
en_date=ex_date=该时间日期;invoice_time 在 2026-06-04 ~ 06-06 内随机。
"""
invoice_number = "264" + "".join(random.choices("0123456789", k=17))
pair = idx % (len(_ETC_STATION_CHAIN) - 1)
w_start = datetime(2026, 5, 13, 15, 35, 38)
w_end = datetime(2026, 5, 15, 13, 35, 43)
seg = (w_end - w_start) / total
trans_time = w_start + seg * idx + timedelta(
seconds=random.uniform(0, seg.total_seconds()))
invoice_time = datetime(2026, 6, 4) + timedelta(
seconds=random.uniform(0, 3 * 86400))
return {
"code": _gen_code(),
"invoice_number": invoice_number,
"total_price": amount,
"amount": amount,
"import_station": _ETC_STATION_CHAIN[pair],
"export_station": _ETC_STATION_CHAIN[pair + 1],
"transaction_time": trans_time.strftime("%Y-%m-%d %H:%M:%S"),
"ex_time": trans_time.strftime("%Y-%m-%d %H:%M:%S"),
"en_date": trans_time.strftime("%Y-%m-%d"),
"ex_date": trans_time.strftime("%Y-%m-%d"),
"invoice_time": invoice_time.strftime("%Y-%m-%d %H:%M:%S"),
}
def step22_insert_etc_invoices(self, dry_run: bool = False) -> list[dict]:
"""
22. 写入多条 ETC 开票信息 (base_etc_invoice)
───────────────── ─────────────────
INSERT INTO ntocc.base_etc_invoice 每个运单插入 etc_invoice.count 条发票(config.yaml 配置,默认 3),
order_code ← 步骤03的 orderCode 每条的 发票号码/金额/进站/出站/时间 各不相同,其余字段按原固定值:
push_order_no ← orderCode + "-1" order_code ← 步骤03的 orderCode
order_detail_code ← 步骤03的 code push_order_no ← orderCode + "-1"
code 按现有数据规则自动生成 (32 位十六进制) order_detail_code ← 步骤03的 code
code 按现有数据规则自动生成 (32 位十六进制),每条独立
金额约束: 所有发票金额合计 ≤ 运费(优先 driverActualFreight,其次
shipmentActualFreight) × 50%;运费未知或上限不足时跳过发票写入。
""" """
self._log_step(22, "ETC 开票信息写入 (base_etc_invoice)") self._log_step(22, "ETC 开票信息写入 (base_etc_invoice × N)")
order_code = self._get("orderCode") order_code = self._get("orderCode")
detail_code = self._get("code") detail_code = self._get("code")
if not order_code or not detail_code: if not order_code or not detail_code:
print(" [!] orderCode/code 为空,无法写入 base_etc_invoice") print(" [!] orderCode/code 为空,无法写入 base_etc_invoice")
return {"table": "base_etc_invoice", "status": "skip_no_vars"} return [{"table": "base_etc_invoice", "status": "skip_no_vars"}]
etc_code = _gen_code() raw_count = self.config.get("etc_invoice", {}).get("count", 3)
n = int(raw_count) if raw_count is not None else 3
push_order_no = f"{order_code}-1" 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" order_code = {order_code}")
print(f" push_order_no = {push_order_no}") print(f" push_order_no = {push_order_no}")
print(f" order_detail_code = {detail_code}") print(f" order_detail_code = {detail_code}")
print(f" code = {etc_code}") print(f" 发票条数 = {n}")
if n <= 0:
print(" [skip] etc_invoice.count <= 0,跳过")
return []
if dry_run: # ---- 金额约束: 所有发票金额合计 ≤ 运费 × 50% ----
print(f" [DRY RUN] SQL:\n{sql}") freight_raw = self._get("driverActualFreight") or self._get("shipmentActualFreight")
return {"table": "base_etc_invoice", "status": "dry_run"}
try: try:
rows = self.db.execute(sql) freight = float(freight_raw)
print(f" [OK] INSERT 成功 (rows: {rows})") except (TypeError, ValueError):
return {"table": "base_etc_invoice", "status": "inserted", "rows": rows} freight = 0.0
except Exception as exc: freight_src = ("driverActualFreight" if self._get("driverActualFreight")
print(f" [FAIL] INSERT 失败: {exc}") else "shipmentActualFreight")
return {"table": "base_etc_invoice", "status": "error", "error": str(exc)} cap = freight * 0.5
cap_floor = int(cap * 100) / 100 # 向下取整到分,确保不超上限
print(f" 运费({freight_src}) = {freight:.2f},发票金额上限(运费×50%) = {cap_floor:.2f}")
if cap_floor < 0.01 * n:
print(f" [!] 上限 {cap_floor:.2f} 不足以分配 {n} 张发票(每张至少 0.01),跳过发票写入")
return [{"table": "base_etc_invoice", "status": "skip_amount_cap"}]
# 随机总额: 上限的 30%~100%,且保证每张至少 0.01;按整数分分配,合计精确
total_amt = min(cap_floor, max(0.01 * n,
round(cap_floor * random.uniform(0.3, 1.0), 2)))
total_cents = int(round(total_amt * 100))
alloc = [0] * n # 每张保底 1 分
pool = total_cents - n
if pool > 0:
weights = [random.uniform(0.5, 1.5) for _ in range(n)]
wsum = sum(weights)
alloc = [int(pool * w / wsum) for w in weights]
for _ in range(pool - sum(alloc)):
alloc[random.randrange(n)] += 1
amounts = [(1 + a) / 100 for a in alloc]
# 写入前显式判断(按构造不应触发,作兜底保护)
if round(sum(amounts), 2) > round(cap, 2):
print(f" [!] 发票合计 {sum(amounts):.2f} 超过运费×50% {cap:.2f},跳过发票写入")
return [{"table": "base_etc_invoice", "status": "skip_amount_cap"}]
print(f" 发票金额合计 = {sum(amounts):.2f}({n} 张,各 {['%.2f' % a for a in amounts]})≤ 上限,校验通过")
results = []
for i in range(n):
inv = self._gen_etc_invoice(i, n, amounts[i])
sql = (
"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, '{inv['code']}', '{order_code}', '{push_order_no}', '{detail_code}', "
f"'{inv['invoice_time']}', '9141010055833160XE', '河南省视博电子股份有限公司', "
f"'{inv['import_station']}', '{inv['export_station']}', "
f"'{inv['transaction_time']}', {inv['total_price']:.2f}, {inv['amount']:.2f}, 0.03, 0.03, "
f"'征税发票', '*', '{inv['invoice_number']}', '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', "
f"'{inv['en_date']}', '{inv['ex_date']}', '{inv['ex_time']}', "
"0, 'etc开票申请2026070604', '冀D8E016', 3, 0, "
"NULL, 0, '', '04b7e539c77d4bdbb4fe4e87eaaf41e4', '2026-07-06 09:55:16', '2026-07-06 10:03:26')"
)
print(f" [{i+1}/{n}] 发票号={inv['invoice_number']} 金额={inv['total_price']:.2f} "
f"{inv['import_station']}→{inv['export_station']} "
f"交易时间={inv['transaction_time']} 开票时间={inv['invoice_time']}")
if dry_run:
print(f" [DRY RUN] SQL:\n{sql}")
results.append({"table": "base_etc_invoice",
"invoice_number": inv["invoice_number"],
"status": "dry_run"})
continue
try:
rows = self.db.execute(sql)
print(f" [OK] INSERT 成功 (rows: {rows})")
results.append({"table": "base_etc_invoice",
"invoice_number": inv["invoice_number"],
"status": "inserted", "rows": rows})
except Exception as exc:
print(f" [FAIL] INSERT 失败: {exc}")
results.append({"table": "base_etc_invoice",
"invoice_number": inv["invoice_number"],
"status": "error", "error": str(exc)})
ok = sum(1 for r in results if r["status"] in ("inserted", "dry_run"))
ng = len(results) - ok
print(f" base_etc_invoice 完成: 成功 {ok}, 失败 {ng}, 总计 {n}")
return results
def step23_insert_etc_push(self, dry_run: bool = False) -> dict: def step23_insert_etc_push(self, dry_run: bool = False) -> dict:
""" """
@@ -669,8 +770,8 @@ class Pipeline:
"VALUES (" "VALUES ("
f"NULL, '{etc_code}', '{push_order_no}', '{order_code}', '{detail_code}', " 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, " "'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', " "NULL, '11111', '11111', 2, '2026-07-07 11:40:21', "
"2, '2026-07-07 15:17:02', '2', 2, '2026-07-07 15:11:04', 'dsdsad', " "1, '2026-07-07 15:17:02', '成功', 1, '2026-07-07 15:11:04', '成功', "
"0, NULL, NULL, '2026-07-07 11:09:17', '2026-07-07 15:15:18')" "0, NULL, NULL, '2026-07-07 11:09:17', '2026-07-07 15:15:18')"
) )
print(f" order_no = {order_code}") print(f" order_no = {order_code}")
@@ -689,19 +790,47 @@ class Pipeline:
print(f" [FAIL] INSERT 失败: {exc}") print(f" [FAIL] INSERT 失败: {exc}")
return {"table": "tms_order_etc_push", "status": "error", "error": str(exc)} return {"table": "tms_order_etc_push", "status": "error", "error": str(exc)}
def step24_update_order_etc_status(self, dry_run: bool = False) -> dict:
"""
24. 更新运单 ETC 开票状态 (tms_order_detail)
─────────────────
UPDATE ntocc.tms_order_detail SET etc_invoice_status = 3
WHERE order_code = 步骤03的 orderCode
"""
self._log_step(24, "更新运单 ETC 开票状态 (tms_order_detail.etc_invoice_status=3)")
order_code = self._get("orderCode")
if not order_code:
print(" [!] orderCode 为空,无法更新 tms_order_detail")
return {"table": "tms_order_detail", "status": "skip_no_vars"}
sql = (f"UPDATE `ntocc`.`tms_order_detail` SET etc_invoice_status = 3 "
f"WHERE order_code = '{order_code}'")
print(f" order_code = {order_code}")
if dry_run:
print(f" [DRY RUN] SQL:\n{sql}")
return {"table": "tms_order_detail", "status": "dry_run"}
try:
rows = self.db.execute(sql)
print(f" [OK] UPDATE 成功 (rows: {rows})")
return {"table": "tms_order_detail", "status": "updated", "rows": rows}
except Exception as exc:
print(f" [FAIL] UPDATE 失败: {exc}")
return {"table": "tms_order_detail", "status": "error", "error": str(exc)}
def run_etc_invoice_flow(self, dry_run: bool = False) -> list[dict]: def run_etc_invoice_flow(self, dry_run: bool = False) -> list[dict]:
"""ETC 开票 SQL 写入(步骤 22-23)""" """ETC 开票 SQL 写入(步骤 22-24)"""
print("\n" + "=" * 60) print("\n" + "=" * 60)
print(" ETC 开票 SQL 写入 (Step 22-23)") print(" ETC 开票 SQL 写入 (Step 22-24)")
print("=" * 60) print("=" * 60)
# 确保 orderCode / code 已就绪(独立运行 --stage receipt 时需要) # 确保 orderCode / code 已就绪(独立运行 --stage receipt 时需要)
self._ensure_code_and_order_code() self._ensure_code_and_order_code()
results = [ results = []
self.step22_insert_etc_invoice(dry_run=dry_run), results.extend(self.step22_insert_etc_invoices(dry_run=dry_run))
self.step23_insert_etc_push(dry_run=dry_run), results.append(self.step23_insert_etc_push(dry_run=dry_run))
] results.append(self.step24_update_order_etc_status(dry_run=dry_run))
return results return results
def _ensure_code_and_order_code(self): def _ensure_code_and_order_code(self):