refactor: move flat modules into inbound_verify package (Tier 1, behavior-identical)

Relocate 12 root .py modules into inbound_verify/ (sites/, cli/ subpackages). Rewrite all internal imports to package-qualified; drop the site_ prefix on the 5 site modules and their 28 call sites. Fix paths.py BASE_DIR to anchor at the project root. Add main() entry wrappers (cli/router, cli/server, store). No behavior change.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-07-23 12:21:31 +08:00
parent 0174e64a04
commit 7065b88269
12 changed files with 134 additions and 93 deletions

View File

@@ -0,0 +1,275 @@
# sites/baishi.py
import os
import yaml
from playwright.sync_api import sync_playwright
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异常兜底重置用也供 main_router 的 SITES_CONFIG 引用(单一来源)
HOME_URL = "https://v5.800best.com"
def baishi_reset(page):
"""异常兜底:重置百世到初始态(跳首页 URL丢弃当前页面状态登录态保留"""
page.goto(HOME_URL)
page.wait_for_timeout(1500)
def dismiss_baishi_popups(page):
"""清理百世首页杂乱弹窗:优惠券广告 / 通知消息 / 配置检查面板 / 聊天通知。
所有弹窗都有时序问题——不是登录瞬间就出现,旧代码一登录就判定"无弹窗"而跳过,
导致从未成功关闭过任何一个。修复策略统一为「轮询等待出现 → 关闭」。
经 CDP 端到端验证的选择器2026-07-19
- ① 优惠券广告:全屏居中 Ant Design modal.ant-modal-wrap.ant-modal-centered
关闭键 .ant-modal-close纯图标×、无文字。登录后约 2s 才加载。
- ② Ant Design 通知("您有新的消息".ant-notification 组件,
右下角卡片,关闭键 .ant-notification-notice-close纯图标×
- ③ 配置检查面板:.modal-config-check.react-draggable可拖拽浮动面板
默认 display:none系统检测后展开为 block。底部 footer 有两个 button
"重新检查"(danger) 和 "关 闭"(primary)。点"关 闭"后面板从 DOM 移除。
- ④ 聊天/调查通知("您还有调研单未填写...".chat-notification-popover-wrapper
z-index:1000关闭键 .chat-notification-close图标×
注意:左侧首页轮播/广告横幅是页面正常内容区(无关闭键),不在此处处理。
"""
import time as _time
# 单条轮询循环:每个 tick短间隔同时检查并关闭「所有当前可见」的弹窗。
#
# 旧实现是串行四段,每段 _poll_and_click 在「未命中」时会阻塞等待整段
# max_poll_s=5s 才返回 False。于是广告段跑完(~7s)才开始处理通知、配置,
# 而广告/通知其实登录后 1~2s 就出现了,却被卡在前面段的等待窗口里,表现为
# 「检查配置 / 消息通知关得特别慢」。改为单循环后,谁先出现谁在下一个 tick
# ~350ms就被关掉四类互不排队。
#
# 选择器均经 CDP 端到端验证(见函数 docstring
targets = [
(".ant-modal-wrap.ant-modal-centered .ant-modal-close", "优惠券广告"),
(".ant-notification-notice-close", "通知消息"),
(".modal-config-check button:has-text('关 闭')", "配置检查面板"),
(".chat-notification-close", "聊天调查通知"),
]
deadline = _time.monotonic() + 12.0 # 最长处理 12s兜底正常几秒内结束
tick_ms = 350
idle_rounds = 0 # 连续无处理的轮数
while _time.monotonic() < deadline:
handled_any = False
for sel, label in targets:
try:
loc = page.locator(sel)
if loc.count() > 0 and loc.first.is_visible(timeout=120):
loc.first.click(timeout=2000)
handled_any = True
page.wait_for_timeout(150) # 留一点关闭动画时间
except Exception:
pass
if handled_any:
idle_rounds = 0
else:
idle_rounds += 1
if idle_rounds >= 3: # 连续 ~1.05s 无任何弹窗 → 提前结束
break
page.wait_for_timeout(tick_ms)
def _remove_if_exists(path):
"""删除文件(若存在):流程开头清理上次的最终文件,避免无数据/失败时残留旧数据。"""
try:
if os.path.exists(path):
os.remove(path)
except Exception:
pass
def _close_tab(page, tab_name):
"""关闭指定名称的百世标签页。
通过 li > span 中的文本定位标签,并点击其内部 title 为 "关闭标签页" 的图标。
"""
try:
tab = page.locator("li").filter(has=page.locator("span", has_text=tab_name))
if tab.count() == 0:
print(f" 未找到标签页【{tab_name}】(可能尚未打开或已关闭),跳过。")
return
# 点击百世特有的关闭按钮
tab.first.locator("i[title='关闭标签页']").click()
print(f" 🗙 已关闭标签页【{tab_name}")
page.wait_for_timeout(300)
except Exception as e:
print(f" ⚠️ 关闭标签页【{tab_name}】时出错: {e}")
def baishi_download_undelivered_data(page):
"""百世:一键提取应到未到(当日未扫)数据(内部含异常兜底重试,路由层无感)。"""
return with_retry(
"百世",
"应到未到",
lambda: baishi_download_undelivered_data_impl(page),
lambda: baishi_reset(page),
)
def baishi_download_undelivered_data_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"))
try:
# 1. 导航与页面加载
print(">> 正在进入【扫描综合查询】界面...")
# 打开基础服务菜单面板
page.locator("div.nav-level1", has_text="基础服务").click()
# 限制在菜单面板nav-level2-wrapper内查找避免命中右侧同名标签页
page.locator(".nav-level2-wrapper").locator(
"a", has_text="扫描综合查询"
).click()
# 验证表格主界面加载完毕
page.get_by_role("tab", name="实时扫描率").wait_for(state="visible")
print("✅ 扫描综合查询界面已加载")
# 1.5 顺手抓取「到/接件扫描 → 当日」的 应扫/已扫(=应到/实到基数),
# 供汇总报表填写百世行的应到件/实到件。
# 实时扫描率表头为 14 列 leaf发/交件[昨日×3, 当日×4] +
# 到/接件[昨日×3, 当日×4],到/接件当日四列索引为
# 10(应扫) 11(已扫) 12(未扫) 13(率)。(与下方 nth(12) 未扫同源)
try:
_first_row = page.locator(".ant-table-tbody > tr").first
_exp_txt = (
_first_row.locator("td").nth(10).inner_text().strip().replace(",", "")
)
_arr_txt = (
_first_row.locator("td").nth(11).inner_text().strip().replace(",", "")
)
def _to_int(v):
try:
return int(float(v)) if v not in ("", "-") else 0
except (TypeError, ValueError):
return 0
_exp_n, _arr_n = _to_int(_exp_txt), _to_int(_arr_txt)
if _exp_n > 0:
state_store.set_setting("百世", "scan_expected_pieces", str(_exp_n))
state_store.set_setting("百世", "scan_arrived_pieces", str(_arr_n))
print(f" 已记录百世应到/实到基数:应扫 {_exp_n} / 已扫 {_arr_n}")
except Exception as _e:
# 抓取失败绝不影响未到明细下载主流程
print(f" ⚠️ 抓取百世应到/实到基数失败(不影响未到明细下载):{_e}")
# 2. 精准定位并点击【到/接件扫描 -> 当日 -> 未扫】的数字控件
print(">> 正在解析表格,提取当日到件未扫明细...")
# 定位第一行数据的第 13 列(索引 12
target_cell = page.locator(".ant-table-tbody > tr").first.locator("td").nth(12)
# 特殊情况处理检查未扫数量是否为0
cell_text = target_cell.inner_text().strip()
if cell_text == "0" or cell_text == "":
print(
f" 注意:当日未扫数量为【{cell_text}】,无数据需要提取,任务结束。"
)
# 数据为空时,提前结束前也需清理环境
_close_tab(page, "扫描综合查询")
return True
# 有未扫数据,继续点击操作
target_cell.locator("a").click()
# 等待下方弹出的明细表格区域加载完毕
detail_section = page.locator(".m-query-all-scanRateDetail")
detail_section.wait_for(state="visible")
page.wait_for_timeout(1000)
# 3. 触发导出设置模态框
print(">> 正在打开导出配置面板...")
# 使用明细区域内的导出按钮,避免误点主表导出
detail_section.locator(".export-wrap a[title='导出']").click()
# 验证模态框弹出
page.locator(".ant-modal-title", has_text="导出设置").wait_for(state="visible")
# 4. 读取百世导出密码(存 state.db由前端站点配置
print(">> 正在读取配置并填写密码...")
password = state_store.get_setting("百世", "password")
if not password:
print(" ⚠️ 警告:未设置百世导出密码(前端站点配置),可能导致导出失败。")
page.get_by_placeholder("请输入登录密码").fill(password)
# 5. 执行最终下载
print(">> 正在下载...")
with page.expect_download() as download_info:
# 点击模态框底部的“导 出”按钮
page.locator(".ant-modal-footer").get_by_role(
"button", name="导 出"
).click()
download = download_info.value
save_path = os.path.join(download_dir, "百世-应到未到货物数据.xlsx")
# 如果之前已经有同名文件,覆盖保存
if os.path.exists(save_path):
os.remove(save_path)
download.save_as(save_path)
print(f"====================================================")
print(f" 提取成功。")
print(f" 已下载: {save_path}")
print(f"====================================================")
# 6. 环境清理
print(">> 任务完成,正在清理环境...")
_close_tab(page, "扫描综合查询")
print("\n【百世 - 应到未到数据提取】流程结束。")
return True
except Exception as e:
print(f"\n❌ 任务执行过程中发生异常: {e}")
return False