Implement Yunda actual-arrival download and harden query loading

- Implement yunda_actual_download via the 报表管理 -> 扫描记录查询
  menu: set date range, switch scan type to 到件, then reuse the
  shared poll/download/merge engine
- Add a three-stage query buffer (yield, wait for the Bootstrap Table
  loading mask, settle) before the data/empty verdict to avoid stale
  DOM reads in the expected-arrival flow
- Trim the offline-export retry state machine's verbose logs
- Fix a typo (研编 -> 研判) and drop the 待开发 tag on menu [7]

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-06-20 18:59:49 +08:00
parent 194fbc7cda
commit 604d1822e4
2 changed files with 200 additions and 49 deletions

View File

@@ -223,7 +223,7 @@ def run_multi_site_daemon():
print("-" * 52) print("-" * 52)
print(" 模块四:【韵达】数据处理流") print(" 模块四:【韵达】数据处理流")
print(" [6] 执行 - 应到货物数据下载") print(" [6] 执行 - 应到货物数据下载")
print(" [7] 执行 - 实到货物数据下载 (待开发)") print(" [7] 执行 - 实到货物数据下载")
print("-" * 52) print("-" * 52)
print(" 全局离线数据引擎") print(" 全局离线数据引擎")
print(" [9] 执行 - 异常数据清洗比对 (Left Anti-Join)") print(" [9] 执行 - 异常数据清洗比对 (Left Anti-Join)")

View File

@@ -125,12 +125,26 @@ def yunda_expected_download(page):
calendar2.locator(".laydate-btns-confirm").click() calendar2.locator(".laydate-btns-confirm").click()
page.wait_for_timeout(500) page.wait_for_timeout(500)
# 3. 多维状态机静默加载校验 # ====================================================================
# 🛡️ 深度优化:三段式安全缓冲,彻底规避 DOM 旧状态穿透陷阱
# ====================================================================
print(">> 正在触发现场查询数据流...") print(">> 正在触发现场查询数据流...")
ws_frame.locator("a.btn-success", has_text="查询").click() ws_frame.locator("a.btn-success", has_text="查询").click()
page.wait_for_timeout(1000) # 阶段1物理安全挂起赋予浏览器触发渲染周期和 Ajax 的生命力
page.wait_for_timeout(800)
# 阶段2精准抓取 Bootstrap Table 专属的加载遮罩层
loading_mask = ws_frame.locator(
".fixed-table-loading", has_text="正在努力地加载数据中"
)
if loading_mask.is_visible():
print(" ⏳ 检测到专属数据加载罩,正在等待后端重载返回...")
loading_mask.wait_for(state="hidden", timeout=30000)
page.wait_for_timeout(500) # DOM 重排完毕后再缓冲半秒
# 阶段3进入绝对真实的终态判定世界
sum_panel = ws_frame.locator("#sum") sum_panel = ws_frame.locator("#sum")
has_data = False has_data = False
@@ -170,7 +184,7 @@ def yunda_expected_download(page):
current_row = ws_frame.locator("#exampleTable1 tbody tr[data-index]").nth(i) current_row = ws_frame.locator("#exampleTable1 tbody tr[data-index]").nth(i)
raw_no = current_row.locator("td").nth(1).inner_text().strip() raw_no = current_row.locator("td").nth(1).inner_text().strip()
print(f" -> 锁定交接单号: {raw_no}") print(f" -> 锁定提取单号: {raw_no}")
current_row.dblclick() current_row.dblclick()
@@ -184,75 +198,46 @@ def yunda_expected_download(page):
) )
export_frame = ws_frame.frame_locator('iframe[name="target1"]') export_frame = ws_frame.frame_locator('iframe[name="target1"]')
# 首次触发全选所有字段
export_frame.locator(".allRight").click() export_frame.locator(".allRight").click()
page.wait_for_timeout(400) page.wait_for_timeout(400)
# ==================================================================== # 智能重试容错状态机
# 🛡️ 智能容错重试状态机:完美兼容【离线建立失败】与【字段全选漏掉报错】
# ====================================================================
print(" >> 正在建立后台离线任务...") print(" >> 正在建立后台离线任务...")
task_success = False task_success = False
for attempt in range(5): for attempt in range(5):
export_frame.locator("#submitbutton", has_text="导出数据").click() export_frame.locator("#submitbutton", has_text="导出数据").click()
print(
f" -> 已触发第 {attempt + 1} 次【导出数据】指令,等待校验响应..."
)
# 依靠具防抖特性的 Link 角色定位“确定”按钮
confirm_link = export_frame.get_by_role("link", name="确定") confirm_link = export_frame.get_by_role("link", name="确定")
try: try:
# 等待提交重载后的新文档渲染出提示框
confirm_link.wait_for(state="visible", timeout=6000) confirm_link.wait_for(state="visible", timeout=6000)
# 场景 1建立成功
if export_frame.get_by_text("导出任务建立成功").is_visible(): if export_frame.get_by_text("导出任务建立成功").is_visible():
print(" ✅ 判定通过:成功捕获到【导出任务建立成功】特征!") print(" ✅ 判定通过:成功捕获到【导出任务建立成功】特征!")
confirm_link.click() confirm_link.click()
task_success = True task_success = True
break break
# 场景 2核心优化 - 发现字段全选未生效报错
elif export_frame.get_by_text( elif export_frame.get_by_text(
"请选择格式相应的导出字段" "请选择格式相应的导出字段"
).is_visible(): ).is_visible():
print( print(
" ⚠️ 警告:检测到【未选择字段】错误提示!正在清除弹窗并强行补点全选】按钮..." " ⚠️ 警告:检测到【未选择字段】错误,重新触发补点全选..."
) )
confirm_link.click() confirm_link.click()
page.wait_for_timeout(500) page.wait_for_timeout(500)
# 再次执行强行补点全选
export_frame.locator(".allRight").click() export_frame.locator(".allRight").click()
print(
" -> 【全选】按钮已重新触发补点,准备进入下一轮提交。"
)
page.wait_for_timeout(500) page.wait_for_timeout(500)
# 场景 3其他网络或未知异常提示
else: else:
print(
" ⚠️ 警告:检测到其他异常提示消息,正在清除弹窗并重试..."
)
confirm_link.click() confirm_link.click()
page.wait_for_timeout(1000) page.wait_for_timeout(1000)
except Exception as e: except Exception:
print(
f" ⚠️ 提示:第 {attempt + 1} 次提交后未能在安全时间内捕获到提示框 ({e}),准备重试..."
)
page.wait_for_timeout(1000) page.wait_for_timeout(1000)
if not task_success: if not task_success:
raise RuntimeError( raise RuntimeError(
"致命异常:连续 5 次尝试均无法成功建立任务,请检查网点后台事务层" "致命异常:连续 5 次尝试均无法成功建立应到数据离线任务。"
) )
# ====================================================================
# 关闭高层 Layui 挂载的数据导出窗口
ws_frame.locator(".layui-layer-close1").click() ws_frame.locator(".layui-layer-close1").click()
page.wait_for_timeout(500) page.wait_for_timeout(500)
# 回退到主表标签页
ws_frame.locator("#myTab a", has_text="交接单信息").click() ws_frame.locator("#myTab a", has_text="交接单信息").click()
page.wait_for_timeout(800) page.wait_for_timeout(800)
@@ -278,6 +263,180 @@ def yunda_expected_download(page):
print(f"\n❌ 任务执行过程中发生异常: {e}") print(f"\n❌ 任务执行过程中发生异常: {e}")
def yunda_actual_download(page):
"""韵达:实到货物数据下载"""
print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...")
target_task_title = "扫描记录数据"
download_dir = os.path.join(os.getcwd(), "downloads")
if not os.path.exists(download_dir):
os.makedirs(download_dir)
export_times = []
try:
page.locator(".el-menu-item", has_text="首页").wait_for(
state="visible", timeout=15000
)
print("✅ 韵达工作台首页成功加载")
yunda_smart_menu_click(page, ["报表管理", "扫描记录查询"])
print(">> [雷达扫描] 正在跨域动态追踪【扫描记录查询】业务窗体...")
ws_frame = _wait_and_get_frame_by_selector(page, "#startDate")
ws_frame.locator(".no-records-found", has_text="没有找到匹配的记录").wait_for(
state="visible", timeout=15000
)
print("✅ 扫描记录查询工作区初始化完毕")
query_days = 1
try:
if os.path.exists("config.yaml"):
with open("config.yaml", "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="正在努力地加载数据中"
)
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")
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="没有找到匹配的记录"
).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
# 5. 执行导出流
print(">> 正在发起【导出】申请指令...")
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"]')
export_frame.locator(".allRight").click()
page.wait_for_timeout(400)
print(" >> 正在建立后台离线任务...")
task_success = False
for attempt in range(5):
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()
task_success = True
break
elif export_frame.get_by_text("请选择格式相应的导出字段").is_visible():
print(" ⚠️ 警告:检测到字段未全选,执行强补点击...")
confirm_link.click()
page.wait_for_timeout(500)
export_frame.locator(".allRight").click()
page.wait_for_timeout(500)
else:
confirm_link.click()
page.wait_for_timeout(1000)
except Exception:
page.wait_for_timeout(1000)
if not task_success:
raise RuntimeError(
"致命异常:连续 5 次尝试均无法成功建立实到数据离线任务。"
)
ws_frame.locator(".layui-layer-close1").click()
page.wait_for_timeout(500)
export_times.append(datetime.now())
print(">> 📤 任务提交流闭环,正在执行【扫描记录查询】工作台销毁...")
page.locator(".tags-view-item", has_text="扫描记录查询").locator(
".el-icon-close"
).click()
page.wait_for_timeout(500)
# 6. 收割下载
_yunda_poll_and_download_tasks(
page,
export_times,
target_task_title,
download_dir,
"韵达-实到货物数据.xlsx",
)
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
def _yunda_poll_and_download_tasks( def _yunda_poll_and_download_tasks(
page, export_times, target_task_title, download_dir, final_filename page, export_times, target_task_title, download_dir, final_filename
): ):
@@ -286,7 +445,6 @@ def _yunda_poll_and_download_tasks(
yunda_smart_menu_click(page, ["基础数据", "导出服务"]) yunda_smart_menu_click(page, ["基础数据", "导出服务"])
export_ws_frame = page.frame_locator("section iframe") export_ws_frame = page.frame_locator("section iframe")
export_ws_frame.get_by_role("cell", name="模块名称", exact=True).wait_for( export_ws_frame.get_by_role("cell", name="模块名称", exact=True).wait_for(
state="visible", timeout=15000 state="visible", timeout=15000
) )
@@ -330,7 +488,7 @@ def _yunda_poll_and_download_tasks(
total_found = len(ready_indices) + len(processing_indices) total_found = len(ready_indices) + len(processing_indices)
print( 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: if total_found < total_expected or len(processing_indices) > 0:
@@ -343,7 +501,7 @@ def _yunda_poll_and_download_tasks(
print(">> ✅ 所有目标离线任务全量就绪!开始依序接入文件流...") print(">> ✅ 所有目标离线任务全量就绪!开始依序接入文件流...")
break break
# 8. 下载成果流 # 下载成果流
downloaded_files = [] downloaded_files = []
for row_idx in ready_indices: for row_idx in ready_indices:
try: try:
@@ -373,7 +531,7 @@ def _yunda_poll_and_download_tasks(
except Exception as e: except Exception as e:
print(f" ❌ 文件流接收失败: {e}") print(f" ❌ 文件流接收失败: {e}")
# 环境清理彻底关闭“导出服务”Tab # 环境清理
print(">> 📥 【导出服务】数据提取链闭环,正在执行当前 Tab 窗口销毁...") print(">> 📥 【导出服务】数据提取链闭环,正在执行当前 Tab 窗口销毁...")
try: try:
page.locator(".tags-view-item", has_text="导出服务").locator( page.locator(".tags-view-item", has_text="导出服务").locator(
@@ -383,7 +541,7 @@ def _yunda_poll_and_download_tasks(
except: except:
pass pass
# 9. 合并扁平数据集 # 合并扁平数据集
if downloaded_files: if downloaded_files:
print("\n>> 🧪 正在启动离线数据清洗与高能扁平合并流...") print("\n>> 🧪 正在启动离线数据清洗与高能扁平合并流...")
all_dfs = [] all_dfs = []
@@ -407,10 +565,3 @@ def _yunda_poll_and_download_tasks(
for file_path in downloaded_files: for file_path in downloaded_files:
os.remove(file_path) os.remove(file_path)
print(" ✅ 临时缓存阵列已无缝净化。") print(" ✅ 临时缓存阵列已无缝净化。")
def yunda_actual_download(page):
"""韵达:实到货物数据下载"""
print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...")
print("🚧 韵达实到数据功能规划中...")
pass