780 lines
32 KiB
Python
780 lines
32 KiB
Python
# sites/yunda.py
|
||
|
||
import os
|
||
import re
|
||
import time
|
||
import yaml
|
||
from datetime import datetime, timedelta
|
||
import pandas as pd
|
||
|
||
from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
|
||
from inbound_verify import state_store
|
||
|
||
|
||
def with_retry(site_name, label, flow, reset, max_attempts=3):
|
||
"""异常兜底:flow 失败 → 重置回初始态 → 重试,最多 max_attempts 次(含首次)。
|
||
|
||
每次失败都重置(含最终放弃那一次):既是重试前的清场,也保证最终放弃时环境干净。
|
||
flow 为零参可调用;返回 False 视为失败,其余视为成功。
|
||
返回 True=最终成功,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 True
|
||
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},放弃(环境已清理)。"
|
||
)
|
||
return False
|
||
|
||
|
||
# 站点首页 URL:异常兜底重置用,也供 runtime 的 SITES_CONFIG 引用(单一来源)
|
||
HOME_URL = "https://ky-sso.yunda56.com"
|
||
|
||
|
||
def dismiss_audio_prompt(page):
|
||
"""关闭韵达登录后弹出的阻塞式提示弹窗(如音频设备未授权/未找到)。
|
||
|
||
不依赖具体文案(不同主机/音频状态下文案不同:未授权/未找到…),只要出现
|
||
.ivu-modal-confirm 就点其「确定」。主页加载后调用(初始 + 重试重载)。
|
||
"""
|
||
try:
|
||
modal = page.locator(".ivu-modal-confirm").first
|
||
modal.wait_for(state="visible", timeout=2000)
|
||
modal.locator(".ivu-modal-confirm-footer button.ivu-btn-primary").click()
|
||
print(" ✅ 【韵达】已关闭提示弹窗。")
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def yunda_reset(page):
|
||
"""异常兜底:重置韵达到初始态(跳首页 URL,丢弃当前页面状态,登录态保留)。"""
|
||
page.goto(HOME_URL)
|
||
page.wait_for_timeout(1500)
|
||
dismiss_audio_prompt(page) # 主页重载后音频授权提示会复现,清理之
|
||
|
||
|
||
def _remove_if_exists(path):
|
||
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
|
||
try:
|
||
if os.path.exists(path):
|
||
os.remove(path)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _resolve_export_frame(ws_frame):
|
||
"""定位韵达数据导出面板内嵌的 iframe。
|
||
|
||
韵达改版后导出面板换用 Element UI(全选/向右转移/导出按钮)。实到流程的面板
|
||
iframe 名为 myFrame(已验证);应到流程历史上为 target1。这里短超时轮流探测,
|
||
返回首个出现「全选」按钮的 frame;都未命中则 dump 面板内所有 iframe 名便于排查。
|
||
"""
|
||
for name in ("myFrame", "target1"):
|
||
frame = ws_frame.frame_locator(f'iframe[name="{name}"]')
|
||
try:
|
||
frame.locator("button.el-button", has_text="全选").first.wait_for(
|
||
state="visible", timeout=5000
|
||
)
|
||
print(f" [导出iframe] 命中 iframe[name={name}]")
|
||
return frame
|
||
except Exception:
|
||
continue
|
||
try:
|
||
names = ws_frame.locator("iframe").evaluate_all(
|
||
"els => els.map(e => e.name || '(无name)')"
|
||
)
|
||
print(
|
||
f" [导出iframe] myFrame/target1 均未命中全选;面板 iframe 名: {names}"
|
||
)
|
||
except Exception as e:
|
||
print(f" [导出iframe] dump 失败: {e}")
|
||
return ws_frame.frame_locator('iframe[name="myFrame"]')
|
||
|
||
|
||
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)
|
||
|
||
# 凭证存 state.db(由前端站点配置)
|
||
username = state_store.get_setting("韵达", "username")
|
||
password = state_store.get_setting("韵达", "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, force=False, date=None):
|
||
"""韵达:应到货物数据下载(内部含异常兜底重试,路由层无感)。"""
|
||
|
||
return with_retry(
|
||
"韵达",
|
||
"应到",
|
||
lambda: yunda_expected_download_impl(page, force=force, date=date),
|
||
lambda: yunda_reset(page),
|
||
)
|
||
|
||
|
||
def yunda_expected_download_impl(page, force=False, date=None):
|
||
"""韵达:应到货物数据下载(单次执行,无重试;供自动化测试探测原始结果用)。"""
|
||
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. 读取服务端日期偏移(0=今天,1=昨天…),单日范围:起止同日
|
||
offset = state_store.get_offset("韵达")
|
||
today = datetime.now()
|
||
if date:
|
||
target = datetime.strptime(date, "%Y-%m-%d")
|
||
else:
|
||
target = today - timedelta(days=offset)
|
||
target_ymd = f"{target.year}-{target.month}-{target.day}"
|
||
start_date_ymd = target_ymd
|
||
today_ymd = target_ymd
|
||
src = f"指定 {date}" if date else f"偏移 {offset},0=今天"
|
||
print(f">> 设置查询日期: [{target_ymd}]({src})")
|
||
|
||
# 设定起始时间
|
||
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} 条")
|
||
|
||
# 【去重】加载本站已落库交接单号;force=True 或查询失败时 existing=空集(不去重)
|
||
if force:
|
||
existing = set()
|
||
print(">> [去重] 强制重下,跳过去重。")
|
||
else:
|
||
try:
|
||
from inbound_verify import store
|
||
|
||
existing = store.get_existing_handover_nos("韵达")
|
||
except Exception as _e:
|
||
existing = set()
|
||
print(f">> [去重] 加载已落库交接单号失败,本次不去重: {_e}")
|
||
|
||
# 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()
|
||
|
||
# 【去重】已落库的交接单号不再提交导出任务
|
||
if raw_no in existing:
|
||
print(f" ⏭️ 交接单号 {raw_no} 已落库,跳过提交导出。")
|
||
continue
|
||
|
||
# 跳过已绑定的交接单
|
||
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)
|
||
|
||
# 导出弹窗重试:外层重新打开面板(最多 3 次)。
|
||
# 韵达站点已将导出面板从 jQuery(.allRight/#submitbutton) 改版为 Element UI
|
||
# (与实到流程同一导出组件,iframe=myFrame):
|
||
# 全选(button“全选”) → 向右转移(i.el-icon-d-arrow-right) → 导出(i.el-icon-download)
|
||
# → 正在导出中(.el-loading-mask) → 成功提示(.el-message-box 导出任务建立成功) → 确定
|
||
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 = _resolve_export_frame(ws_frame)
|
||
|
||
try:
|
||
# 校验 Element UI 字段选择区是否加载完成(以「全选」按钮就绪为标志)
|
||
export_frame.locator(
|
||
"button.el-button", has_text="全选"
|
||
).first.wait_for(state="visible", timeout=8000)
|
||
except Exception:
|
||
print(" ⚠️ 导出面板字段区未加载,关闭面板后重试...")
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
|
||
print(" -> 全选字段并向右转移...")
|
||
export_frame.locator("button.el-button", has_text="全选").first.click()
|
||
page.wait_for_timeout(400)
|
||
export_frame.locator(
|
||
"button.el-button:has(i.el-icon-d-arrow-right)"
|
||
).first.click()
|
||
page.wait_for_timeout(500)
|
||
|
||
print(" -> 正在提交导出任务...")
|
||
export_frame.locator(
|
||
"button.el-button:has(i.el-icon-download)"
|
||
).first.click()
|
||
|
||
# 等待「正在导出中」遮罩出现并消失
|
||
loading = export_frame.locator(".el-loading-mask").first
|
||
try:
|
||
loading.wait_for(state="visible", timeout=5000)
|
||
loading.wait_for(state="hidden", timeout=60000)
|
||
except Exception:
|
||
pass
|
||
|
||
# 等待结果提示并判定
|
||
inner_success = False
|
||
try:
|
||
msg_box = export_frame.locator(".el-message-box.my-alert").first
|
||
msg_box.wait_for(state="visible", timeout=30000)
|
||
if msg_box.get_by_text("导出任务建立成功").is_visible():
|
||
print(" ✅ 导出任务已建立成功。")
|
||
inner_success = True
|
||
else:
|
||
print(" ⚠️ 导出结果提示非成功状态,将重试。")
|
||
msg_box.locator("button.el-button", has_text="确定").first.click()
|
||
page.wait_for_timeout(500)
|
||
except Exception:
|
||
print(" ⚠️ 未检测到导出结果提示,将重试。")
|
||
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
if inner_success:
|
||
task_success = True
|
||
break
|
||
|
||
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, force=False, date=None):
|
||
"""韵达:实到货物数据下载(内部含异常兜底重试,路由层无感)。"""
|
||
|
||
return with_retry(
|
||
"韵达",
|
||
"实到",
|
||
lambda: yunda_actual_download_impl(page, date=date),
|
||
lambda: yunda_reset(page),
|
||
)
|
||
|
||
|
||
def yunda_actual_download_impl(page, date=None):
|
||
"""韵达:实到货物数据下载(单次执行,无重试;供自动化测试探测原始结果用)。"""
|
||
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("✅ 扫描记录查询页面已初始化")
|
||
|
||
offset = state_store.get_offset("韵达", "actual")
|
||
today = datetime.now()
|
||
if date:
|
||
target = datetime.strptime(date, "%Y-%m-%d")
|
||
else:
|
||
target = today - timedelta(days=offset)
|
||
start_date = target # 单日范围:起止同日
|
||
today = target # 让下方"截止时间"选择器也指向 target
|
||
src = f"指定 {date}" if date else f"偏移 {offset},0=今天"
|
||
print(
|
||
f">> 设置实到查询日期: [{target.year}-{target.month}-{target.day}]({src})"
|
||
)
|
||
|
||
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
|
||
|
||
# 导出弹窗重试:外层重新打开面板(最多 3 次)。
|
||
# 韵达站点已将导出面板从 jQuery(.allRight/#submitbutton) 改版为 Element UI:
|
||
# 全选(button“全选”) → 向右转移(i.el-icon-d-arrow-right) → 导出(i.el-icon-download)
|
||
# → 正在导出中(.el-loading-mask) → 成功提示(.el-message-box 导出任务建立成功) → 确定
|
||
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:
|
||
# 校验 Element UI 字段选择区是否加载完成(以「全选」按钮就绪为标志)
|
||
export_frame.locator(
|
||
"button.el-button", has_text="全选"
|
||
).first.wait_for(state="visible", timeout=8000)
|
||
except Exception:
|
||
print(" ⚠️ 导出面板字段区未加载,关闭面板后重试...")
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(1000)
|
||
continue
|
||
|
||
print(" -> 全选字段并向右转移...")
|
||
export_frame.locator("button.el-button", has_text="全选").first.click()
|
||
page.wait_for_timeout(400)
|
||
export_frame.locator(
|
||
"button.el-button:has(i.el-icon-d-arrow-right)"
|
||
).first.click()
|
||
page.wait_for_timeout(500)
|
||
|
||
print(" -> 正在提交导出任务...")
|
||
export_frame.locator(
|
||
"button.el-button:has(i.el-icon-download)"
|
||
).first.click()
|
||
|
||
# 等待「正在导出中」遮罩出现并消失
|
||
loading = export_frame.locator(".el-loading-mask").first
|
||
try:
|
||
loading.wait_for(state="visible", timeout=5000)
|
||
loading.wait_for(state="hidden", timeout=60000)
|
||
except Exception:
|
||
pass
|
||
|
||
# 等待结果提示并判定
|
||
inner_success = False
|
||
try:
|
||
msg_box = export_frame.locator(".el-message-box.my-alert").first
|
||
msg_box.wait_for(state="visible", timeout=30000)
|
||
if msg_box.get_by_text("导出任务建立成功").is_visible():
|
||
print(" ✅ 导出任务已建立成功。")
|
||
inner_success = True
|
||
else:
|
||
print(" ⚠️ 导出结果提示非成功状态,将重试。")
|
||
msg_box.locator("button.el-button", has_text="确定").first.click()
|
||
page.wait_for_timeout(500)
|
||
except Exception:
|
||
print(" ⚠️ 未检测到导出结果提示,将重试。")
|
||
|
||
ws_frame.locator(".layui-layer-close1").click()
|
||
page.wait_for_timeout(500)
|
||
|
||
if inner_success:
|
||
task_success = True
|
||
break
|
||
|
||
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(" 临时文件已清理。")
|