Add Anneng actual-data (scan) download and wire into automation test

安能站点第二阶段:实到数据下载 + 接入自动化测试。

- 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 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-06-21 22:12:23 +08:00
parent 9d3d3cbb78
commit dea9ad6e07
2 changed files with 302 additions and 4 deletions

View File

@@ -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):
"""把每页大小改成 500Ant 分页 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__":