成败信号统一化(顺心/中通/韵达/安能/百世): - 各站 impl 开头清理上次的最终文件,避免无数据/失败时残留旧数据误导比对 - 轮询下载段加汇总校验:下载数 < 预期则 raise,堵住"零/部分下载被判成功" - 正常完成统一显式 return True(原中通/韵达/安能靠 None 隐式成功) - 无数据统一不落文件(安能实到不再落空 xlsx);韵达裸 except 改 except Exception - 中通"票数0无空提示"分支改判失败 中通 bug 修复(site_zto.py): - poll 加 stall 快速失败(连续4轮无进展即 raise)+ 刷新短超时 + 双失败 raise, 避免页面被遮挡时干等 5 分钟才触发重试 - "生成离线导出任务成功"提示所在 iframe 提交后被销毁,wait_for 抛 "Frame was detached" 属正常(提示已随 iframe 消失=任务已建立),改判成功 Co-Authored-By: Claude <noreply@anthropic.com>
744 lines
30 KiB
Python
744 lines
30 KiB
Python
# site_yunda.py
|
||
|
||
import os
|
||
import re
|
||
import time
|
||
import yaml
|
||
from datetime import datetime, timedelta
|
||
import pandas as pd
|
||
|
||
from paths import DOWNLOAD_DIR, CONFIG_PATH
|
||
|
||
|
||
def with_retry(site_name, label, flow, reset, max_attempts=3):
|
||
"""异常兜底:flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
|
||
|
||
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
|
||
flow 为零参可调用;返回 False 视为失败,其余视为成功。
|
||
"""
|
||
for attempt in range(1, max_attempts + 1):
|
||
try:
|
||
ret = flow()
|
||
if ret is False:
|
||
raise RuntimeError("流程返回失败状态")
|
||
if attempt > 1:
|
||
print(f">> 【{site_name}-{label}】第 {attempt} 次尝试成功 ✅")
|
||
return
|
||
except Exception as e:
|
||
print(f"⚠️ 【{site_name}-{label}】第 {attempt}/{max_attempts} 次失败: {e}")
|
||
print(f" → 重置【{site_name}】到初始态,清理环境 ...")
|
||
try:
|
||
reset()
|
||
except Exception as re:
|
||
print(f" ⚠️ 重置异常: {re}")
|
||
if attempt < max_attempts:
|
||
continue
|
||
print(
|
||
f"❌ 【{site_name}-{label}】已达最大尝试次数 {max_attempts},放弃(环境已清理)。"
|
||
)
|
||
|
||
|
||
# 站点首页 URL:异常兜底重置用,也供 main_router 的 SITES_CONFIG 引用(单一来源)
|
||
HOME_URL = "https://ky-sso.yunda56.com"
|
||
|
||
|
||
def yunda_reset(page):
|
||
"""异常兜底:重置韵达到初始态(跳首页 URL,丢弃当前页面状态,登录态保留)。"""
|
||
page.goto(HOME_URL)
|
||
page.wait_for_timeout(1500)
|
||
|
||
|
||
def _remove_if_exists(path):
|
||
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
|
||
try:
|
||
if os.path.exists(path):
|
||
os.remove(path)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def yunda_login(page):
|
||
"""韵达自动登录:未登录则填充表单并提交,已登录则跳过。"""
|
||
print(">> 正在检查韵达登录状态...")
|
||
try:
|
||
# 定位“账号密码登录”切换按钮
|
||
switch_btn = page.locator("span", has_text="账号密码登录")
|
||
|
||
# 5 秒内若出现该按钮,说明当前未登录
|
||
if switch_btn.is_visible(timeout=5000):
|
||
print(" -> 检测到未登录界面,正在切换到【账号密码登录】...")
|
||
switch_btn.click()
|
||
page.wait_for_timeout(500)
|
||
|
||
# 从配置读取凭证(默认空串;真实凭据仅存于被忽略的 config.yaml)
|
||
username = ""
|
||
password = ""
|
||
if os.path.exists(CONFIG_PATH):
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
config = yaml.safe_load(f) or {}
|
||
yd_cfg = config.get("yunda", {})
|
||
username = str(yd_cfg.get("username", ""))
|
||
password = str(yd_cfg.get("password", ""))
|
||
|
||
print(f" -> 正在填充登录表单 (账号: {username})...")
|
||
page.locator("#username").fill(username)
|
||
page.locator("#password").fill(password)
|
||
page.wait_for_timeout(300)
|
||
|
||
print(" -> 正在点击【登录】按钮并提交表单...")
|
||
page.locator('button[type="submit"]', has_text="登录").click()
|
||
page.wait_for_timeout(1000)
|
||
else:
|
||
print(" -> 未发现登录按钮,判定为已登录,跳过。")
|
||
except Exception as e:
|
||
print(f" ⚠️ 登录检测出错(可能已在工作台内): {e}")
|
||
|
||
|
||
def yunda_smart_menu_click(page, menu_path):
|
||
"""韵达多级菜单导航:展开父级菜单并点击目标项(已展开则跳过,避免误折叠)。"""
|
||
print(f">> 导航韵达菜单: {' -> '.join(menu_path)}")
|
||
|
||
for item in menu_path:
|
||
title_locator = page.locator(
|
||
f"xpath=//div[contains(@class, 'el-submenu__title') and .//span[normalize-space(.)='{item}']]"
|
||
).first
|
||
parent_li = page.locator(
|
||
f"xpath=//div[contains(@class, 'el-submenu__title') and .//span[normalize-space(.)='{item}']]/.."
|
||
).first
|
||
leaf_locator = page.locator(
|
||
f"xpath=//li[contains(@class, 'el-menu-item') and .//span[normalize-space(.)='{item}']]"
|
||
).first
|
||
|
||
if title_locator.is_visible():
|
||
current_class = parent_li.get_attribute("class") or ""
|
||
is_opened = "is-opened" in current_class
|
||
|
||
if not is_opened:
|
||
print(f" -> 父菜单 [{item}] 处于收起状态,点击展开")
|
||
title_locator.click()
|
||
page.wait_for_timeout(500)
|
||
else:
|
||
print(f" -> 父菜单 [{item}] 已展开,跳过点击")
|
||
|
||
elif leaf_locator.is_visible():
|
||
print(f" -> 点击菜单项 [{item}]")
|
||
leaf_locator.click()
|
||
page.wait_for_timeout(1000)
|
||
|
||
|
||
def yunda_expected_download(page):
|
||
"""韵达:应到货物数据下载(内部含异常兜底重试,路由层无感)。"""
|
||
|
||
return with_retry(
|
||
"韵达",
|
||
"应到",
|
||
lambda: yunda_expected_download_impl(page),
|
||
lambda: yunda_reset(page),
|
||
)
|
||
|
||
|
||
def yunda_expected_download_impl(page):
|
||
"""韵达:应到货物数据下载(单次执行,无重试;供自动化测试探测原始结果用)。"""
|
||
print("\n▶ 开始执行【韵达 - 应到货物数据下载】任务...")
|
||
|
||
download_dir = DOWNLOAD_DIR
|
||
if not os.path.exists(download_dir):
|
||
os.makedirs(download_dir)
|
||
|
||
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
|
||
_remove_if_exists(os.path.join(download_dir, "韵达-应到货物数据.xlsx"))
|
||
|
||
export_times = []
|
||
|
||
try:
|
||
# 1. 验证首页并导航菜单
|
||
page.locator(".el-menu-item", has_text="首页").wait_for(
|
||
state="visible", timeout=15000
|
||
)
|
||
print("✅ 韵达工作台首页已加载")
|
||
|
||
yunda_smart_menu_click(page, ["运营管理", "进站管理", "进站交接单查询"])
|
||
|
||
print(">> 正在定位【进站交接单查询】iframe...")
|
||
ws_frame = page.frame_locator("section iframe")
|
||
|
||
ws_frame.locator("#startTime").wait_for(state="attached", timeout=15000)
|
||
print("✅ 进站交接单查询页面就绪")
|
||
|
||
# 2. 从配置文件中解析并计算绝对日期跨度
|
||
query_days = 1
|
||
try:
|
||
if os.path.exists(CONFIG_PATH):
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
config = yaml.safe_load(f) or {}
|
||
query_days = int(config.get("yunda", {}).get("query_days", 1))
|
||
except Exception as e:
|
||
print(f" ⚠️ 读取 config.yaml 失败,默认查询 1 天: {e}")
|
||
|
||
today = datetime.now()
|
||
start_date = today - timedelta(days=(query_days - 1))
|
||
|
||
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(" >> 设置起始时间...")
|
||
page.wait_for_timeout(1000)
|
||
ws_frame.locator("#startTime").click(force=True)
|
||
|
||
calendar1 = ws_frame.locator(".layui-laydate:visible").first
|
||
calendar1.wait_for(state="visible", timeout=5000)
|
||
calendar1.locator(f"td[lay-ymd='{start_date_ymd}']").click()
|
||
calendar1.locator(".laydate-btns-confirm").click()
|
||
page.wait_for_timeout(400)
|
||
|
||
# 设定截止时间
|
||
print(" >> 设置截止时间...")
|
||
ws_frame.locator("#endTime").click(force=True)
|
||
|
||
calendar2 = ws_frame.locator(".layui-laydate:visible").first
|
||
calendar2.wait_for(state="visible", timeout=5000)
|
||
calendar2.locator(f"td[lay-ymd='{today_ymd}']").click()
|
||
calendar2.locator(".laydate-btns-confirm").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
# 3. 等待数据加载完成
|
||
print(">> 正在执行查询...")
|
||
ws_frame.locator("a.btn-success", has_text="查询").click()
|
||
|
||
page.wait_for_timeout(800)
|
||
|
||
# 将 Loading 蒙层与数据判断限制在 #tab-1 内
|
||
loading_mask = ws_frame.locator(
|
||
"#tab-1 .fixed-table-loading", has_text="正在努力地加载数据中"
|
||
).first
|
||
if loading_mask.is_visible():
|
||
print(" ⏳ 检测到数据加载遮罩,等待加载完成...")
|
||
loading_mask.wait_for(state="hidden", timeout=30000)
|
||
|
||
page.wait_for_timeout(500)
|
||
|
||
sum_panel = ws_frame.locator("#sum").first
|
||
has_data = False
|
||
|
||
if sum_panel.is_visible():
|
||
sum_text = sum_panel.inner_text()
|
||
match_tickets = re.search(r"进站实际票数:(\d+)", sum_text)
|
||
if match_tickets and int(match_tickets.group(1)) > 0:
|
||
has_data = True
|
||
print(f" ✅ 统计面板已加载,实际票数: [{match_tickets.group(1)}]")
|
||
|
||
if not has_data:
|
||
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"
|
||
).click()
|
||
return
|
||
|
||
# 4. 深度等待表格第一行数据行渲染就绪
|
||
ws_frame.locator("#exampleTable1 tbody tr[data-index='0']").wait_for(
|
||
state="visible", timeout=10000
|
||
)
|
||
|
||
main_rows = ws_frame.locator("#exampleTable1 tbody tr[data-index]")
|
||
row_count = main_rows.count()
|
||
print(f">> 当前视窗共捕获到活跃交接单记录: {row_count} 条")
|
||
|
||
# 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}]")
|
||
|
||
if bind_status == "已绑定":
|
||
print(" ⏭️ 该交接单已绑定,跳过。")
|
||
continue
|
||
|
||
current_row.dblclick()
|
||
|
||
ws_frame.locator("#docSum").wait_for(state="visible", timeout=15000)
|
||
page.wait_for_timeout(500)
|
||
|
||
# 导出弹窗双层重试:外层重新打开面板,内层重新提交。
|
||
# 区分字段漏选(补点全选)与字段列表消失(重新打开面板)。
|
||
task_success = False
|
||
for major_attempt in range(3):
|
||
print(f" >> 正在打开数据导出面板 (尝试 {major_attempt + 1}/3)...")
|
||
ws_frame.locator('a.btn-info[onclick*="exportFile"]').click()
|
||
|
||
ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for(
|
||
state="visible", timeout=15000
|
||
)
|
||
|
||
export_frame = ws_frame.frame_locator('iframe[name="target1"]')
|
||
|
||
try:
|
||
# 校验字段列表是否加载完成(以“交接单号”为标志)
|
||
export_frame.get_by_text("交接单号").first.wait_for(
|
||
state="visible", timeout=3000
|
||
)
|
||
except Exception:
|
||
print(" ⚠️ 字段列表未加载,关闭面板后重试...")
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
|
||
export_frame.locator(".allRight").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
inner_success = False
|
||
needs_reopen = False
|
||
|
||
print(" -> 正在提交导出任务...")
|
||
for attempt in range(4):
|
||
export_frame.locator("#submitbutton", has_text="导出数据").click()
|
||
confirm_link = export_frame.get_by_role("link", name="确定")
|
||
try:
|
||
confirm_link.wait_for(state="visible", timeout=6000)
|
||
if export_frame.get_by_text("导出任务建立成功").is_visible():
|
||
print(" ✅ 导出任务已建立成功。")
|
||
confirm_link.click()
|
||
inner_success = True
|
||
break
|
||
elif export_frame.get_by_text(
|
||
"请选择格式相应的导出字段"
|
||
).is_visible():
|
||
confirm_link.click()
|
||
page.wait_for_timeout(500)
|
||
|
||
# 区分:字段漏选 还是 字段列表消失
|
||
if export_frame.get_by_text("交接单号").first.is_visible():
|
||
print(
|
||
" ⚠️ 检测到未选择字段(字段列表仍在),重新点击全选..."
|
||
)
|
||
export_frame.locator(".allRight").click()
|
||
page.wait_for_timeout(500)
|
||
else:
|
||
print(
|
||
" ⚠️ 字段列表异常消失,重新打开导出面板..."
|
||
)
|
||
needs_reopen = True
|
||
break # 跳出内层循环,重新打开面板
|
||
else:
|
||
confirm_link.click()
|
||
page.wait_for_timeout(1000)
|
||
except Exception:
|
||
page.wait_for_timeout(1000)
|
||
|
||
if inner_success:
|
||
task_success = True
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(500)
|
||
break # 跳出外层循环,继续后续步骤
|
||
elif needs_reopen:
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue # 重新打开面板
|
||
else:
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
|
||
if not task_success:
|
||
raise RuntimeError("多次重试后仍未能建立应到数据离线任务。")
|
||
|
||
ws_frame.locator("#myTab a", has_text="交接单信息").click()
|
||
page.wait_for_timeout(800)
|
||
export_times.append(datetime.now())
|
||
|
||
print(">> 任务提交完成,正在关闭【进站交接单查询】标签页...")
|
||
page.locator(".tags-view-item", has_text="进站交接单查询").locator(
|
||
".el-icon-close"
|
||
).click()
|
||
page.wait_for_timeout(500)
|
||
|
||
# 若所有记录都被跳过,export_times 为空,直接结束
|
||
if not export_times:
|
||
print(">> ⚠️ 本次未产生任何离线下载任务(无数据或已全部跳过),结束。")
|
||
return
|
||
|
||
_yunda_poll_and_download_tasks(
|
||
page,
|
||
export_times,
|
||
download_dir,
|
||
"韵达-应到货物数据.xlsx",
|
||
)
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ 任务执行过程中发生异常: {e}")
|
||
return False
|
||
|
||
|
||
def yunda_actual_download(page):
|
||
"""韵达:实到货物数据下载(内部含异常兜底重试,路由层无感)。"""
|
||
|
||
return with_retry(
|
||
"韵达",
|
||
"实到",
|
||
lambda: yunda_actual_download_impl(page),
|
||
lambda: yunda_reset(page),
|
||
)
|
||
|
||
|
||
def yunda_actual_download_impl(page):
|
||
"""韵达:实到货物数据下载(单次执行,无重试;供自动化测试探测原始结果用)。"""
|
||
print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...")
|
||
|
||
download_dir = DOWNLOAD_DIR
|
||
if not os.path.exists(download_dir):
|
||
os.makedirs(download_dir)
|
||
|
||
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
|
||
_remove_if_exists(os.path.join(download_dir, "韵达-实到货物数据.xlsx"))
|
||
|
||
export_times = []
|
||
|
||
try:
|
||
page.locator(".el-menu-item", has_text="首页").wait_for(
|
||
state="visible", timeout=15000
|
||
)
|
||
print("✅ 韵达工作台首页已加载")
|
||
|
||
yunda_smart_menu_click(page, ["报表管理", "扫描记录查询"])
|
||
|
||
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("✅ 扫描记录查询页面已初始化")
|
||
|
||
query_days = 1
|
||
try:
|
||
if os.path.exists(CONFIG_PATH):
|
||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||
config = yaml.safe_load(f) or {}
|
||
query_days = int(config.get("yunda", {}).get("query_days", 1))
|
||
except Exception:
|
||
pass
|
||
|
||
today = datetime.now()
|
||
start_date = today - timedelta(days=(query_days - 1))
|
||
|
||
print(f">> 设置实到查询时间范围: [近 {query_days} 天]")
|
||
|
||
print(" >> 正在设定起始时间...")
|
||
ws_frame.locator("#startDate").click()
|
||
page.wait_for_timeout(400)
|
||
box1 = ws_frame.locator("#laydate_box:visible").first
|
||
box1.locator(
|
||
f"td[y='{start_date.year}'][m='{start_date.month}'][d='{start_date.day}']"
|
||
).click()
|
||
page.wait_for_timeout(400)
|
||
|
||
print(" >> 正在设定截止时间...")
|
||
ws_frame.locator("#endDate").click()
|
||
page.wait_for_timeout(400)
|
||
box2 = ws_frame.locator("#laydate_box:visible").first
|
||
box2.locator(
|
||
f"td[y='{today.year}'][m='{today.month}'][d='{today.day}']"
|
||
).click()
|
||
page.wait_for_timeout(500)
|
||
|
||
print(" >> 正在变更扫描类型为【到件】...")
|
||
ws_frame.locator("#scanRecordTyp").select_option(value="03")
|
||
page.wait_for_timeout(500)
|
||
|
||
print(">> 正在执行查询...")
|
||
ws_frame.locator('input[type="button"][value="查询"]').click()
|
||
|
||
page.wait_for_timeout(800)
|
||
|
||
loading_mask = ws_frame.locator(
|
||
".fixed-table-loading", has_text="正在努力地加载数据中"
|
||
).first
|
||
if loading_mask.is_visible():
|
||
print(" ⏳ 检测到数据加载遮罩,等待加载完成...")
|
||
loading_mask.wait_for(state="hidden", timeout=30000)
|
||
|
||
page.wait_for_timeout(500)
|
||
|
||
pg_info = ws_frame.locator(".pagination-info").first
|
||
has_records = False
|
||
|
||
if pg_info.is_visible():
|
||
info_text = pg_info.inner_text()
|
||
match_total = re.search(r"总共\s*(\d+)\s*条记录", info_text)
|
||
if match_total and int(match_total.group(1)) > 0:
|
||
has_records = True
|
||
print(f" ✅ 实到数据已加载,总记录数: [{match_total.group(1)}] 条。")
|
||
|
||
if not has_records:
|
||
if ws_frame.locator(
|
||
".no-records-found", has_text="没有找到匹配的记录"
|
||
).first.is_visible():
|
||
print(" ⚠️ 当前查询范围内为空数据,终止并关闭标签页。")
|
||
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
|
||
".el-icon-close"
|
||
).click()
|
||
return
|
||
else:
|
||
print(" ⚠️ 未找到数据,也未出现空数据提示,结束。")
|
||
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
|
||
".el-icon-close"
|
||
).click()
|
||
return
|
||
|
||
# 导出弹窗双层重试:外层重新打开面板,内层重新提交。
|
||
# 区分字段漏选(补点全选)与字段列表消失(重新打开面板)。
|
||
print(">> 正在发起导出...")
|
||
task_success = False
|
||
|
||
for major_attempt in range(3):
|
||
print(f" >> 正在打开数据导出面板 (尝试 {major_attempt + 1}/3)...")
|
||
ws_frame.locator('input[type="button"][id="export"]').click()
|
||
|
||
ws_frame.locator(".layui-layer-title", has_text="数据导出").wait_for(
|
||
state="visible", timeout=15000
|
||
)
|
||
export_frame = ws_frame.frame_locator('iframe[name="myFrame"]')
|
||
|
||
try:
|
||
# 校验字段列表是否加载完成(以“扫描类型”为标志)
|
||
export_frame.get_by_text("扫描类型").first.wait_for(
|
||
state="visible", timeout=3000
|
||
)
|
||
except Exception:
|
||
print(" ⚠️ 字段列表未加载,关闭面板后重试...")
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
|
||
export_frame.locator(".allRight").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
inner_success = False
|
||
needs_reopen = False
|
||
|
||
print(" -> 正在提交导出任务...")
|
||
for attempt in range(4):
|
||
export_frame.locator("#submitbutton", has_text="导出数据").click()
|
||
confirm_link = export_frame.get_by_role("link", name="确定")
|
||
try:
|
||
confirm_link.wait_for(state="visible", timeout=6000)
|
||
if export_frame.get_by_text("导出任务建立成功").is_visible():
|
||
print(" ✅ 导出任务已建立成功。")
|
||
confirm_link.click()
|
||
inner_success = True
|
||
break
|
||
elif export_frame.get_by_text(
|
||
"请选择格式相应的导出字段"
|
||
).is_visible():
|
||
confirm_link.click()
|
||
page.wait_for_timeout(500)
|
||
|
||
# 区分:字段漏选 还是 字段列表消失
|
||
if export_frame.get_by_text("扫描类型").first.is_visible():
|
||
print(
|
||
" ⚠️ 检测到未选择字段(字段列表仍在),重新点击全选..."
|
||
)
|
||
export_frame.locator(".allRight").click()
|
||
page.wait_for_timeout(500)
|
||
else:
|
||
print(" ⚠️ 字段列表异常消失,重新打开导出面板...")
|
||
needs_reopen = True
|
||
break
|
||
else:
|
||
confirm_link.click()
|
||
page.wait_for_timeout(1000)
|
||
except Exception:
|
||
page.wait_for_timeout(1000)
|
||
|
||
if inner_success:
|
||
task_success = True
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(500)
|
||
break
|
||
elif needs_reopen:
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
else:
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
|
||
if not task_success:
|
||
raise RuntimeError("多次重试后仍未能建立实到数据离线任务。")
|
||
|
||
export_times.append(datetime.now())
|
||
|
||
print(">> 任务提交完成,正在关闭【扫描记录查询】标签页...")
|
||
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
|
||
".el-icon-close"
|
||
).click()
|
||
page.wait_for_timeout(500)
|
||
|
||
# 若未产生导出任务则结束
|
||
if not export_times:
|
||
print(">> ⚠️ 本次未产生离线下载任务,结束。")
|
||
return
|
||
|
||
# 6. 轮询并下载
|
||
_yunda_poll_and_download_tasks(
|
||
page,
|
||
export_times,
|
||
download_dir,
|
||
"韵达-实到货物数据.xlsx",
|
||
)
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"\n❌ 任务执行过程中发生异常: {e}")
|
||
return False
|
||
|
||
|
||
def _yunda_poll_and_download_tasks(page, export_times, download_dir, final_filename):
|
||
"""韵达离线任务的轮询与下载"""
|
||
print("\n>> 正在前往【导出服务】界面...")
|
||
yunda_smart_menu_click(page, ["基础数据", "导出服务"])
|
||
|
||
export_ws_frame = page.frame_locator("section iframe")
|
||
|
||
export_ws_frame.get_by_role("cell", name="模块名称", exact=True).wait_for(
|
||
state="visible", timeout=15000
|
||
)
|
||
page.wait_for_timeout(1000)
|
||
|
||
print(">> 开始轮询离线任务队列,直到全部完成...")
|
||
total_expected = len(export_times)
|
||
|
||
poll_deadline = (
|
||
time.monotonic() + 300
|
||
) # 5 分钟上限:任务卡死/匹配不上时超时失败,交由上层重置重试
|
||
while True:
|
||
if time.monotonic() > poll_deadline:
|
||
raise RuntimeError(
|
||
"轮询导出任务超时(5 分钟未全部完成/匹配),疑似任务卡死"
|
||
)
|
||
task_rows = export_ws_frame.locator(
|
||
".datagrid-view2 .datagrid-btable tbody tr.datagrid-row"
|
||
)
|
||
row_count = task_rows.count()
|
||
|
||
ready_indices = []
|
||
processing_indices = []
|
||
|
||
# 单账号无并发:仅按创建时间容差(40s)认领本批任务,不再校验模块名称
|
||
# (站点可能调整标题名,写死标题会导致匹配失败、任务一直查不到)。
|
||
for idx in range(row_count):
|
||
row = task_rows.nth(idx)
|
||
status_name = row.locator("td[field='fileStatus']").inner_text().strip()
|
||
create_time_str = (
|
||
row.locator("td[field='createdTime']").inner_text().strip()
|
||
)
|
||
|
||
try:
|
||
row_time = datetime.strptime(create_time_str, "%Y-%m-%d %H:%M:%S")
|
||
matched = any(
|
||
abs((row_time - et).total_seconds()) <= 40 for et in export_times
|
||
)
|
||
|
||
if matched:
|
||
if status_name == "导出完成":
|
||
ready_indices.append(idx)
|
||
else:
|
||
processing_indices.append(idx)
|
||
except Exception:
|
||
pass
|
||
|
||
total_found = len(ready_indices) + len(processing_indices)
|
||
print(
|
||
f" 📊 状态统计:期望 [{total_expected}],已入表 [{total_found}] (完成 [{len(ready_indices)}],生成中 [{len(processing_indices)}])"
|
||
)
|
||
|
||
if total_found < total_expected or len(processing_indices) > 0:
|
||
print(" ⏳ 队列未齐全,点击查询刷新...")
|
||
export_ws_frame.locator(
|
||
"#ydkyimport_basic_export_searchData1_ky_export_common"
|
||
).click()
|
||
page.wait_for_timeout(3000)
|
||
else:
|
||
print(">> 所有离线任务已就绪,开始依次下载...")
|
||
break
|
||
|
||
downloaded_files = []
|
||
for row_idx in ready_indices:
|
||
try:
|
||
target_row = export_ws_frame.locator(
|
||
".datagrid-view2 .datagrid-btable tbody tr.datagrid-row"
|
||
).nth(row_idx)
|
||
time_flag = (
|
||
target_row.locator("td[field='createdTime']").inner_text().strip()
|
||
)
|
||
print(f" 开始下载任务 [{time_flag}] ...")
|
||
|
||
with page.expect_download() as download_info:
|
||
target_row.locator("td[field='extreFile'] a").get_by_text(
|
||
"下载"
|
||
).first.click()
|
||
|
||
download = download_info.value
|
||
|
||
safe_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||
custom_filename = f"韵达_temp_{safe_timestamp}.xlsx"
|
||
save_path = os.path.join(download_dir, custom_filename)
|
||
|
||
download.save_as(save_path)
|
||
downloaded_files.append(save_path)
|
||
print(f" 已下载: downloads/{custom_filename}")
|
||
page.wait_for_timeout(500)
|
||
except Exception as e:
|
||
print(f" ❌ 下载失败: {e}")
|
||
|
||
# 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败
|
||
if len(downloaded_files) < total_expected:
|
||
raise RuntimeError(
|
||
f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整"
|
||
)
|
||
|
||
print(">> 【导出服务】下载完成,正在关闭标签页...")
|
||
try:
|
||
page.locator(".tags-view-item", has_text="导出服务").locator(
|
||
".el-icon-close"
|
||
).click()
|
||
print(" ✅ 【导出服务】标签页已关闭。")
|
||
except Exception:
|
||
pass
|
||
|
||
if downloaded_files:
|
||
print("\n>> 正在合并下载的数据...")
|
||
all_dfs = []
|
||
for file_path in downloaded_files:
|
||
try:
|
||
df = pd.read_excel(file_path, dtype=str)
|
||
if not df.empty:
|
||
all_dfs.append(df)
|
||
except Exception:
|
||
pass
|
||
|
||
if all_dfs:
|
||
combined_df = pd.concat(all_dfs, ignore_index=True)
|
||
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"====================================================")
|
||
|
||
for file_path in downloaded_files:
|
||
os.remove(file_path)
|
||
print(" 临时文件已清理。")
|