From dea9ad6e07cbb6620cc697a086fa040f16b9523e Mon Sep 17 00:00:00 2001 From: Misaka Date: Sun, 21 Jun 2026 22:12:23 +0800 Subject: [PATCH] Add Anneng actual-data (scan) download and wire into automation test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 安能站点第二阶段:实到数据下载 + 接入自动化测试。 - site_anneng.py:新增 anneng_actual_download()。网点到件扫描查询页是 Ant Design 且无导出功能——综合查询→扫描查询→网点到件扫描查询(新)→ 设扫描时间(直接输入 +回车)→ 选「子单」→ 每页 500(size-changer 用 mousedown 展开)→ 查询 → 直接 从表格 DOM 抽数、逐页翻页取完 → 存 安能-实到货物数据.xlsx → 关 tab。复用应到的 CDP/菜单/tab 辅助;扫描专有逻辑单独成段。main() 支持 `actual` 参数独立跑实到。 修掉复用 tab 时首查读到旧总数的问题(查询后等总数变化再读)。 - main_router.py:菜单加 [11] 实到货物数据下载;run_automation_test 的 flow_table 加入安能(expected/actual),执行循环对应用类站点(APP_SITES)走无 page 分支。 Co-Authored-By: Claude --- main_router.py | 18 +++- site_anneng.py | 288 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 302 insertions(+), 4 deletions(-) diff --git a/main_router.py b/main_router.py index 4288473..ec6b12c 100644 --- a/main_router.py +++ b/main_router.py @@ -166,6 +166,10 @@ def run_automation_test(pages_map): "expected": ("应到", site_yunda.yunda_expected_download), "actual": ("实到", site_yunda.yunda_actual_download), }, + "安能": { + "expected": ("应到", site_anneng.anneng_expected_download), + "actual": ("实到", site_anneng.anneng_actual_download), + }, } # 构建测试计划:[(站点, 流程中文名, 流程函数), ...] @@ -195,13 +199,17 @@ def run_automation_test(pages_map): print("\n----------------------------------------------------") print(f"[步骤 {idx}/{total}] 站点【{site_name}】流程【{label}】") print("----------------------------------------------------") - page = pages_map[site_name] start = time.time() status = "PASS" err = "" try: - page.bring_to_front() - ret = func(page) + if site_name in APP_SITES: + # 安能:Electron 应用,无 Playwright page,函数不收 page 参数 + ret = func() + else: + page = pages_map[site_name] + page.bring_to_front() + ret = func(page) if ret is False: status = "FAIL" err = "流程返回失败状态" @@ -443,6 +451,7 @@ def run_multi_site_daemon(): print("-" * 52) print(" 模块五:【安能】数据处理流(Electron 应用)") print(" [10] 执行 - 应到货物数据下载(运单信息)") + print(" [11] 执行 - 实到货物数据下载(网点到件扫描)") print("-" * 52) print(" 自动化测试") print(" [8] 执行 - 全站点下载流程自动化测试 (交叉跑通校验)") @@ -479,6 +488,8 @@ def run_multi_site_daemon(): site_yunda.yunda_actual_download(pages_map["韵达"]) elif choice == "10" and is_site_ready("安能"): site_anneng.anneng_expected_download() + elif choice == "11" and is_site_ready("安能"): + site_anneng.anneng_actual_download() elif choice == "8": run_automation_test(pages_map) elif choice == "9": @@ -503,6 +514,7 @@ def run_multi_site_daemon(): "8", "9", "10", + "11", "0", ]: print("\n⚠️ 无效输入,请查证后回车。") diff --git a/site_anneng.py b/site_anneng.py index bce874d..de13c4c 100644 --- a/site_anneng.py +++ b/site_anneng.py @@ -62,6 +62,10 @@ JIAOJIEDAN_URL_HINT = ( ) EXPORT_TAB_URL_HINT = "exportAllRecords" +# 实到(网点到件扫描查询)页相关 +SCAN_TAB_URL_HINT = "scan/scan/toSend" +ACTUAL_FINAL_FILENAME = "安能-实到货物数据.xlsx" + # ==================================================================== # CDP 基础层 @@ -887,8 +891,290 @@ def anneng_expected_download(): main_cdp.close() +# ==================================================================== +# 实到数据:网点到件扫描查询(综合查询 → 扫描查询) +# 该页是 Ant Design,且无导出功能——直接从表格 DOM 抽数据,逐页翻页取完。 +# ==================================================================== + + +def set_scan_date(cdp, placeholder, value): + """向扫描时间输入框写入日期时间并回车接受(Ant 可编辑 DatePicker)。""" + ph = json.dumps(placeholder) + return cdp.eval( + "(() => {const inp = [...document.querySelectorAll('input')]" + f".find(i => i.placeholder === {ph});" + "if (!inp) return false; inp.focus();" + "const setter = Object.getOwnPropertyDescriptor(" + " window.HTMLInputElement.prototype, 'value').set;" + f" setter.call(inp, {json.dumps(value)});" + " inp.dispatchEvent(new Event('input', {bubbles:true}));" + " inp.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter', code:'Enter'," + " keyCode:13, which:13, bubbles:true}));" + " return inp.value;})()" + ) + + +def select_zidan(cdp): + """选择扫描记录类型「子单」,并确认其 radio 被选中(表单未就绪/点击未生效则重试)。""" + deadline = time.monotonic() + 10.0 + while time.monotonic() < deadline: + cdp.eval( + "(() => {const t = [...document.querySelectorAll('label')]" + ".find(l => l.textContent.trim() === '子单');" + "if (t) t.click(); return true;})()" + ) + time.sleep(0.2) + checked = cdp.eval( + "(() => {const t = [...document.querySelectorAll('label')]" + ".find(l => l.textContent.trim() === '子单');" + "if (!t) return null; const r = t.querySelector('input[type=radio]');" + "return r ? r.checked : null;})()" + ) + if checked: + return True + return False + + +def click_scan_query(cdp): + cdp.eval( + "(() => {const b = [...document.querySelectorAll('button.ant-btn-primary')]" + ".find(b => b.textContent.trim().includes('查询'));" + "if (b) b.click(); return !!b;})()" + ) + + +def set_page_size_500(cdp): + """把每页大小改成 500(Ant 分页 size-changer,需 mousedown 才能展开下拉)。""" + cdp.eval( + "(() => {const sel = document.querySelector(" + " '.ant-pagination .ant-select-selector');" + "if (!sel) return false; sel.dispatchEvent(new MouseEvent('mousedown',{bubbles:true}));" + "sel.dispatchEvent(new MouseEvent('click',{bubbles:true})); return true;})()" + ) + wait_until( + cdp, + "[...document.querySelectorAll('.ant-select-item-option')]" + ".some(o => o.textContent.includes('500'))", + "每页大小下拉出现", + timeout=5.0, + ) + cdp.eval( + "(() => {const opt = [...document.querySelectorAll('.ant-select-item-option')]" + ".find(o => o.textContent.includes('500')); if (opt) opt.click(); return !!opt;})()" + ) + time.sleep(0.5) + + +def read_scan_total(cdp): + """读取“总共: N 条”里的 N。""" + txt = cdp.eval( + "document.querySelector('.ant-pagination-total-text')?.textContent?.trim() || ''" + ) + m = "".join(ch for ch in (txt or "") if ch.isdigit()) + return int(m) if m else 0 + + +def extract_scan_rows(cdp): + """抽取当前页所有数据行(按表头建 dict,丢弃空表头列与“操作”列)。""" + return ( + cdp.eval( + "(() => {" + " const headers = [...document.querySelectorAll('.ant-table-thead th')]" + " .map(th => th.textContent.trim());" + " const rows = [...document.querySelectorAll(" + " '.ant-table-tbody tr.ant-table-row')];" + " return rows.map(tr => {" + " const cells = [...tr.querySelectorAll('td')].map(td => td.textContent.trim());" + " const o = {};" + " headers.forEach((h, i) => {" + " if (h && h !== '操作' && cells[i] !== undefined) o[h] = cells[i];" + " });" + " return o;" + " });" + "})()" + ) + or [] + ) + + +def _has_next_page(cdp): + return cdp.eval( + "(() => {const n = document.querySelector('.ant-pagination-next');" + "return !!(n && !n.classList.contains('ant-pagination-disabled'));})()" + ) + + +def _active_page(cdp): + return cdp.eval( + "document.querySelector('.ant-pagination-item-active')?.textContent?.trim() || ''" + ) + + +def goto_next_page(cdp): + """点下一页,并等到 active 页号 +1、行重新出现。""" + cur = _active_page(cdp) + cdp.eval( + "(() => {const n = document.querySelector('.ant-pagination-next');" + "if (n) n.click(); return true;})()" + ) + target = str(int(cur) + 1) if cur.isdigit() else None + if target: + wait_until( + cdp, + f"document.querySelector('.ant-pagination-item-active')?.textContent?.trim()" + f" === {json.dumps(target)}", + f"翻到第 {target} 页", + timeout=20.0, + ) + time.sleep(0.6) + + +def wait_scan_form_ready(cdp, timeout=20.0): + """等扫描查询表单渲染完(日期输入框 + 子单 选项出现)。 + + 新开的 tab 里 URL 一匹配就返回,但 Ant 表单还在渲染,此时设日期/选子单会落空。 + """ + wait_until( + cdp, + "(() => !!document.querySelector(\"input[placeholder='开始日期']\")" + " && [...document.querySelectorAll('label')].some(l => l.textContent.trim()==='子单'))()", + "扫描查询表单加载", + timeout=timeout, + ) + + +def wait_query_settled(cdp, timeout=20.0): + """点查询后等结果稳定:先等“总共: N 条”出现(查询完成),再等表格行刷新。""" + try: + wait_until( + cdp, + "!!document.querySelector('.ant-pagination-total-text')", + "扫描查询完成", + timeout=timeout, + ) + except TimeoutError: + pass + try: + wait_until( + cdp, + "(() => {const b = document.querySelector('.ant-table-tbody');" + "return !!b && (b.querySelectorAll('tr.ant-table-row').length > 0 " + "|| (b.textContent || '').includes('暂无数据'));})()", + "扫描表格行刷新", + timeout=10.0, + ) + except TimeoutError: + pass + time.sleep(0.4) + + +def wait_total_changed(cdp, old_total, timeout=20.0): + """等“总共: N 条”里的数值变成与 old_total 不同(用于复用 tab 时避免读到上一次的旧总数)。""" + try: + wait_until( + cdp, + "(() => {const m = (document.querySelector('.ant-pagination-total-text')" + "?.textContent || '').match(/\\d+/);" + f"return !!m && parseInt(m[0]) !== {int(old_total)};}})()", + "扫描查询完成(总数更新)", + timeout=timeout, + ) + except TimeoutError: + pass + + +def _save_actual(rows, download_dir): + out = os.path.join(download_dir, ACTUAL_FINAL_FILENAME) + df = pd.DataFrame(rows) + df.to_excel(out, index=False) + print("====================================================") + print(f" 已保存:{ACTUAL_FINAL_FILENAME}(共 {len(df)} 行)") + print(f" 📁 输出路径: {out}") + print("====================================================") + + +def anneng_actual_download(): + """安能:实到数据(网点到件扫描,子单)下载,完整流程。""" + print("\n▶ 开始执行【安能 - 实到货物数据下载】任务 ...") + download_dir = DOWNLOAD_DIR + os.makedirs(download_dir, exist_ok=True) + + days = _load_query_days() + today = datetime.now() + start = today - timedelta(days=days - 1) + start_str = f"{start.year}/{start.month:02d}/{start.day:02d} 00:00:00" + end_str = f"{today.year}/{today.month:02d}/{today.day:02d} 23:59:59" + print(f">> 扫描时间范围: [{start_str}] 至 [{end_str}](近 {days} 天)") + + main_cdp = find_main_page_cdp() + try: + # 1) 导航到扫描查询 tab + print(">> 导航菜单:综合查询 → 扫描查询 → 网点到件扫描查询(新)") + open_submenus(main_cdp, ["综合查询", "扫描查询"]) + cdp = ensure_tab_open(main_cdp, "网点到件扫描查询(新)", SCAN_TAB_URL_HINT) + try: + wait_scan_form_ready(cdp) + print("✅ 扫描查询页已加载") + # 2) 设扫描时间 + print(">> 设置扫描时间范围 …") + set_scan_date(cdp, "开始日期", start_str) + set_scan_date(cdp, "结束日期", end_str) + time.sleep(0.3) + # 3) 选子单 + print(">> 选择扫描记录类型:子单 …") + if not select_zidan(cdp): + raise RuntimeError("未能选中扫描类型「子单」") + time.sleep(0.3) + # 4) 首次查询(让分页出现)。复用 tab 时先记下旧总数,查询后等它变化,避免读到旧值。 + print(">> 执行查询 …") + old_total = read_scan_total(cdp) + click_scan_query(cdp) + wait_total_changed(cdp, old_total) + wait_query_settled(cdp) + total = read_scan_total(cdp) + print(f" · 总共 {total} 条") + if total == 0: + print(">> ⚠ 无扫描数据,结束。") + _save_actual([], download_dir) + return + # 5) 改每页 500 并重新查询 + print(">> 设置每页 500 条 …") + set_page_size_500(cdp) + click_scan_query(cdp) + wait_query_settled(cdp) + # 6) 逐页抽取 + all_rows = [] + page_no = 1 + while True: + rows = extract_scan_rows(cdp) + all_rows.extend(rows) + print(f" · 第 {page_no} 页抽取 {len(rows)} 条,累计 {len(all_rows)}") + if not _has_next_page(cdp): + break + goto_next_page(cdp) + page_no += 1 + + print(f">> 共抽取 {len(all_rows)} 条扫描记录") + _save_actual(all_rows, download_dir) + print("✅ 安能实到数据下载流程完成。") + finally: + cdp.close() + # 7) 关闭扫描查询 tab(用完即关) + print(">> 关闭【网点到件扫描查询(新)】tab …") + close_tab_by_label(main_cdp, "网点到件扫描查询(新)") + except Exception as e: + print(f"\n❌ 任务执行过程中发生异常: {e}") + return False + finally: + main_cdp.close() + + def main(): - anneng_expected_download() + flow = sys.argv[1] if len(sys.argv) > 1 else "expected" + if flow == "actual": + anneng_actual_download() + else: + anneng_expected_download() if __name__ == "__main__":