修复安能实到日期设定 + 站点统计口径重构

- site_anneng.py: set_scan_date 改用 execCommand 全选替换+回车(修复 Ant DatePicker 受控组件未写入导致实到下成当天); wait_scan_form_ready 增加 settle 与 DatePicker 预热,消除首设竞态
- expected_undelivered.py: 应到口径改交接件数、实到改单号去重、未到明细列已到单号(不再编子单号),百世排除
- docs: 补充站点统计逻辑审查报告与韵达/安能计算逻辑梳理
This commit is contained in:
Misaka
2026-07-19 13:16:01 +08:00
parent f3ff188302
commit 8f79cbc5d7
4 changed files with 430 additions and 122 deletions

View File

@@ -962,21 +962,62 @@ def anneng_expected_download_impl():
# ====================================================================
def set_scan_date(cdp, placeholder, value):
"""向扫描时间输入框写入日期时间并回车接受Ant 可编辑 DatePicker)。"""
def set_scan_date(cdp, placeholder, value, target_ymd=None):
"""向扫描时间输入框写入日期时间并回车接受Ant Design DatePickerrc-picker 受控组件)。
关键rc-picker 是 React 受控组件,原生 setter 直接改 .value 会被其重渲染冲掉;且控件里
往往已有默认日期,必须先「全选」再整体替换为新值。正确做法对齐人工操作:
进入控件(焦点激活) → 全选既有文本 → insertText 整体替换(派发 input 事件, onChange 受理) → 回车提交。
target_ymd 可显式传入 (y,m,d) 用于写后回读校验;不传则从 value 解析。
返回 True 表示写入并校验成功,否则 False。
"""
import re as _re
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;})()"
)
val_json = json.dumps(value)
if target_ymd is None:
m = _re.search(r"(\d{4})[-/](\d{1,2})[-/](\d{1,2})", value)
target_ymd = (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None
def _norm(s):
if not s:
return None
mm = _re.search(r"(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})", s)
return (int(mm.group(1)), int(mm.group(2)), int(mm.group(3))) if mm else None
for attempt in range(6):
# 1) 进入控件focus + click 让光标处于激活状态)
cdp.eval(
"(() => {const inp=[...document.querySelectorAll('input')]"
f".find(i=>i.placeholder==={ph}); if(!inp) return false; inp.focus(); inp.click(); return true;}})()"
)
time.sleep(0.35)
# 2) 全选控件里既有的旧日期 → insertText 整体替换为新值rc-picker 的 onChange 会受理);
# execCommand 不可用时回退原生 setter + input 事件。
wrote = cdp.eval(
"(() => {const inp=[...document.querySelectorAll('input')]"
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 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;"
f" s.call(inp,{val_json}); inp.dispatchEvent(new Event('input',{{bubbles:true}})); }}"
"return inp.value;})()"
)
# 3) 回车提交(文本输入完成按回车即完成设定)
cdp.eval(
"(() => {const inp=[...document.querySelectorAll('input')]"
f".find(i=>i.placeholder==={ph}); if(!inp) return false;"
"inp.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true}));"
"inp.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true}));"
"return true;})()"
)
# 4) 写后回读校验rc-picker 提交后 input.value 会被清空,故只校验「写后瞬间」的值)
if target_ymd is not None and _norm(wrote) == target_ymd:
return True
time.sleep(0.4)
# 全部尝试失败:告警,避免静默下成「今天」
print(f" ⚠ set_scan_date 未能将 {placeholder} 设为目标日期 {target_ymd}(请检查 DatePicker 是否就绪)")
return False
def select_zidan(cdp):
@@ -1095,9 +1136,11 @@ def goto_next_page(cdp):
def wait_scan_form_ready(cdp, timeout=20.0):
"""等扫描查询表单渲染完(日期输入框 + 子单 选项出现)。
"""等扫描查询表单渲染完(日期输入框 + 子单 选项出现),并预热 DatePicker 使其完全挂载
新开的 tab 里 URL 一匹配就返回,但 Ant 表单还在渲染,此时设日期/选子单会落空。
这里除了等元素出现,再额外 settle 一段时间,并依次 focus 两个 DatePicker 打开面板再
Esc 关闭,强制 rc-picker 完成挂载,避免随后 set_scan_date 写入的值被重渲染冲掉。
"""
wait_until(
cdp,
@@ -1106,6 +1149,26 @@ def wait_scan_form_ready(cdp, timeout=20.0):
"扫描查询表单加载",
timeout=timeout,
)
# 额外 settle等 SPA 把表单真正挂载稳定(避免出现后又重渲染把值刷掉)
time.sleep(1.2)
# 预热:依次 focus 两个 DatePicker 打开面板再 Esc 关闭,强制 rc-picker 完全挂载
for ph in ("开始日期", "结束日期"):
cdp.eval(
"(() => {const inp=[...document.querySelectorAll('input')]"
f".find(i=>i.placeholder==={json.dumps(ph)}); if(!inp) return false; inp.focus(); return true;}})()"
)
try:
wait_until(cdp, "!!document.querySelector('.ant-picker-panel')",
"预热面板打开", timeout=4.0)
except Exception:
pass
cdp.eval(
"(() => {const inp=[...document.querySelectorAll('input')]"
f".find(i=>i.placeholder==={json.dumps(ph)}); if(!inp) return false;"
"inp.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',code:'Escape',keyCode:27,which:27,bubbles:true}));"
"return true;})()"
)
time.sleep(0.3)
def wait_query_settled(cdp, timeout=20.0):