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

@@ -11,8 +11,8 @@ import time
import yaml import yaml
from paths import CONFIG_PATH from inbound_verify.paths import CONFIG_PATH
from runtime import ( from inbound_verify.runtime import (
APP_SITES, APP_SITES,
HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL,
dispatch_task, dispatch_task,
@@ -20,15 +20,11 @@ from runtime import (
run_heartbeat, run_heartbeat,
) )
import state_store from inbound_verify import state_store
# 各站点模块(自动化测试 + 比对用;任务派发在 runtime # 各站点模块(自动化测试 + 比对用;任务派发在 runtime
import site_shunxin from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng
import site_baishi from inbound_verify import expected_undelivered
import site_zto
import site_yunda
import site_anneng
import expected_undelivered
def run_undelivered_compare(): def run_undelivered_compare():
@@ -57,20 +53,20 @@ def run_automation_test(pages_map):
# 不被模块内部"失败→重置→重试"机制掩盖。 # 不被模块内部"失败→重置→重试"机制掩盖。
flow_table = { flow_table = {
"顺心": { "顺心": {
"expected": ("应到", site_shunxin.shunxin_expected_download_impl), "expected": ("应到", shunxin.shunxin_expected_download_impl),
"actual": ("实到", site_shunxin.shunxin_actual_download_impl), "actual": ("实到", shunxin.shunxin_actual_download_impl),
}, },
"中通": { "中通": {
"expected": ("应到", site_zto.zto_expected_download_impl), "expected": ("应到", zto.zto_expected_download_impl),
"actual": ("实到", site_zto.zto_actual_download_impl), "actual": ("实到", zto.zto_actual_download_impl),
}, },
"韵达": { "韵达": {
"expected": ("应到", site_yunda.yunda_expected_download_impl), "expected": ("应到", yunda.yunda_expected_download_impl),
"actual": ("实到", site_yunda.yunda_actual_download_impl), "actual": ("实到", yunda.yunda_actual_download_impl),
}, },
"安能": { "安能": {
"expected": ("应到", site_anneng.anneng_expected_download_impl), "expected": ("应到", anneng.anneng_expected_download_impl),
"actual": ("实到", site_anneng.anneng_actual_download_impl), "actual": ("实到", anneng.anneng_actual_download_impl),
}, },
} }
@@ -83,7 +79,7 @@ def run_automation_test(pages_map):
continue continue
for sx_idx, sx_page in enumerate(pages_map["顺心"], start=1): for sx_idx, sx_page in enumerate(pages_map["顺心"], start=1):
try: try:
tag = site_shunxin.shunxin_belonging(sx_page) tag = shunxin.shunxin_belonging(sx_page)
except Exception: except Exception:
tag = f"账号{sx_idx}" # 读不到归属地时用序号占位,不阻断测试 tag = f"账号{sx_idx}" # 读不到归属地时用序号占位,不阻断测试
for flow_key in CROSS_TEST_SEQUENCE: for flow_key in CROSS_TEST_SEQUENCE:
@@ -102,7 +98,7 @@ def run_automation_test(pages_map):
( (
"百世", "百世",
"应到未到", "应到未到",
site_baishi.baishi_download_undelivered_data_impl, baishi.baishi_download_undelivered_data_impl,
pages_map["百世"], pages_map["百世"],
"", "",
) )
@@ -343,5 +339,10 @@ def run_multi_site_daemon():
print("程序已退出。") print("程序已退出。")
if __name__ == "__main__": def main():
"""交互菜单模式入口。"""
run_multi_site_daemon() run_multi_site_daemon()
if __name__ == "__main__":
main()

View File

@@ -26,9 +26,9 @@ from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel from pydantic import BaseModel
from paths import DOWNLOAD_DIR, OUTPUT_DIR from inbound_verify.paths import DOWNLOAD_DIR, OUTPUT_DIR
import state_store from inbound_verify import state_store
from runtime import ( from inbound_verify.runtime import (
HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL,
TASK_HANDLERS, TASK_HANDLERS,
dispatch_task, dispatch_task,
@@ -64,7 +64,9 @@ def _worker_loop():
# 【P1-2 重启自愈】worker 就绪后清理上轮遗留的 pending/running 僵尸任务 # 【P1-2 重启自愈】worker 就绪后清理上轮遗留的 pending/running 僵尸任务
cleaned = state_store.fail_stale_tasks() cleaned = state_store.fail_stale_tasks()
if cleaned: if cleaned:
print(f">> [worker] 自愈:清理 {cleaned} 条遗留任务pending/running → failed") print(
f">> [worker] 自愈:清理 {cleaned} 条遗留任务pending/running → failed"
)
print(">> [worker] 各站就绪,开始接收任务 ...") print(">> [worker] 各站就绪,开始接收任务 ...")
except Exception as e: except Exception as e:
worker_state["error"] = str(e) worker_state["error"] = str(e)
@@ -161,7 +163,9 @@ def create_task(req: TaskRequest):
"""提交任务 {site, kind} → 入队,返回 task_id。""" """提交任务 {site, kind} → 入队,返回 task_id。"""
# 【P0】后端未就绪时直接拒绝避免任务在 worker 启动前入队卡死 # 【P0】后端未就绪时直接拒绝避免任务在 worker 启动前入队卡死
if not worker_state["ready"]: if not worker_state["ready"]:
raise HTTPException(status_code=409, detail="后端尚未就绪,请等待各站点登录完成后再操作") raise HTTPException(
status_code=409, detail="后端尚未就绪,请等待各站点登录完成后再操作"
)
if (req.site, req.kind) not in TASK_HANDLERS: if (req.site, req.kind) not in TASK_HANDLERS:
raise HTTPException(status_code=400, detail=f"无效任务: {req.site}/{req.kind}") raise HTTPException(status_code=400, detail=f"无效任务: {req.site}/{req.kind}")
task_id = state_store.create_task(req.site, req.kind) task_id = state_store.create_task(req.site, req.kind)
@@ -287,5 +291,10 @@ def download_data(filename: str):
return FileResponse(path, filename=filename) return FileResponse(path, filename=filename)
def main():
"""服务模式入口。传字符串导入路径(规范写法;不开 reload/workers 时进程内 import行为等价"""
uvicorn.run("inbound_verify.cli.server:app", host="0.0.0.0", port=8000)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000) main()

View File

@@ -63,7 +63,7 @@ def arrived_pieces_zhongtong(df):
for v in df["运单号"]: for v in df["运单号"]:
v = str(v).strip() v = str(v).strip()
if len(v) > 8 and v[-4:].isdigit(): if len(v) > 8 and v[-4:].isdigit():
res[v[:-8]].add(v) # 以完整复合串作为“已到单号”存入 res[v[:-8]].add(v) # 以完整复合串作为“已到单号”存入
return res return res
@@ -72,6 +72,7 @@ def arrived_pieces_by_cols(wb_col, piece_col):
wb_col实到表中与应到运单号对齐的干净列 wb_col实到表中与应到运单号对齐的干净列
顺心=运单号 / 韵达=主单号 / 安能=所属单号 顺心=运单号 / 韵达=主单号 / 安能=所属单号
piece_col实到表中每件货物的单号列子单号 / 扫描单号""" piece_col实到表中每件货物的单号列子单号 / 扫描单号"""
def parse(df): def parse(df):
res = defaultdict(set) res = defaultdict(set)
for m, s in zip(df[wb_col], df[piece_col]): for m, s in zip(df[wb_col], df[piece_col]):
@@ -79,6 +80,7 @@ def arrived_pieces_by_cols(wb_col, piece_col):
if m and s: if m and s:
res[m].add(s) res[m].add(s)
return res return res
return parse return parse
@@ -87,9 +89,9 @@ STATIONS = [
"name": "中通", "name": "中通",
"exp": "中通-应到货物数据.xlsx", "exp": "中通-应到货物数据.xlsx",
"act": "中通-实到货物数据.xlsx", "act": "中通-实到货物数据.xlsx",
"exp_qty": "交接件数", # 应到件数口径:交接件数(非录单件数) "exp_qty": "交接件数", # 应到件数口径:交接件数(非录单件数)
"exp_wb": "运单号", # 应到表运单号列(兼作去重键) "exp_wb": "运单号", # 应到表运单号列(兼作去重键)
"exp_jd": "交接单号", # 未到数据需展示的交接单号 "exp_jd": "交接单号", # 未到数据需展示的交接单号
"arrived_pieces": arrived_pieces_zhongtong, "arrived_pieces": arrived_pieces_zhongtong,
"columns": ["交接单号", "运单号", "总件数"], "columns": ["交接单号", "运单号", "总件数"],
}, },
@@ -174,7 +176,7 @@ def process(name):
# 实到件数(新口径)= 实到表单号去重数量(分组 运单->已到单号集合) # 实到件数(新口径)= 实到表单号去重数量(分组 运单->已到单号集合)
arrived = cfg["arrived_pieces"](df_act) arrived = cfg["arrived_pieces"](df_act)
act_pieces = sum(len(s) for s in arrived.values()) # 全局去重单号数 act_pieces = sum(len(s) for s in arrived.values()) # 全局去重单号数
# 未到:逐运单比较,列出实际已到的单号(顺序号乱序,无法反推缺件序号) # 未到:逐运单比较,列出实际已到的单号(顺序号乱序,无法反推缺件序号)
rows = [] rows = []
@@ -301,7 +303,9 @@ def process_baishi():
# 应到/实到基数取自「扫描综合查询」应扫/已扫(到/接件扫描→当日), # 应到/实到基数取自「扫描综合查询」应扫/已扫(到/接件扫描→当日),
# 由 baishi_download_undelivered_data_impl 在同次导航里抓取并落 site_settings。 # 由 baishi_download_undelivered_data_impl 在同次导航里抓取并落 site_settings。
# 未抓取过则 get_setting 返回 "" → 视为无基数(报表显示「—」)。 # 未抓取过则 get_setting 返回 "" → 视为无基数(报表显示「—」)。
import state_store # 与 _read_business_dates 一致:比对模块纯离线,懒加载 from inbound_verify import (
state_store,
) # 与 _read_business_dates 一致:比对模块纯离线,懒加载
def _to_int(v): def _to_int(v):
v = (v or "").strip().replace(",", "") v = (v or "").strip().replace(",", "")
@@ -348,7 +352,7 @@ def _read_business_dates(include):
"""从状态库读各站业务日期dispatch 下载成功时快照写入),供报告「数据日期」列。 """从状态库读各站业务日期dispatch 下载成功时快照写入),供报告「数据日期」列。
4 站取 expected_business_date报告按应到口径百世取 undelivered_business_date 4 站取 expected_business_date报告按应到口径百世取 undelivered_business_date
从未下过的站返回空串诚实留空不反推""" 从未下过的站返回空串诚实留空不反推"""
import state_store # lazy import比对模块本身保持纯离线 from inbound_verify import state_store # lazy import比对模块本身保持纯离线
status = state_store.get_all_status() status = state_store.get_all_status()
dates = {} dates = {}
@@ -534,8 +538,16 @@ def build_summary(ws, results, generated_at, dates=None):
# 百世:未到明细已知;若已抓取应到/实到基数(扫描综合查询应扫/已扫)则填真实值 # 百世:未到明细已知;若已抓取应到/实到基数(扫描综合查询应扫/已扫)则填真实值
if s["应到件"] is not None and s["已到件"] is not None: if s["应到件"] is not None and s["已到件"] is not None:
srate = (s["未到件"] / s["应到件"]) if s["应到件"] else 0 srate = (s["未到件"] / s["应到件"]) if s["应到件"] else 0
vals = [name, s["运单数"], s["应到件"], s["已到件"], vals = [
s["未到件"], srate, "", ""] name,
s["运单数"],
s["应到件"],
s["已到件"],
s["未到件"],
srate,
"",
"",
]
else: else:
vals = [name, s["运单数"], "", "", s["未到件"], "", "", ""] vals = [name, s["运单数"], "", "", s["未到件"], "", "", ""]
else: else:
@@ -564,7 +576,12 @@ def build_summary(ws, results, generated_at, dates=None):
cell.fill = PatternFill("solid", fgColor=ZEBRA) cell.fill = PatternFill("solid", fgColor=ZEBRA)
if isinstance(v, (int, float)): if isinstance(v, (int, float)):
cell.number_format = "0.0%" if i == 5 else "#,##0" cell.number_format = "0.0%" if i == 5 else "#,##0"
if i == 5 and s is not None and isinstance(v, (int, float)) and not isinstance(v, bool): if (
i == 5
and s is not None
and isinstance(v, (int, float))
and not isinstance(v, bool)
):
cell.fill = PatternFill("solid", fgColor=heat(srate)) cell.fill = PatternFill("solid", fgColor=heat(srate))
ws.row_dimensions[r].height = 19 ws.row_dimensions[r].height = 19
r += 1 r += 1

View File

@@ -6,7 +6,8 @@
import os import os
# 项目根目录(以本文件所在位置为基准,与从哪个目录启动脚本无关) # 项目根目录(以本文件所在位置为基准,与从哪个目录启动脚本无关)
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # __file__ = <root>/inbound_verify/paths.py → 上两级 = 项目根
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 统一的下载 / 输出目录 # 统一的下载 / 输出目录
DOWNLOAD_DIR = os.path.join(BASE_DIR, "downloads") DOWNLOAD_DIR = os.path.join(BASE_DIR, "downloads")

View File

@@ -21,22 +21,18 @@ from datetime import datetime, timedelta
import yaml import yaml
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store from inbound_verify import state_store
import site_shunxin from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng
import site_baishi from inbound_verify import expected_undelivered # dispatch 的 compare 任务用
import site_zto
import site_yunda
import site_anneng
import expected_undelivered # dispatch 的 compare 任务用
# 各网页站点首页 URL单一来源取自各站点模块 HOME_URL # 各网页站点首页 URL单一来源取自各站点模块 HOME_URL
SITES_CONFIG = { SITES_CONFIG = {
"顺心": site_shunxin.HOME_URL, "顺心": shunxin.HOME_URL,
"百世": site_baishi.HOME_URL, "百世": baishi.HOME_URL,
"中通": site_zto.HOME_URL, "中通": zto.HOME_URL,
"韵达": site_yunda.HOME_URL, "韵达": yunda.HOME_URL,
} }
# 站点就绪特征:登录成功进入工作台后的标志性控件 # 站点就绪特征:登录成功进入工作台后的标志性控件
@@ -114,8 +110,10 @@ def launch_anneng(app_path):
anneng_env.pop("NODE_OPTIONS", None) anneng_env.pop("NODE_OPTIONS", None)
port = _find_free_port() port = _find_free_port()
print(f">> 以调试模式启动【安能】应用(端口 {port}{app_path}") print(f">> 以调试模式启动【安能】应用(端口 {port}{app_path}")
proc = subprocess.Popen([app_path, f"--remote-debugging-port={port}"], env=anneng_env) proc = subprocess.Popen(
site_anneng.set_cdp_port(port) [app_path, f"--remote-debugging-port={port}"], env=anneng_env
)
anneng.set_cdp_port(port)
if not _wait_cdp_up(port): if not _wait_cdp_up(port):
raise RuntimeError( raise RuntimeError(
f"安能应用调试端口 {port} 未就绪——可能应用已在运行(单实例)," f"安能应用调试端口 {port} 未就绪——可能应用已在运行(单实例),"
@@ -136,7 +134,7 @@ def probe_site_login(site_name, pages_map):
if site_name not in pages_map: if site_name not in pages_map:
return False return False
if site_name == "安能": if site_name == "安能":
return site_anneng.anneng_ready() return anneng.anneng_ready()
if site_name == "顺心": if site_name == "顺心":
return all( return all(
pg.locator(READY_SELECTORS["顺心"]).is_visible(timeout=500) pg.locator(READY_SELECTORS["顺心"]).is_visible(timeout=500)
@@ -333,7 +331,7 @@ def launch_and_prepare(debug_mode=False, debug_target=""):
if "韵达" in pages_map: if "韵达" in pages_map:
try: try:
pages_map["韵达"].bring_to_front() pages_map["韵达"].bring_to_front()
site_yunda.yunda_login(pages_map["韵达"]) yunda.yunda_login(pages_map["韵达"])
except Exception as e: except Exception as e:
print(f" ⚠️ 韵达前置自动登录模块发生波动: {e}") print(f" ⚠️ 韵达前置自动登录模块发生波动: {e}")
@@ -403,9 +401,9 @@ def _dismiss_initial_popups(pages_map):
bs_page = pages_map["百世"] bs_page = pages_map["百世"]
bs_page.bring_to_front() bs_page.bring_to_front()
print(">> 正在处理【百世】初始弹窗(阅读消息 / 配置检查 / 优惠券广告)...") print(">> 正在处理【百世】初始弹窗(阅读消息 / 配置检查 / 优惠券广告)...")
# 委托给 site_baishi 的专用清理:优惠券广告是全屏居中 modal # 委托给 baishi 的专用清理:优惠券广告是全屏居中 modal
# 关闭键为 .ant-modal-close纯图标无文字必须点它才能真正关掉。 # 关闭键为 .ant-modal-close纯图标无文字必须点它才能真正关掉。
site_baishi.dismiss_baishi_popups(bs_page) baishi.dismiss_baishi_popups(bs_page)
print(" ✅ 【百世】初始弹窗处理完成。") print(" ✅ 【百世】初始弹窗处理完成。")
except Exception: except Exception:
pass pass
@@ -414,7 +412,7 @@ def _dismiss_initial_popups(pages_map):
yd_page = pages_map["韵达"] yd_page = pages_map["韵达"]
yd_page.bring_to_front() yd_page.bring_to_front()
print(">> 正在检查【韵达】音频设备授权提示...") print(">> 正在检查【韵达】音频设备授权提示...")
site_yunda.dismiss_audio_prompt(yd_page) yunda.dismiss_audio_prompt(yd_page)
except Exception: except Exception:
pass pass
@@ -460,20 +458,20 @@ def _site_undelivered_handler(site):
TASK_HANDLERS = { TASK_HANDLERS = {
("顺心", "expected"): _web_handler("顺心", site_shunxin.shunxin_expected_download), ("顺心", "expected"): _web_handler("顺心", shunxin.shunxin_expected_download),
("顺心", "actual"): _web_handler("顺心", site_shunxin.shunxin_actual_download), ("顺心", "actual"): _web_handler("顺心", shunxin.shunxin_actual_download),
("顺心", "undelivered"): _site_undelivered_handler("顺心"), ("顺心", "undelivered"): _site_undelivered_handler("顺心"),
("百世", "undelivered"): _web_handler( ("百世", "undelivered"): _web_handler(
"百世", site_baishi.baishi_download_undelivered_data "百世", baishi.baishi_download_undelivered_data
), ),
("中通", "expected"): _web_handler("中通", site_zto.zto_expected_download), ("中通", "expected"): _web_handler("中通", zto.zto_expected_download),
("中通", "actual"): _web_handler("中通", site_zto.zto_actual_download), ("中通", "actual"): _web_handler("中通", zto.zto_actual_download),
("中通", "undelivered"): _site_undelivered_handler("中通"), ("中通", "undelivered"): _site_undelivered_handler("中通"),
("韵达", "expected"): _web_handler("韵达", site_yunda.yunda_expected_download), ("韵达", "expected"): _web_handler("韵达", yunda.yunda_expected_download),
("韵达", "actual"): _web_handler("韵达", site_yunda.yunda_actual_download), ("韵达", "actual"): _web_handler("韵达", yunda.yunda_actual_download),
("韵达", "undelivered"): _site_undelivered_handler("韵达"), ("韵达", "undelivered"): _site_undelivered_handler("韵达"),
("安能", "expected"): lambda ctx: site_anneng.anneng_expected_download(), ("安能", "expected"): lambda ctx: anneng.anneng_expected_download(),
("安能", "actual"): lambda ctx: site_anneng.anneng_actual_download(), ("安能", "actual"): lambda ctx: anneng.anneng_actual_download(),
("安能", "undelivered"): _site_undelivered_handler("安能"), ("安能", "undelivered"): _site_undelivered_handler("安能"),
("__compare__", "compare"): lambda ctx: (expected_undelivered.main() or True), ("__compare__", "compare"): lambda ctx: (expected_undelivered.main() or True),
} }

View File

@@ -1,4 +1,4 @@
# site_anneng.py # sites/anneng.py
# #
# 安能全网门户Electron 应用)—— 应到货物数据下载。 # 安能全网门户Electron 应用)—— 应到货物数据下载。
# #
@@ -7,7 +7,7 @@
# 本模块通过该端口的 CDP 驱动它;「进站交接单查询」「导出下载」等右侧 tab 是 # 本模块通过该端口的 CDP 驱动它;「进站交接单查询」「导出下载」等右侧 tab 是
# **独立 webContents**(远程网页),在 /json 里是独立目标。 # **独立 webContents**(远程网页),在 /json 里是独立目标。
# 也可脱离 main_router 独立运行(此时用默认端口 9222需已自行启动应用 # 也可脱离 main_router 独立运行(此时用默认端口 9222需已自行启动应用
# .venv/Scripts/python.exe site_anneng.py # python -m inbound_verify.sites.anneng expected # 或 actual
# #
# 应到 = 运单信息导出(每张交接单下应到的运单明细)。整体流程: # 应到 = 运单信息导出(每张交接单下应到的运单明细)。整体流程:
# 主页(重庆鱼洞镇) → 运营管理 → 进站管理 → 进站交接单查询(tab) # 主页(重庆鱼洞镇) → 运营管理 → 进站管理 → 进站交接单查询(tab)
@@ -39,8 +39,8 @@ import yaml
# Windows 控制台默认 GBK打印中文/emoji 会崩,强制 UTF-8。 # Windows 控制台默认 GBK打印中文/emoji 会崩,强制 UTF-8。
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
from paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store from inbound_verify import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3): def with_retry(site_name, label, flow, reset, max_attempts=3):
@@ -73,7 +73,7 @@ def with_retry(site_name, label, flow, reset, max_attempts=3):
return False return False
CDP_PORT = 9222 # 默认端口(独立运行 site_anneng.py 时用main_router 启动时会用 set_cdp_port 覆盖 CDP_PORT = 9222 # 默认端口(独立运行python -m inbound_verify.sites.anneng时用main_router 启动时会用 set_cdp_port 覆盖
POLL_INTERVAL = 0.5 POLL_INTERVAL = 0.5
DEFAULT_TIMEOUT = 25.0 DEFAULT_TIMEOUT = 25.0
@@ -973,6 +973,7 @@ def set_scan_date(cdp, placeholder, value, target_ymd=None):
返回 True 表示写入并校验成功否则 False 返回 True 表示写入并校验成功否则 False
""" """
import re as _re import re as _re
ph = json.dumps(placeholder) ph = json.dumps(placeholder)
val_json = json.dumps(value) val_json = json.dumps(value)
if target_ymd is None: if target_ymd is None:
@@ -998,7 +999,9 @@ def set_scan_date(cdp, placeholder, value, target_ymd=None):
"(() => {const inp=[...document.querySelectorAll('input')]" "(() => {const inp=[...document.querySelectorAll('input')]"
f".find(i=>i.placeholder==={ph}); if(!inp) return false; inp.focus();" f".find(i=>i.placeholder==={ph}); if(!inp) return false; inp.focus();"
"let sel=true; try{ sel=document.execCommand('selectAll'); }catch(e){ try{inp.select();}catch(_){ sel=false; } }" "let sel=true; try{ sel=document.execCommand('selectAll'); }catch(e){ try{inp.select();}catch(_){ sel=false; } }"
"let done=false; try{ done=document.execCommand('insertText',false," + val_json + "); }catch(e){ done=false; }" "let done=false; try{ done=document.execCommand('insertText',false,"
+ val_json
+ "); }catch(e){ done=false; }"
"if(!done){ const s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;" "if(!done){ const s=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;"
f" s.call(inp,{val_json}); inp.dispatchEvent(new Event('input',{{bubbles:true}})); }}" f" s.call(inp,{val_json}); inp.dispatchEvent(new Event('input',{{bubbles:true}})); }}"
"return inp.value;})()" "return inp.value;})()"
@@ -1016,7 +1019,9 @@ def set_scan_date(cdp, placeholder, value, target_ymd=None):
return True return True
time.sleep(0.4) time.sleep(0.4)
# 全部尝试失败:告警,避免静默下成「今天」 # 全部尝试失败:告警,避免静默下成「今天」
print(f" ⚠ set_scan_date 未能将 {placeholder} 设为目标日期 {target_ymd}(请检查 DatePicker 是否就绪)") print(
f" ⚠ set_scan_date 未能将 {placeholder} 设为目标日期 {target_ymd}(请检查 DatePicker 是否就绪)"
)
return False return False
@@ -1158,8 +1163,12 @@ def wait_scan_form_ready(cdp, timeout=20.0):
f".find(i=>i.placeholder==={json.dumps(ph)}); if(!inp) return false; inp.focus(); return true;}})()" f".find(i=>i.placeholder==={json.dumps(ph)}); if(!inp) return false; inp.focus(); return true;}})()"
) )
try: try:
wait_until(cdp, "!!document.querySelector('.ant-picker-panel')", wait_until(
"预热面板打开", timeout=4.0) cdp,
"!!document.querySelector('.ant-picker-panel')",
"预热面板打开",
timeout=4.0,
)
except Exception: except Exception:
pass pass
cdp.eval( cdp.eval(

View File

@@ -1,11 +1,11 @@
# site_baishi.py # sites/baishi.py
import os import os
import yaml import yaml
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store from inbound_verify import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3): def with_retry(site_name, label, flow, reset, max_attempts=3):
@@ -181,8 +181,12 @@ def baishi_download_undelivered_data_impl(page):
# 10(应扫) 11(已扫) 12(未扫) 13(率)。(与下方 nth(12) 未扫同源) # 10(应扫) 11(已扫) 12(未扫) 13(率)。(与下方 nth(12) 未扫同源)
try: try:
_first_row = page.locator(".ant-table-tbody > tr").first _first_row = page.locator(".ant-table-tbody > tr").first
_exp_txt = _first_row.locator("td").nth(10).inner_text().strip().replace(",", "") _exp_txt = (
_arr_txt = _first_row.locator("td").nth(11).inner_text().strip().replace(",", "") _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): def _to_int(v):
try: try:

View File

@@ -1,4 +1,4 @@
# site_shunxin.py # sites/shunxin.py
import os import os
import re import re
@@ -7,8 +7,8 @@ import yaml
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pandas as pd import pandas as pd
from paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store from inbound_verify import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3): def with_retry(site_name, label, flow, reset, max_attempts=3):

View File

@@ -1,4 +1,4 @@
# site_yunda.py # sites/yunda.py
import os import os
import re import re
@@ -7,8 +7,8 @@ import yaml
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pandas as pd import pandas as pd
from paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store from inbound_verify import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3): def with_retry(site_name, label, flow, reset, max_attempts=3):

View File

@@ -1,4 +1,4 @@
# site_zto.py # sites/zto.py
import os import os
import re import re
@@ -7,8 +7,8 @@ import yaml
from datetime import datetime from datetime import datetime
import pandas as pd import pandas as pd
from paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
import state_store from inbound_verify import state_store
def with_retry(site_name, label, flow, reset, max_attempts=3): def with_retry(site_name, label, flow, reset, max_attempts=3):

View File

@@ -9,7 +9,7 @@ import os
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from paths import STATE_DB_PATH from inbound_verify.paths import STATE_DB_PATH
# 登录态枚举 # 登录态枚举
LOGIN_UNKNOWN = "unknown" # 尚未探测过 LOGIN_UNKNOWN = "unknown" # 尚未探测过

View File

@@ -28,9 +28,11 @@ import psycopg
import yaml import yaml
from psycopg.types.json import Jsonb from psycopg.types.json import Jsonb
from paths import BASE_DIR, CONFIG_PATH, DOWNLOAD_DIR from inbound_verify.paths import BASE_DIR, CONFIG_PATH, DOWNLOAD_DIR
import expected_undelivered as eu # 复用站点 / 文件名 / 列映射 / 基号口径(单一来源) from inbound_verify import (
expected_undelivered as eu,
) # 复用站点 / 文件名 / 列映射 / 基号口径(单一来源)
SCHEMA_PATH = os.path.join(BASE_DIR, "schema.sql") SCHEMA_PATH = os.path.join(BASE_DIR, "schema.sql")
@@ -376,7 +378,7 @@ def ingest(site=None):
# ============================== 命令行 ============================== # ============================== 命令行 ==============================
def _cli(): def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else "all" cmd = sys.argv[1] if len(sys.argv) > 1 else "all"
site = sys.argv[2] if len(sys.argv) > 2 else None site = sys.argv[2] if len(sys.argv) > 2 else None
if cmd == "createdb": if cmd == "createdb":
@@ -395,4 +397,4 @@ def _cli():
if __name__ == "__main__": if __name__ == "__main__":
_cli() main()