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:
288
site_anneng.py
288
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__":
|
||||
|
||||
Reference in New Issue
Block a user