From 604d1822e4ab1587a53da760deaf293eee21810e Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 20 Jun 2026 18:59:49 +0800 Subject: [PATCH] Implement Yunda actual-arrival download and harden query loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- main_router.py | 2 +- site_yunda.py | 247 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 200 insertions(+), 49 deletions(-) diff --git a/main_router.py b/main_router.py index a7e4e51..449840a 100644 --- a/main_router.py +++ b/main_router.py @@ -223,7 +223,7 @@ def run_multi_site_daemon(): print("-" * 52) print(" 模块四:【韵达】数据处理流") print(" [6] 执行 - 应到货物数据下载") - print(" [7] 执行 - 实到货物数据下载 (待开发)") + print(" [7] 执行 - 实到货物数据下载") print("-" * 52) print(" 全局离线数据引擎") print(" [9] 执行 - 异常数据清洗比对 (Left Anti-Join)") diff --git a/site_yunda.py b/site_yunda.py index dcb5d6d..cad3672 100644 --- a/site_yunda.py +++ b/site_yunda.py @@ -125,12 +125,26 @@ def yunda_expected_download(page): calendar2.locator(".laydate-btns-confirm").click() page.wait_for_timeout(500) - # 3. 多维状态机静默加载校验 + # ==================================================================== + # 🛡️ 深度优化:三段式安全缓冲,彻底规避 DOM 旧状态穿透陷阱 + # ==================================================================== print(">> 正在触发现场查询数据流...") 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") has_data = False @@ -170,7 +184,7 @@ def yunda_expected_download(page): current_row = ws_frame.locator("#exampleTable1 tbody tr[data-index]").nth(i) raw_no = current_row.locator("td").nth(1).inner_text().strip() - print(f" -> 锁定交接单号: {raw_no}") + print(f" -> 锁定提取单号: {raw_no}") current_row.dblclick() @@ -184,75 +198,46 @@ def yunda_expected_download(page): ) export_frame = ws_frame.frame_locator('iframe[name="target1"]') - - # 首次触发全选所有字段 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() - print( - f" -> 已触发第 {attempt + 1} 次【导出数据】指令,等待校验响应..." - ) - - # 依靠具防抖特性的 Link 角色定位“确定”按钮 confirm_link = export_frame.get_by_role("link", name="确定") try: - # 等待提交重载后的新文档渲染出提示框 confirm_link.wait_for(state="visible", timeout=6000) - - # 场景 1:建立成功 if export_frame.get_by_text("导出任务建立成功").is_visible(): print(" ✅ 判定通过:成功捕获到【导出任务建立成功】特征!") confirm_link.click() task_success = True break - - # 场景 2:核心优化 - 发现字段全选未生效报错 elif export_frame.get_by_text( "请选择格式相应的导出字段" ).is_visible(): print( - " ⚠️ 警告:检测到【未选择字段】错误提示!正在清除弹窗并强行补点【全选】按钮..." + " ⚠️ 警告:检测到【未选择字段】错误,重新触发补点全选..." ) confirm_link.click() page.wait_for_timeout(500) - # 再次执行强行补点全选 export_frame.locator(".allRight").click() - print( - " -> 【全选】按钮已重新触发补点,准备进入下一轮提交。" - ) page.wait_for_timeout(500) - - # 场景 3:其他网络或未知异常提示 else: - print( - " ⚠️ 警告:检测到其他异常提示消息,正在清除弹窗并重试..." - ) confirm_link.click() page.wait_for_timeout(1000) - except Exception as e: - print( - f" ⚠️ 提示:第 {attempt + 1} 次提交后未能在安全时间内捕获到提示框 ({e}),准备重试..." - ) + except Exception: page.wait_for_timeout(1000) if not task_success: raise RuntimeError( - "致命异常:连续 5 次尝试均无法成功建立任务,请检查网点后台事务层。" + "致命异常:连续 5 次尝试均无法成功建立应到数据离线任务。" ) - # ==================================================================== - # 关闭高层 Layui 挂载的数据导出窗口 ws_frame.locator(".layui-layer-close1").click() page.wait_for_timeout(500) - # 回退到主表标签页 ws_frame.locator("#myTab a", has_text="交接单信息").click() page.wait_for_timeout(800) @@ -278,6 +263,180 @@ def yunda_expected_download(page): 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( 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, ["基础数据", "导出服务"]) export_ws_frame = page.frame_locator("section iframe") - export_ws_frame.get_by_role("cell", name="模块名称", exact=True).wait_for( state="visible", timeout=15000 ) @@ -330,7 +488,7 @@ 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: @@ -343,7 +501,7 @@ def _yunda_poll_and_download_tasks( print(">> ✅ 所有目标离线任务全量就绪!开始依序接入文件流...") break - # 8. 下载成果流 + # 下载成果流 downloaded_files = [] for row_idx in ready_indices: try: @@ -373,7 +531,7 @@ def _yunda_poll_and_download_tasks( except Exception as e: print(f" ❌ 文件流接收失败: {e}") - # 环境清理:彻底关闭“导出服务”Tab + # 环境清理 print(">> 📥 【导出服务】数据提取链闭环,正在执行当前 Tab 窗口销毁...") try: page.locator(".tags-view-item", has_text="导出服务").locator( @@ -383,7 +541,7 @@ def _yunda_poll_and_download_tasks( except: pass - # 9. 合并扁平数据集 + # 合并扁平数据集 if downloaded_files: print("\n>> 🧪 正在启动离线数据清洗与高能扁平合并流...") all_dfs = [] @@ -407,10 +565,3 @@ def _yunda_poll_and_download_tasks( for file_path in downloaded_files: os.remove(file_path) print(" ✅ 临时缓存阵列已无缝净化。") - - -def yunda_actual_download(page): - """韵达:实到货物数据下载""" - print("\n▶ 开始执行【韵达 - 实到货物数据下载】任务...") - print("🚧 韵达实到数据功能规划中...") - pass