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:
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
@@ -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),
|
||||||
}
|
}
|
||||||
@@ -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(
|
||||||
@@ -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:
|
||||||
@@ -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):
|
||||||
@@ -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):
|
||||||
@@ -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):
|
||||||
@@ -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" # 尚未探测过
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user