diff --git a/inbound_verify/sites/anneng.py b/inbound_verify/sites/anneng.py index 284707b..8193bc6 100644 --- a/inbound_verify/sites/anneng.py +++ b/inbound_verify/sites/anneng.py @@ -11,8 +11,8 @@ # # 应到 = 运单信息导出(每张交接单下应到的运单明细)。整体流程: # 主页(重庆鱼洞镇) → 运营管理 → 进站管理 → 进站交接单查询(tab) -# → 设日期 → 查询 → 等“带小数点的 0”出现(数据加载完毕) -# → 逐条交接单:记下交接单号 → 双击 → 等运单信息加载并核对单号 +# → 设日期 → 等 ZK 组件值提交确认 → 查询 → 等“带小数点的 0”出现(数据加载完毕) +# → 逐条交接单(交接日期超出范围的不导出):记下交接单号 → 双击 → 等运单信息加载并核对单号 # → 导出 → 双击转移全部待选字段 → 导出数据 → 任务添加成功 → 确认 → 回交接单信息 # → 关闭进站交接单查询 tab → 打开导出下载(tab) # → 轮询直到本批“交接单明细”任务全部导出完成 → 逐个免对话框下载(x-auth+TGC 直连 GET) @@ -30,7 +30,7 @@ import sys import time import urllib.parse import urllib.request -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import pandas as pd import websocket @@ -375,25 +375,57 @@ def wait_query_page_ready(tab_cdp): def set_zk_datebox(tab_cdp, index, date_str): - """直接向第 index 个 z-datebox-inp 写入日期(YYYY-MM-DD),并触发 change。 + """向第 index 个 z-datebox-inp 真实键入日期(YYYY-MM-DD),JS blur 触发提交。 - 安能的日期可直接输入(无需日历控件)。ZK datebox 监听 input 的 change 事件。 + 安能日期框是 ZK 组件:直接用原生 setter 改 value + 派发 input/change 事件不会 + 触发 ZK 的 onChange AU(服务端拿不到新值,查询仍用旧范围);必须用 CDP Input 域 + 真实键入(insertText),再 JS blur() 让 ZK 解析、校验并提交 onChange。 + 不用 Tab 按键触发 blur:CDP 按键事件依赖窗口在前台,窗口在后台时会被丢弃。 + 超出页面允许的查询范围时 ZK 会拒绝(不发 onChange),由 + wait_zk_datebox_committed 等待超时暴露出来。 """ - js = ( + tab_cdp.eval( "(() => {" f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];" " if (!inp) return false;" " inp.focus();" - " const setter = Object.getOwnPropertyDescriptor(" - " window.HTMLInputElement.prototype, 'value').set;" - f" setter.call(inp, {json.dumps(date_str)});" - " inp.dispatchEvent(new Event('input', {bubbles:true}));" - " inp.dispatchEvent(new Event('change', {bubbles:true}));" - " inp.blur();" - " return inp.value;" + " inp.select();" + " return true;" "})()" ) - return tab_cdp.eval(js) + time.sleep(0.15) + tab_cdp.call("Input.insertText", text=date_str) + time.sleep(0.2) + tab_cdp.eval( + "(() => {" + f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];" + " if (inp) inp.blur();" + " return true;" + "})()" + ) + + +def wait_zk_datebox_committed(tab_cdp, index, date_str, timeout=5.0): + """轮询 ZK 组件值已提交到服务端(读 zk.Widget 的 getText()),确认后再返回。 + + 真实键入 + blur 触发 onChange AU 后,服务端接受才会把新值同步回组件(setAttr), + 因此 getText() 等于目标日期即代表服务端已确认。超时说明日期被页面拒绝 + (大概率超出该页允许的查询范围),会抛超时报错而不是用旧范围静默查询。 + """ + js_date = json.dumps(date_str) + wait_until( + tab_cdp, + "(() => {" + f" const inp = document.querySelectorAll('input.z-datebox-inp')[{index}];" + " if (!inp || typeof zk === 'undefined') return false;" + " const w = zk.Widget.$(inp);" + " if (!w || typeof w.getText !== 'function') return false;" + " const t = String(w.getText() ?? '').trim();" + f" return t === {js_date} || t.split(' ')[0] === {js_date};" + "})()", + f"ZK 日期组件[{index}]已提交 {date_str}", + timeout=timeout, + ) def click_query_button(tab_cdp): @@ -421,12 +453,54 @@ def wait_query_done(tab_cdp): ) -def collect_jiaojie_dan_ids(tab_cdp): - """收集交接单信息表里所有交接单号(从行复选框的 ewbsListNo 解析,按出现顺序)。 +_JAVA_MONTHS = { + m: i + for i, m in enumerate( + [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ], + start=1, + ) +} - 交接单号是 19 位纯数字,必须按文本保留,避免精度丢失。 + +def _parse_java_date(text): + """解析 Java Date.toString()(如 'Mon Aug 03 05:50:46 CST 2026')为 date。 + + 固定 token 切分,避免 strptime 对 %Z/%a/%b 的 locale 依赖。 """ - return ( + parts = (text or "").split() + if len(parts) != 6: + return None + month = _JAVA_MONTHS.get(parts[1]) + if not month: + return None + try: + return date(int(parts[5]), month, int(parts[2])) + except (TypeError, ValueError): + return None + + +def collect_jiaojie_dan_ids(tab_cdp, start=None, end=None): + """收集交接单信息表里的交接单号(从行复选框的 ewbsListNo 解析,按出现顺序)。 + + 同时解析每行 value 里的 createdTime(交接时间)。传 start/end(date)时, + 只保留交接日期在 [start, end] 内的交接单;超期或日期解析失败的只跳过、不中断, + 数量计入返回的 skipped。交接单号是 19 位纯数字,必须按文本保留,避免精度丢失。 + 返回 (ids, skipped)。 + """ + rows = ( tab_cdp.eval( "(() => {" " const boxes = [...document.querySelectorAll('.z-listbox')];" @@ -439,14 +513,25 @@ def collect_jiaojie_dan_ids(tab_cdp): " const rows = [...box.querySelectorAll('tr.z-listitem')]" " .filter(tr => tr.querySelector('input[value*=\"ewbsListNo=\"]'));" " return rows.map(tr => {" - " const inp = tr.querySelector('input[value*=\"ewbsListNo=\"]');" - " const m = /ewbsListNo=(\\d+)/.exec(inp.value || '');" - " return m ? m[1] : null;" + " const v = (tr.querySelector('input[value*=\"ewbsListNo=\"]').value) || '';" + " const m = /ewbsListNo=(\\d+)/.exec(v);" + " if (!m) return null;" + " const c = /createdTime=([^,\\]]+)/.exec(v);" + " return {id: m[1], created: c ? c[1] : null};" " }).filter(Boolean);" "})()" ) or [] ) + ids, skipped = [], 0 + for rec in rows: + created = _parse_java_date(rec.get("created")) if rec.get("created") else None + if start is not None and end is not None: + if created is None or not (start <= created <= end): + skipped += 1 + continue + ids.append(rec["id"]) + return ids, skipped def dblclick_jiaojie_dan_row(tab_cdp, ewbs_no): @@ -947,25 +1032,41 @@ def anneng_expected_download_impl(force=False, date=None): wait_query_page_ready(tab_cdp) print("✅ 进站交接单查询页已加载") - # 2) 设日期 + 查询 + 等加载 + # 2) 设日期 + 提交确认 + 查询 + 等加载 print(">> 正在设置查询时间范围(直接输入日期)...") set_zk_datebox(tab_cdp, 0, start_str) + # ZK 组件值异步提交:逐个设置并等服务端确认后再设下一个, + # 避免前一个 datebox 的 AU 响应与后一个的键入互相干扰(曾导致偶发提交超时); + # 未确认前点查询会按旧的时间范围查询(安能页面特性)。 + wait_zk_datebox_committed(tab_cdp, 0, start_str) set_zk_datebox(tab_cdp, 1, today_str) - time.sleep(0.3) - print(">> 正在执行查询 ...") + wait_zk_datebox_committed(tab_cdp, 1, today_str) + print(">> 时间范围已提交确认,正在执行查询 ...") click_query_button(tab_cdp) wait_query_done(tab_cdp) print("✅ 查询完成,数据已加载") # 3) 收集交接单号并逐条导出 - # 行的渲染可能比统计的小数点晚一拍:轮询等行出现;持续为空才视为无数据。 + # 空结果页面会出现「没有任何相关信息」空态文案;若旧行还没被清掉, + # 靠交接日期范围过滤兜底。持续为空才视为无数据。 target_ids = [] + skipped_ids = 0 collect_deadline = time.monotonic() + 15.0 while time.monotonic() < collect_deadline: - target_ids = collect_jiaojie_dan_ids(tab_cdp) + if tab_cdp.eval("document.body.innerText.includes('没有任何相关信息')"): + print(">> 查询完成但无交接单数据(页面空态),结束。") + return + target_ids, skipped_ids = collect_jiaojie_dan_ids( + tab_cdp, start=target.date(), end=target.date() + ) if target_ids: break time.sleep(0.5) + if skipped_ids: + print( + f">> 忽略 {skipped_ids} 条超期/日期解析失败的交接单" + f"(超出 [{start_str} ~ {today_str}] 范围)" + ) print(f">> 共捕获 {len(target_ids)} 条交接单记录") if not target_ids: print(">> ⚠ 没有交接单数据,结束。")