统一各站成败反馈信号;修复中通报错卡死与提示框误判

成败信号统一化(顺心/中通/韵达/安能/百世):
- 各站 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>
This commit is contained in:
Misaka
2026-07-16 21:38:56 +08:00
parent 55e9e9e931
commit 5030e5b89b
5 changed files with 171 additions and 23 deletions

View File

@@ -783,6 +783,12 @@ def poll_and_download_tasks(export_cdp, export_times, download_dir):
print(f" 已下载 [{idx}/{len(matched)}]: downloads/{save_name} ({fname})")
time.sleep(0.3)
# 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败
if len(downloaded_files) < total_expected:
raise RuntimeError(
f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整"
)
return downloaded_files
@@ -821,6 +827,15 @@ def merge_and_cleanup(downloaded_files, download_dir):
# ====================================================================
def _remove_if_exists(path):
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
try:
if os.path.exists(path):
os.remove(path)
except Exception:
pass
def _load_query_days():
"""从 config.yaml 读 anneng.query_days默认 1。"""
days = 1
@@ -846,6 +861,9 @@ def anneng_expected_download_impl():
download_dir = DOWNLOAD_DIR
os.makedirs(download_dir, exist_ok=True)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, FINAL_FILENAME))
query_days = _load_query_days()
today = datetime.now()
start = today - timedelta(days=(query_days - 1))
@@ -925,6 +943,7 @@ def anneng_expected_download_impl():
time.sleep(0.5)
merge_and_cleanup(downloaded, download_dir)
print("✅ 安能应到数据下载流程完成。")
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
@@ -1147,6 +1166,9 @@ def anneng_actual_download_impl():
download_dir = DOWNLOAD_DIR
os.makedirs(download_dir, exist_ok=True)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, ACTUAL_FINAL_FILENAME))
days = _load_query_days()
today = datetime.now()
start = today - timedelta(days=days - 1)
@@ -1183,8 +1205,9 @@ def anneng_actual_download_impl():
print(f" · 总共 {total}")
if total == 0:
print(">> ⚠ 无扫描数据,结束。")
_save_actual([], download_dir)
return
# 无数据:删除残留的旧最终文件(不落空文件),让比对层跳过本站
_remove_if_exists(os.path.join(download_dir, ACTUAL_FINAL_FILENAME))
return True
# 5) 改每页 500 并重新查询
print(">> 设置每页 500 条 …")
set_page_size_500(cdp)
@@ -1203,6 +1226,8 @@ def anneng_actual_download_impl():
page_no += 1
print(f">> 共抽取 {len(all_rows)} 条扫描记录")
if total > 0 and not all_rows:
raise RuntimeError(f"扫描总数 {total} 但抽取到 0 条记录,翻页抽取异常")
_save_actual(all_rows, download_dir)
print("✅ 安能实到数据下载流程完成。")
finally:
@@ -1210,6 +1235,7 @@ def anneng_actual_download_impl():
# 7) 关闭扫描查询 tab用完即关
print(">> 关闭【网点到件扫描查询】tab …")
close_tab_by_label(main_cdp, "网点到件扫描查询(新)")
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False

View File

@@ -45,6 +45,15 @@ def baishi_reset(page):
page.wait_for_timeout(1500)
def _remove_if_exists(path):
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
try:
if os.path.exists(path):
os.remove(path)
except Exception:
pass
def _close_tab(page, tab_name):
"""关闭指定名称的百世标签页。
@@ -83,6 +92,9 @@ def baishi_download_undelivered_data_impl(page):
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, "百世-应到未到货物数据.xlsx"))
try:
# 1. 导航与页面加载
print(">> 正在进入【扫描综合查询】界面...")
@@ -111,7 +123,7 @@ def baishi_download_undelivered_data_impl(page):
)
# 数据为空时,提前结束前也需清理环境
_close_tab(page, "扫描综合查询")
return
return True
# 有未扫数据,继续点击操作
target_cell.locator("a").click()

View File

@@ -76,6 +76,15 @@ def _sanitize_for_filename(name):
return re.sub(r'[\\/:*?"<>|]', "", str(name)).strip()
def _remove_if_exists(path):
"""删除文件(若存在):清理上次的本账号中间文件/最终文件,避免残留旧数据。"""
try:
if os.path.exists(path):
os.remove(path)
except Exception:
pass
def shunxin_belonging(page):
"""读取顺心当前账号的归属网点名(仅在首页可见,须在导航离开首页前调用)。
@@ -118,7 +127,8 @@ def shunxin_merge_final(kind, tags):
print(f" ⚠️ 读取中间文件 {mid_name} 失败: {e}")
if not frames:
print(f">> ⚠️ 所有归属地均无{kind}数据,未生成 {final_name}")
print(f">> ⚠️ 所有归属地均无{kind}数据,删除残留的 {final_name}(不写空表)")
_remove_if_exists(final_path)
return
combined = pd.concat(frames, ignore_index=True)
@@ -182,6 +192,12 @@ def shunxin_expected_download_impl(page, out_tag=""):
os.makedirs(download_dir)
print(f">> 已创建下载目录: {download_dir}")
# 清理上次本账号的中间文件,避免本次无数据/失败时残留旧数据被 merge_final 误读
_mid_suffix = f"-{out_tag}" if out_tag else ""
_remove_if_exists(
os.path.join(download_dir, f"顺心{_mid_suffix}-应到货物数据.xlsx")
)
export_times = []
target_task_timestamps = []
@@ -420,6 +436,12 @@ def shunxin_expected_download_impl(page, out_tag=""):
except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
# 汇总校验:下载成功数必须等于目标任务数,否则判失败
if len(downloaded_files) < len(target_task_timestamps):
raise RuntimeError(
f"仅成功下载 {len(downloaded_files)}/{len(target_task_timestamps)} 个任务,数据不完整"
)
# 9. 合并数据
if downloaded_files:
print("\n>> 正在合并下载的数据...")
@@ -499,6 +521,12 @@ def shunxin_actual_download_impl(page, out_tag=""):
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次本账号的中间文件,避免本次无数据/失败时残留旧数据被 merge_final 误读
_mid_suffix = f"-{out_tag}" if out_tag else ""
_remove_if_exists(
os.path.join(download_dir, f"顺心{_mid_suffix}-实到货物数据.xlsx")
)
export_times = []
target_task_timestamps = []
@@ -710,6 +738,12 @@ def shunxin_actual_download_impl(page, out_tag=""):
except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
# 汇总校验:下载成功数必须等于目标任务数,否则判失败
if len(downloaded_files) < len(target_task_timestamps):
raise RuntimeError(
f"仅成功下载 {len(downloaded_files)}/{len(target_task_timestamps)} 个任务,数据不完整"
)
# 8. 合并数据
if downloaded_files:
print("\n>> 正在合并下载的数据...")

View File

@@ -48,6 +48,15 @@ def yunda_reset(page):
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(">> 正在检查韵达登录状态...")
@@ -136,6 +145,9 @@ def yunda_expected_download_impl(page):
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, "韵达-应到货物数据.xlsx"))
export_times = []
try:
@@ -361,6 +373,7 @@ def yunda_expected_download_impl(page):
download_dir,
"韵达-应到货物数据.xlsx",
)
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
@@ -386,6 +399,9 @@ def yunda_actual_download_impl(page):
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, "韵达-实到货物数据.xlsx"))
export_times = []
try:
@@ -582,6 +598,7 @@ def yunda_actual_download_impl(page):
download_dir,
"韵达-实到货物数据.xlsx",
)
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
@@ -686,13 +703,19 @@ def _yunda_poll_and_download_tasks(page, export_times, download_dir, final_filen
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:
except Exception:
pass
if downloaded_files:
@@ -703,7 +726,7 @@ def _yunda_poll_and_download_tasks(page, export_times, download_dir, final_filen
df = pd.read_excel(file_path, dtype=str)
if not df.empty:
all_dfs.append(df)
except:
except Exception:
pass
if all_dfs:

View File

@@ -48,6 +48,15 @@ def zto_reset(page):
page.wait_for_timeout(1500)
def _remove_if_exists(path):
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
try:
if os.path.exists(path):
os.remove(path)
except Exception:
pass
def _wait_and_get_frame(page, text_indicator, timeout_ms=20000):
"""在主页面与所有 iframe 中查找包含指定文本的窗口"""
start_time = datetime.now()
@@ -116,6 +125,9 @@ def zto_expected_download_impl(page):
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, "中通-应到货物数据.xlsx"))
export_times = []
try:
@@ -213,14 +225,14 @@ def zto_expected_download_impl(page):
pass
return
else:
print(" ⚠️ 票数为 0 但未出现空记录提示,终止流程")
print(" ⚠️ 票数为 0 但未出现空记录提示,页面状态异常,判失败")
try:
page.locator(".mini-tab", has_text="进站交接单查询").locator(
".mini-tab-close"
).click()
except Exception:
pass
return
raise RuntimeError("票数为 0 但未出现空记录提示,页面状态异常")
else:
print(" >> 票数校验通过,等待主表格渲染数据行...")
ewb_frame.locator(
@@ -272,16 +284,22 @@ def zto_expected_download_impl(page):
print(" ✅ 已确认【温馨提示】弹窗。")
print(" >> 正在等待服务器建立后台离线任务...")
try:
# 提示「生成离线导出任务成功」出现在导出列 iframe(/comm/download) 中;
# 提交完成后该 iframe 会被站点销毁,此时 wait_for 会抛 "Frame was detached"
# ——这恰恰说明提示已随 iframe 消失、任务已建立,属正常,不视为失败。
ctx_tips = _wait_and_get_frame(
page, "生成离线导出任务成功", timeout_ms=10000
)
try:
ctx_tips.locator(".mini-tips-success").wait_for(
state="hidden", timeout=15000
)
except Exception as e:
if "detached" in str(e).lower():
print(" 提示框所在 iframe 已随提交关闭,任务已建立。")
else:
raise
print(" ✅ 成功提示框已消失。")
except Exception:
print(" ✅ 成功提示框已关闭。")
export_times.append(datetime.now())
@@ -309,6 +327,7 @@ def zto_expected_download_impl(page):
download_dir,
"中通-应到货物数据.xlsx",
)
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
@@ -334,6 +353,9 @@ def zto_actual_download_impl(page):
if not os.path.exists(download_dir):
os.makedirs(download_dir)
# 清理上次的最终文件,避免本次无数据/失败时残留旧数据误导比对
_remove_if_exists(os.path.join(download_dir, "中通-实到货物数据.xlsx"))
export_times = []
try:
@@ -444,16 +466,20 @@ def zto_actual_download_impl(page):
print(" ✅ 已确认【温馨提示】弹窗。")
print(" >> 正在等待服务器建立后台离线任务...")
# 提示「生成离线导出任务成功」出现在导出列 iframe(/comm/download) 中;
# 提交完成后该 iframe 会被站点销毁,此时 wait_for 会抛 "Frame was detached"
# ——这恰恰说明提示已随 iframe 消失、任务已建立,属正常,不视为失败。
ctx_tips = _wait_and_get_frame(page, "生成离线导出任务成功", timeout_ms=10000)
try:
ctx_tips = _wait_and_get_frame(
page, "生成离线导出任务成功", timeout_ms=10000
)
ctx_tips.locator(".mini-tips-success").wait_for(
state="hidden", timeout=15000
)
except Exception as e:
if "detached" in str(e).lower():
print(" 提示框所在 iframe 已随提交关闭,任务已建立。")
else:
raise
print(" ✅ 成功提示框已消失。")
except Exception:
print(" ✅ 成功提示框已关闭。")
export_times.append(datetime.now())
@@ -477,6 +503,7 @@ def zto_actual_download_impl(page):
download_dir,
"中通-实到货物数据.xlsx",
)
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
@@ -500,6 +527,8 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam
poll_deadline = (
time.monotonic() + 300
) # 5 分钟上限:任务卡死/匹配不上时超时失败,交由上层重置重试
last_total_found = -1
stall_rounds = 0 # 连续无进展轮数:刷新后 total_found 不增长则累计,超阈值快速失败
while True:
if time.monotonic() > poll_deadline:
raise RuntimeError(
@@ -543,14 +572,31 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam
)
if total_found < total_expected or len(processing_timestamps) > 0:
# 连续无进展即快速失败:避免页面被遮挡/任务卡住时干等到 5 分钟超时
if total_found == last_total_found:
stall_rounds += 1
else:
stall_rounds = 0
last_total_found = total_found
if stall_rounds >= 4:
raise RuntimeError(
f"连续 {stall_rounds} 轮刷新无进展(仍 {total_found}/{total_expected}"
"疑似页面被遮挡或任务异常,触发重试"
)
print(" ⏳ 任务尚未齐全或仍在生成,点击查询刷新...")
# 查询按钮加短超时;失败则菜单刷新,两者都失败直接报错触发重试
try:
taskdone_frame.locator(
".mini-button-text", has_text="查询"
).first.click()
).first.click(timeout=8000)
except Exception as e:
print(f" ⚠️ 查询按钮不可用,改用菜单刷新: {e}")
page.locator("li.leaf span.menu-name", has_text="导出任务管理").click()
print(f" ⚠️ 查询按钮不可用({e}),改用菜单刷新...")
try:
page.locator(
"li.leaf span.menu-name", has_text="导出任务管理"
).click(timeout=8000)
except Exception as e2:
raise RuntimeError(f"查询与菜单刷新均失败,疑似页面被遮挡: {e2}")
page.wait_for_timeout(3000)
else:
@@ -598,6 +644,13 @@ def _zto_poll_and_download_tasks(page, export_times, download_dir, final_filenam
except Exception as e:
print(f" ❌ 下载任务 [{time_str}] 失败: {e}")
# 汇总校验:下载成功数必须等于本批提交的任务数,否则判失败
# (防"部分/全失败却被判成功"
if len(downloaded_files) < total_expected:
raise RuntimeError(
f"仅成功下载 {len(downloaded_files)}/{total_expected} 个任务,数据不完整"
)
# ====================================================================
# 所有目标文件下载完成后,关闭“导出任务管理”标签页
# ====================================================================