refactor(api): yun8_sync 并入 tests 分层框架(接口层 apis/yun8 + 用例层 api + Allure)

- tests/core: 新增 BaseHttpClient 统一 HTTP 封装(直连绕行/超时/日志/自定义编码);
  db_helper 追加 Db 长连接类(事务/mogrify/自动重连),既有函数不动
- tests/apis/yun8: 21 个 step 方法按域拆分为 5 个 Mixin(driver/accounting/
  audit_payment/receipt/third_party),Yun8Pipeline 24 步编排原样迁移,
  SQL 全部参数化并集中至 sql_templates
- tests/api: 5 阶段有序 P0 E2E 用例(业务失败由 print 改为真实报错)+
  README 迁移(删除明文密码段,修正步骤编号漂移)
- yun8_sync 目录退役:配置并入 test_config.yaml 的 yun8_api 段;
  造数 CLI 保留(python -m tests.apis.yun8.cli,兼容旧 config.yaml 结构)
- pytest.ini 注册 api/yun8_sync marker;requirements.txt 补 requests>=2.28
- 静态验证: compileall / 全仓 collect 24 条 / marker 过滤 / CLI --help /
  客户端与 pipeline 离线实例化 / SQL 占位符核对 / cross_end 兼容
This commit is contained in:
xst
2026-09-08 11:38:17 +08:00
parent e62e3224bf
commit 52b73f456c
26 changed files with 2147 additions and 1786 deletions
+204
View File
@@ -0,0 +1,204 @@
"""yun8 运输流程造数 CLI。
旧 yun8_sync/main.py 的等价入口,复用 tests/apis/yun8 的 API 层与编排层:
python -m tests.apis.yun8.cli --stage all --repeat 10 --delay 5
注意:
- --dry-run 仅跳过回单/ETC 的 SQL 写入;HTTP 步骤全部真实执行,
driver 阶段的 reset SQL 也不受 dry-run 影响(与旧工具行为一致)。
- 数据库连接读取 tests/config/test_config.yaml 的 database 段。
阶段选择:
all 完整流程 (默认): 司机端 → 核算 → 付款 → 回单SQL → 上报
driver 仅司机端 (步骤 1-5)
accounting 仅运费核算 (步骤 6-9)
payment 仅审核付款 (步骤 10-16)
receipt 仅回单文件+SQL+上报 (步骤 14-17)
"""
import argparse
import sys
import time
import traceback
from datetime import datetime
from pathlib import Path
import yaml
from tests.core.config_loader import load_config
from tests.core.db_helper import Db
from tests.apis.yun8.pipeline import Yun8Pipeline
STAGE_HELP = """阶段选择:
all 完整流程 (默认): 司机端 → 核算 → 付款 → 回单SQL → 上报
driver 仅司机端 (步骤 1-5)
accounting 仅运费核算 (步骤 6-9)
payment 仅审核付款 (步骤 10-16)
receipt 仅回单文件+SQL+上报 (步骤 14-17)
"""
def _load_yun8_section(path: str | Path | None) -> dict:
"""加载 yun8_api 配置段。
- path 为空: 读 tests/config/test_config.yaml 的 yun8_api 段
- path 指向的 YAML 含 yun8_api 键: 直接取该段
- 否则按旧 yun8_sync/config.yaml 结构兼容解析(database 段忽略)
"""
if path is None:
return load_config()["yun8_api"]
with open(path, "r", encoding="utf-8") as fh:
raw = yaml.safe_load(fh)
if "yun8_api" in raw:
return raw["yun8_api"]
# 旧版结构兼容: api/auth/driver_params/... 平铺
api = raw.get("api", {})
section = {
"base_url": api.get("base_url", ""),
"verify_ssl": api.get("verify_ssl", False),
"timeout_seconds": api.get("timeout_seconds", 60),
"common_headers": api.get("common_headers", {}),
"driver_headers": api.get("driver", {}),
"admin_headers": api.get("admin", {}),
"tokens": dict(raw.get("auth", {})),
}
for key in ("driver_params", "accounting", "payment", "etc_invoice",
"sql", "dianyuntong"):
if key in raw:
section[key] = raw[key]
return section
def run_one_cycle(section: dict, stage: str, driver_token: str,
admin_token: str, dry_run: bool) -> bool:
"""执行一次完整流水线,返回 True 表示成功(异常直接向上抛)。"""
db = Db()
pipe = Yun8Pipeline(section, db)
try:
pipe.set_auth(driver_token, admin_token)
if stage in ("all", "driver"):
pipe.run_driver_flow()
# reset_order_detail 模式:仅跑司机端,跳过后续全部步骤
dp = section.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 = section.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)
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
finally:
pipe.close()
db.close()
def main():
parser = argparse.ArgumentParser(
description="yun8 运输流程完整自动化",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=STAGE_HELP,
)
parser.add_argument("--config", default=None,
help="配置文件路径 (默认: tests/config/test_config.yaml 的 yun8_api 段)")
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,不实际写入数据库 (注意: HTTP 步骤仍真实执行)")
parser.add_argument("--repeat", type=int, default=1,
help="循环执行次数 (默认: 1)")
parser.add_argument("--delay", type=int, default=0,
help="每次循环之间的等待秒数 (默认: 0)")
args = parser.parse_args()
# 加载配置
section = _load_yun8_section(args.config)
# 读取 Token(命令行优先)
tokens = section.get("tokens", {})
driver_token = args.auth_driver or tokens.get("driver_token", "")
admin_token = args.auth_admin or tokens.get("admin_token", "")
print("=" * 60)
print(f" yun8 运输流程自动化")
print(f" config: {args.config or 'tests/config/test_config.yaml (yun8_api)'}")
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(section, 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()