Add cross-flow automation test and tone down log/comment wording

- main_router: add run_automation_test that runs each site's download
  flows in a cross sequence (expected/actual) plus a pass/fail report
  with timings; expose it as menu [8]
- Site download functions now return False on their exception paths so
  the test harness can record failures (baishi also flags config-read
  failure)
- Replace overblown log/comment phrasing across all modules with plain
  statements; trim docstrings

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-06-20 23:33:22 +08:00
parent ab3f25a10e
commit 229e474b58
5 changed files with 298 additions and 191 deletions

View File

@@ -10,18 +10,15 @@ from paths import DOWNLOAD_DIR, CONFIG_PATH
def yunda_login(page):
"""
韵达大运系统智能自动登录流
支持‘未登录自动填充提交’与‘已登录静默过客’双工模式
"""
print(">> 正在探测韵达当前的登录会话状态...")
"""韵达自动登录:未登录则填充表单并提交,已登录则跳过。"""
print(">> 正在检查韵达登录状态...")
try:
# 定位“账号密码登录”切换按钮
switch_btn = page.locator("span", has_text="账号密码登录")
# 设定 5 秒探针延迟。如果 5 秒内按钮可见,说明处于未登录的初始状态
# 5 秒内若出现该按钮,说明当前未登录
if switch_btn.is_visible(timeout=5000):
print(" -> 捕获到标准的未登录界面,正在强切至【账号密码登录】模式...")
print(" -> 检测到未登录界面,正在切换到【账号密码登录】...")
switch_btn.click()
page.wait_for_timeout(500)
@@ -35,7 +32,7 @@ def yunda_login(page):
username = str(yd_cfg.get("username", ""))
password = str(yd_cfg.get("password", ""))
print(f" -> 正在程序化充填表单凭证 (账号: {username})...")
print(f" -> 正在填充登录表单 (账号: {username})...")
page.locator("#username").fill(username)
page.locator("#password").fill(password)
page.wait_for_timeout(300)
@@ -45,18 +42,15 @@ def yunda_login(page):
page.wait_for_timeout(1000)
else:
print(
" -> 探针未发现登录按钮,判定当前会话已处于持久化登录状态,静默穿透"
" -> 未发现登录按钮,判定为已登录,跳过"
)
except Exception as e:
print(f" ⚠️ 自动登录状态机探测发生波动(可能已处于工作台内: {e}")
print(f" ⚠️ 登录检测出错(可能已工作台内): {e}")
def yunda_smart_menu_click(page, menu_path):
"""
韵达排他性多级树形菜单智能导航器(带防折叠状态断言机制)
采用 XPath 亲子轴绝对隔离技术,彻底斩断嵌套手风琴菜单的向下漏包干扰
"""
print(f">> 正在智能路由韵达菜单: {' -> '.join(menu_path)}")
"""韵达多级菜单导航:展开父级菜单并点击目标项(已展开则跳过,避免误折叠)。"""
print(f">> 导航韵达菜单: {' -> '.join(menu_path)}")
for item in menu_path:
title_locator = page.locator(
@@ -74,16 +68,16 @@ def yunda_smart_menu_click(page, menu_path):
is_opened = "is-opened" in current_class
if not is_opened:
print(f" -> 探测到父菜单 [{item}] 当前处于收起状态,执行点击展开")
print(f" -> 父菜单 [{item}] 处于收起状态,点击展开")
title_locator.click()
page.wait_for_timeout(500)
else:
print(
f" -> 探测到父菜单 [{item}] 当前已经处于【展开】状态,安全跳过点击(防折叠保护激活)"
f" -> 父菜单 [{item}] 已展开,跳过点击"
)
elif leaf_locator.is_visible():
print(f" -> 成功对焦目标叶子节点 [{item}],直接执行跳转点击")
print(f" -> 点击菜单项 [{item}]")
leaf_locator.click()
page.wait_for_timeout(1000)
@@ -100,20 +94,20 @@ def yunda_expected_download(page):
export_times = []
try:
# 1. 验证首页并智能路由
# 1. 验证首页并导航菜单
page.locator(".el-menu-item", has_text="首页").wait_for(
state="visible", timeout=15000
)
print("✅ 韵达工作台首页成功加载")
print("✅ 韵达工作台首页加载")
yunda_smart_menu_click(page, ["运营管理", "进站管理", "进站交接单查询"])
print(">> 正在跨域动态追踪【进站交接单查询】业务窗体...")
# 彻底移除会引起竞速超时坑的 custom 探测器,使用原生懒加载 frame_locator
print(">> 正在定位【进站交接单查询】iframe...")
# 使用 frame_locator 定位业务 iframe
ws_frame = page.frame_locator("section iframe")
ws_frame.locator("#startTime").wait_for(state="attached", timeout=15000)
print("✅ 进站交接单查询工作区就绪")
print("✅ 进站交接单查询页面就绪")
# 2. 从配置文件中解析并计算绝对日期跨度
query_days = 1
@@ -131,10 +125,10 @@ def yunda_expected_download(page):
today_ymd = f"{today.year}-{today.month}-{today.day}"
start_date_ymd = f"{start_date.year}-{start_date.month}-{start_date.day}"
print(f">> 正在精准对焦时间区间: [{start_date_ymd}] 至 [{today_ymd}]")
print(f">> 设置查询时间范围: [{start_date_ymd}] 至 [{today_ymd}]")
# 设定起始时间
print(" >> 呼出起始时间控件...")
print(" >> 设置起始时间...")
page.wait_for_timeout(1000)
ws_frame.locator("#startTime").click(force=True)
@@ -145,7 +139,7 @@ def yunda_expected_download(page):
page.wait_for_timeout(400)
# 设定截止时间
print(" >> 呼出截止时间控件...")
print(" >> 设置截止时间...")
ws_frame.locator("#endTime").click(force=True)
calendar2 = ws_frame.locator(".layui-laydate:visible").first
@@ -154,21 +148,21 @@ def yunda_expected_download(page):
calendar2.locator(".laydate-btns-confirm").click()
page.wait_for_timeout(500)
# 3. 多维状态机静默加载校验
print(">> 正在触发现场查询数据流...")
# 3. 等待数据加载完成
print(">> 正在执行查询...")
ws_frame.locator("a.btn-success", has_text="查询").click()
page.wait_for_timeout(800)
# ====================================================================
# 🛡️ 核心修复:将 Loading 蒙层数据断限制在当前的 #tab-1 结界内部
# 彻底解决多标签页导致 Strict Mode (5 elements found) 的污染问题
# 将 Loading 蒙层数据断限制在 #tab-1
# 避免多标签页命中多个元素 (Strict Mode)
# ====================================================================
loading_mask = ws_frame.locator(
"#tab-1 .fixed-table-loading", has_text="正在努力地加载数据中"
).first
if loading_mask.is_visible():
print(" ⏳ 检测到专属数据加载罩,正在等待后端重载返回...")
print(" ⏳ 检测到数据加载罩,等待加载完成...")
loading_mask.wait_for(state="hidden", timeout=30000)
page.wait_for_timeout(500)
@@ -182,16 +176,16 @@ def yunda_expected_download(page):
if match_tickets and int(match_tickets.group(1)) > 0:
has_data = True
print(
f"深度断言:局部统计面板加载完毕,实际票数: [{match_tickets.group(1)}],执行穿透。"
f" ✅ 统计面板加载,实际票数: [{match_tickets.group(1)}]"
)
if not has_data:
# 同样将空数据提示隔离在当前 tab
# 空数据提示同样限制在当前 tab
if ws_frame.locator(
"#tab-1 .no-records-found", has_text="没有找到匹配的记录"
).first.is_visible():
print(
" ⚠️ 确认为空数据环境:系统提示【没有找到匹配的记录】,正在执行复位净化..."
" ⚠️ 确认为空数据,正在关闭当前标签页..."
)
page.locator(".tags-view-item", has_text="进站交接单查询").locator(
".el-icon-close"
@@ -207,20 +201,20 @@ def yunda_expected_download(page):
row_count = main_rows.count()
print(f">> 当前视窗共捕获到活跃交接单记录: {row_count}")
# 5. 循环双击穿透提交
# 5. 逐行双击并提交导出
for i in range(row_count):
print(f" ⏳ 正在处理第 {i+1}/{row_count} 个交接单模块...")
current_row = ws_frame.locator("#exampleTable1 tbody tr[data-index]").nth(i)
raw_no = current_row.locator("td").nth(1).inner_text().strip()
# ====================================================================
# 🛡️ 状态机拦截卫语句:识别绑定状态,过滤已绑定交接单
# 跳过已绑定交接单
# ====================================================================
bind_status = current_row.locator("td").nth(2).inner_text().strip()
print(f" -> 锁定提取单号: {raw_no} [绑定状态: {bind_status}]")
print(f" -> 交接单号: {raw_no} [绑定状态: {bind_status}]")
if bind_status == "已绑定":
print(" ⏭️ 状态拦截:该交接单处于【已绑定】状态,安全跳过。")
print(" ⏭️ 该交接单已绑定,跳过。")
continue
# ====================================================================
@@ -248,7 +242,7 @@ def yunda_expected_download(page):
try:
confirm_link.wait_for(state="visible", timeout=6000)
if export_frame.get_by_text("导出任务建立成功").is_visible():
print("判定通过:成功捕获到【导出任务建立成功】特征!")
print("已确认导出任务建立成功")
confirm_link.click()
task_success = True
break
@@ -256,7 +250,7 @@ def yunda_expected_download(page):
"请选择格式相应的导出字段"
).is_visible():
print(
" ⚠️ 警告:检测到未选择字段】错误,重新触发补点全选..."
" ⚠️ 检测到未选择字段,重新点击全选..."
)
confirm_link.click()
page.wait_for_timeout(500)
@@ -270,7 +264,7 @@ def yunda_expected_download(page):
if not task_success:
raise RuntimeError(
"致命异常:连续 5 次尝试均无法成功建立应到数据离线任务。"
"连续 5 次尝试均未能建立应到数据离线任务。"
)
ws_frame.locator(".layui-layer-close1").click()
@@ -279,16 +273,16 @@ def yunda_expected_download(page):
page.wait_for_timeout(800)
export_times.append(datetime.now())
print(">> 📤 任务提交流闭环,正在执行【进站交接单查询】工作台销毁...")
print(">> 任务提交完成,正在关闭【进站交接单查询】标签页...")
page.locator(".tags-view-item", has_text="进站交接单查询").locator(
".el-icon-close"
).click()
page.wait_for_timeout(500)
# 防线:如果全部记录都被跳过export_times 为空,直接结束
# 若所有记录都被跳过export_times 为空,直接结束
if not export_times:
print(
">> ⚠️ 本次查询未产生任何有效的离线下载任务(全部空单或已被跳过),中止后端收割流"
">> ⚠️ 本次未产生任何离线下载任务(无数据或已全部跳过),结束"
)
return
@@ -302,6 +296,7 @@ def yunda_expected_download(page):
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False
def yunda_actual_download(page):
@@ -319,17 +314,17 @@ def yunda_actual_download(page):
page.locator(".el-menu-item", has_text="首页").wait_for(
state="visible", timeout=15000
)
print("✅ 韵达工作台首页成功加载")
print("✅ 韵达工作台首页加载")
yunda_smart_menu_click(page, ["报表管理", "扫描记录查询"])
print(">> 正在跨域动态追踪【扫描记录查询】业务窗体...")
print(">> 正在定位【扫描记录查询】iframe...")
ws_frame = page.frame_locator("section iframe")
ws_frame.locator(
".no-records-found", has_text="没有找到匹配的记录"
).first.wait_for(state="visible", timeout=15000)
print("✅ 扫描记录查询工作区初始化完毕")
print("✅ 扫描记录查询页面已初始化")
query_days = 1
try:
@@ -343,7 +338,7 @@ def yunda_actual_download(page):
today = datetime.now()
start_date = today - timedelta(days=(query_days - 1))
print(f">> 正在精准对焦实到时间区间: [近 {query_days} 天]")
print(f">> 设置实到查询时间范围: [近 {query_days} 天]")
print(" >> 正在设定起始时间...")
ws_frame.locator("#startDate").click()
@@ -367,7 +362,7 @@ def yunda_actual_download(page):
ws_frame.locator("#scanRecordTyp").select_option(value="03")
page.wait_for_timeout(500)
print(">> 正在触发现场查询数据流...")
print(">> 正在执行查询...")
ws_frame.locator('input[type="button"][value="查询"]').click()
page.wait_for_timeout(800)
@@ -376,7 +371,7 @@ def yunda_actual_download(page):
".fixed-table-loading", has_text="正在努力地加载数据中"
).first
if loading_mask.is_visible():
print(" ⏳ 检测到专属数据加载罩,正在等待后端重载返回...")
print(" ⏳ 检测到数据加载罩,等待加载完成...")
loading_mask.wait_for(state="hidden", timeout=30000)
page.wait_for_timeout(500)
@@ -390,7 +385,7 @@ def yunda_actual_download(page):
if match_total and int(match_total.group(1)) > 0:
has_records = True
print(
f"深度断言:实到数据渲染完毕,总记录数: [{match_total.group(1)}] 条。"
f"实到数据已加载,总记录数: [{match_total.group(1)}] 条。"
)
if not has_records:
@@ -398,21 +393,21 @@ def yunda_actual_download(page):
".no-records-found", has_text="没有找到匹配的记录"
).first.is_visible():
print(
" ⚠️ 当前查询范围内确认为空数据:系统提示【没有找到匹配的记录】,终止并关闭环境"
" ⚠️ 当前查询范围内为空数据,终止并关闭标签页"
)
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
".el-icon-close"
).click()
return
else:
print(" ⚠️ 发生渲染异常,未找到数据也未找到空记录提示。安全收尾...")
print(" ⚠️ 未找到数据也未出现空数据提示,结束。")
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
".el-icon-close"
).click()
return
# 5. 执行导出流
print(">> 正在发起导出】申请指令...")
print(">> 正在发起导出...")
ws_frame.locator('input[type="button"][id="export"]').click()
ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for(
@@ -431,12 +426,12 @@ def yunda_actual_download(page):
try:
confirm_link.wait_for(state="visible", timeout=6000)
if export_frame.get_by_text("导出任务建立成功").is_visible():
print("判定通过:成功捕获到【导出任务建立成功】特征!")
print("已确认导出任务建立成功")
confirm_link.click()
task_success = True
break
elif export_frame.get_by_text("请选择格式相应的导出字段").is_visible():
print(" ⚠️ 警告:检测到字段未全选,执行强补点击...")
print(" ⚠️ 检测到字段未全选,重新点击全选...")
confirm_link.click()
page.wait_for_timeout(500)
export_frame.locator(".allRight").click()
@@ -449,14 +444,14 @@ def yunda_actual_download(page):
if not task_success:
raise RuntimeError(
"致命异常:连续 5 次尝试均无法成功建立实到数据离线任务。"
"连续 5 次尝试均未能建立实到数据离线任务。"
)
ws_frame.locator(".layui-layer-close1").click()
page.wait_for_timeout(500)
export_times.append(datetime.now())
print(">> 📤 任务提交流闭环,正在执行【扫描记录查询】工作台销毁...")
print(">> 任务提交完成,正在关闭【扫描记录查询】标签页...")
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
".el-icon-close"
).click()
@@ -464,10 +459,10 @@ def yunda_actual_download(page):
# 防线:双重保护
if not export_times:
print(">> ⚠️ 本次查询未产生有效的离线下载任务,中止后端收割流")
print(">> ⚠️ 本次未产生任何离线下载任务,结束")
return
# 6. 收割下载
# 6. 轮询并下载
_yunda_poll_and_download_tasks(
page,
export_times,
@@ -478,13 +473,14 @@ def yunda_actual_download(page):
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False
def _yunda_poll_and_download_tasks(
page, export_times, target_task_title, download_dir, final_filename
):
"""韵达专属离线任务轮询下载引擎"""
print("\n>> 正在前往【导出服务】中心...")
"""韵达离线任务轮询下载"""
print("\n>> 正在前往【导出服务】界面...")
yunda_smart_menu_click(page, ["基础数据", "导出服务"])
export_ws_frame = page.frame_locator("section iframe")
@@ -494,7 +490,7 @@ def _yunda_poll_and_download_tasks(
)
page.wait_for_timeout(1000)
print(">> 离线文件队列已对接启动【1分钟高频精确校对+缺单局部重载刷新】断言...")
print(">> 开始轮询离线文件队列(定时刷新直到任务齐全)...")
total_expected = len(export_times)
while True:
@@ -532,17 +528,17 @@ def _yunda_poll_and_download_tasks(
total_found = len(ready_indices) + len(processing_indices)
print(
f" 📊 状态研判:自建期望 [{total_expected}]实际入表 [{total_found}] (完成 [{len(ready_indices)}],生成中 [{len(processing_indices)}])"
f" 📊 状态统计:期望 [{total_expected}]入表 [{total_found}] (完成 [{len(ready_indices)}],生成中 [{len(processing_indices)}])"
)
if total_found < total_expected or len(processing_indices) > 0:
print(" ⏳ 队列未齐,执行【点击查询按钮】触发局部无痕刷新...")
print(" ⏳ 队列未齐全,点击查询刷新...")
export_ws_frame.locator(
"#ydkyimport_basic_export_searchData1_ky_export_common"
).click()
page.wait_for_timeout(3000)
else:
print(">> 所有目标离线任务全量就绪开始依序接入文件流...")
print(">> 所有目标离线任务就绪开始依次下载...")
break
downloaded_files = []
@@ -554,7 +550,7 @@ def _yunda_poll_and_download_tasks(
time_flag = (
target_row.locator("td[field='createdTime']").inner_text().strip()
)
print(f" 🎯 触发下载 -> 离线任务时间节点: [{time_flag}] ...")
print(f" 开始下载任务 [{time_flag}] ...")
with page.expect_download() as download_info:
target_row.locator("td[field='extreFile'] a").get_by_text(
@@ -569,22 +565,22 @@ def _yunda_poll_and_download_tasks(
download.save_as(save_path)
downloaded_files.append(save_path)
print(f" ⬇️ 文件已用安全序列号落盘: downloads/{custom_filename}")
print(f" 已下载: downloads/{custom_filename}")
page.wait_for_timeout(500)
except Exception as e:
print(f"文件流接收失败: {e}")
print(f"下载失败: {e}")
print(">> 📥 【导出服务】数据提取链闭环,正在执行当前 Tab 窗口销毁...")
print(">> 【导出服务】下载完成,正在关闭标签页...")
try:
page.locator(".tags-view-item", has_text="导出服务").locator(
".el-icon-close"
).click()
print(" ✅ 【导出服务】工作区已安全关闭。")
print(" ✅ 【导出服务】标签页已关闭。")
except Exception:
pass
if downloaded_files:
print("\n>> 🧪 正在启动离线数据清洗与高能扁平合并流...")
print("\n>> 正在合并下载的数据...")
all_dfs = []
for file_path in downloaded_files:
try:
@@ -599,10 +595,10 @@ def _yunda_poll_and_download_tasks(
final_output = os.path.join(download_dir, final_filename)
combined_df.to_excel(final_output, index=False)
print(f"====================================================")
print(f" 🎉 恭喜!韵达网点数据拉取合流大功告成!")
print(f" 📁 最终存储位置: {final_output}")
print(f" 合并完成。")
print(f" 📁 输出路径: {final_output}")
print(f"====================================================")
for file_path in downloaded_files:
os.remove(file_path)
print(" ✅ 临时缓存阵列已无缝净化")
print(" 临时文件已清理")