364 lines
13 KiB
Python
364 lines
13 KiB
Python
# 合同 S12:暂存后提交。成功后不要等详情页。
|
||
#
|
||
# 测试环境直接提交会落到「该文档不存在」,所以先点暂存再点提交。
|
||
# dry_run 不点提交,返回 False。
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
|
||
from accept import evidence_shot
|
||
|
||
from contract_common import (
|
||
EDIT_PAGE_URL,
|
||
BrowserAdapter,
|
||
ContractFlowContext,
|
||
_ACCEPT,
|
||
_click_toolbar_button,
|
||
_close_blocking_dialogs,
|
||
_current_url,
|
||
_lui_toolbar_button_exists,
|
||
_read_document_id,
|
||
_wait_lui_toolbar_button,
|
||
)
|
||
|
||
|
||
_ERROR_MARKS = (
|
||
"不能为空",
|
||
"请填写",
|
||
"校验失败",
|
||
"提交失败",
|
||
"必填项未填",
|
||
"该文档不存在",
|
||
"操作失败",
|
||
)
|
||
_SUCCESS_MARKS = ("您的操作已成功", "操作已成功", "提交成功", "流程已提交", "成功提交")
|
||
_CONFIRM_MARKS = ("确定提交", "是否提交", "确认提交", "是否确定", "确定要提交")
|
||
|
||
|
||
# 暂存后提交(S12)。dry_run 不点提交。成功后不要等详情页。
|
||
def submit_contract_for_approval(
|
||
browser: BrowserAdapter, ctx: ContractFlowContext, *, fd_id: str = ""
|
||
) -> bool:
|
||
if ctx.submit_mode == "dry_run":
|
||
evidence_shot(browser, "contract-dry-run-ready")
|
||
_ACCEPT.note("S12", "提交", {"mode": "dry_run", "submitted": "no"}, ok=True)
|
||
return False
|
||
_close_blocking_dialogs(browser)
|
||
fd_id = (fd_id or "").strip() or _read_document_id(browser)
|
||
print(f"S12 fdId={fd_id or '-'}", flush=True)
|
||
# 测试环境直接提交会落到「该文档不存在」:先暂存落库,再提交。合同号从详情 #_xform_docNumber 读。
|
||
try:
|
||
saved_click = _click_toolbar_button(browser, "暂存")
|
||
except Exception as exc:
|
||
print(f"暂存未点到:{exc}", flush=True)
|
||
saved_click = False
|
||
save_kind, save_text = _wait_submit_outcome(browser, timeout_seconds=15)
|
||
if save_kind == "error":
|
||
evidence_shot(browser, "contract-submitted", failed=True)
|
||
_ACCEPT.note(
|
||
"S12",
|
||
"提交",
|
||
{"submitted": "no", "error": save_text, "at": "暂存", "clickedSave": "yes" if saved_click else "no"},
|
||
ok=False,
|
||
raise_error=False,
|
||
)
|
||
return False
|
||
if save_kind != "success":
|
||
print("暂存后仍在表单页,继续点提交。", flush=True)
|
||
fd_id = fd_id or _read_document_id(browser)
|
||
if not _lui_toolbar_button_exists(browser, "提交"):
|
||
if not fd_id:
|
||
evidence_shot(browser, "contract-submitted", failed=True)
|
||
_ACCEPT.note(
|
||
"S12",
|
||
"提交",
|
||
{"submitted": "no", "error": "暂存后离开表单且无 fdId,无法再点提交", "clicked": "no"},
|
||
ok=False,
|
||
raise_error=False,
|
||
)
|
||
return False
|
||
print(f"暂存后打开编辑页 fdId={fd_id}", flush=True)
|
||
browser.open_url(EDIT_PAGE_URL.format(fd_id=fd_id))
|
||
if not _wait_lui_toolbar_button(browser, "提交", timeout_seconds=12):
|
||
evidence_shot(browser, "contract-submitted", failed=True)
|
||
_ACCEPT.note(
|
||
"S12",
|
||
"提交",
|
||
{"submitted": "no", "error": "编辑页未见提交按钮 lui_widget_btn_txt", "clicked": "no", "fdId": fd_id},
|
||
ok=False,
|
||
raise_error=False,
|
||
)
|
||
return False
|
||
try:
|
||
clicked = _click_toolbar_button(browser, "提交")
|
||
except Exception as exc:
|
||
evidence_shot(browser, "contract-submitted", failed=True)
|
||
_ACCEPT.note("S12", "提交", {"submitted": "no", "error": str(exc)[:120], "clicked": "no"}, ok=False, raise_error=False)
|
||
return False
|
||
if not clicked:
|
||
evidence_shot(browser, "contract-submitted", failed=True)
|
||
_ACCEPT.note("S12", "提交", {"submitted": "no", "error": "未点到提交按钮 lui_widget_btn_txt", "clicked": "no"}, ok=False, raise_error=False)
|
||
return False
|
||
evidence_shot(browser, "contract-after-submit-click")
|
||
kind, dialog = _wait_submit_outcome(browser, timeout_seconds=20, trust_body_success=False)
|
||
if kind == "error":
|
||
evidence_shot(browser, "contract-submitted", failed=True)
|
||
_ACCEPT.note(
|
||
"S12",
|
||
"提交",
|
||
{"submitted": "no", "clicked": "yes", "error": dialog},
|
||
ok=False,
|
||
raise_error=False,
|
||
)
|
||
return False
|
||
print(f"提交已点 kind={kind} dialog={dialog[:80]!r},S13 从详情读合同号。", flush=True)
|
||
_ACCEPT.note(
|
||
"S12",
|
||
"提交",
|
||
{
|
||
"submitted": "yes",
|
||
"clicked": "yes",
|
||
"via": "暂存后提交",
|
||
"signal": kind,
|
||
"dialog": dialog[:80] or "(none)",
|
||
},
|
||
ok=True,
|
||
)
|
||
return True
|
||
|
||
|
||
# 从页面正文抽出提交失败原因。没有则返回空字符串。
|
||
def _submit_error_from_body(body: str) -> str:
|
||
text = body or ""
|
||
if "该文档不存在" in text:
|
||
return "该文档不存在"
|
||
if "操作失败" in text and "您的请求已提交" in text:
|
||
return "操作失败"
|
||
return ""
|
||
|
||
|
||
# 把弹窗文案收成 error / success / confirm / none。
|
||
def _classify_dialog_text(text: str) -> str:
|
||
blob = text or ""
|
||
for mark in _ERROR_MARKS:
|
||
if mark in blob:
|
||
return "error"
|
||
for mark in _SUCCESS_MARKS:
|
||
if mark in blob:
|
||
return "success"
|
||
for mark in _CONFIRM_MARKS:
|
||
if mark in blob:
|
||
return "confirm"
|
||
return "none"
|
||
|
||
|
||
# 等到提交弹窗给出成功或失败信号。超时返回 none。
|
||
def _wait_submit_outcome(
|
||
browser: BrowserAdapter,
|
||
timeout_seconds: float,
|
||
*,
|
||
trust_body_success: bool = True,
|
||
) -> tuple[str, str]:
|
||
deadline = time.time() + timeout_seconds
|
||
last_dialog = ""
|
||
saw_confirm = False
|
||
while time.time() < deadline:
|
||
kind, text, confirm = _read_submit_signal(
|
||
browser,
|
||
trust_body_success=trust_body_success or saw_confirm,
|
||
)
|
||
if confirm:
|
||
saw_confirm = True
|
||
if text:
|
||
last_dialog = text
|
||
if kind == "error":
|
||
return "error", text
|
||
if kind == "success":
|
||
return "success", text
|
||
time.sleep(0.4)
|
||
kind, text, _confirm = _read_submit_signal(
|
||
browser,
|
||
trust_body_success=trust_body_success or saw_confirm,
|
||
)
|
||
if kind in ("error", "success"):
|
||
return kind, text
|
||
return "none", text or last_dialog
|
||
|
||
|
||
# 读一次原生 alert 和 LUI 弹窗,返回 kind、文案、是否点过确认。
|
||
def _read_submit_signal(
|
||
browser: BrowserAdapter,
|
||
*,
|
||
trust_body_success: bool = True,
|
||
) -> tuple[str, str, bool]:
|
||
alert_text = _accept_native_alert(browser)
|
||
if alert_text:
|
||
kind = _classify_dialog_text(alert_text)
|
||
if kind == "none":
|
||
kind = "confirm"
|
||
print(f"S12 native alert kind={kind} text={alert_text[:80]!r}", flush=True)
|
||
if kind == "error":
|
||
return "error", alert_text, False
|
||
if kind == "success":
|
||
return "success", alert_text, False
|
||
if kind == "confirm":
|
||
return _finish_submit_signal(browser, alert_text, True, trust_body_success)
|
||
snapshot = _handle_submit_dialogs(browser)
|
||
dialog_text = str(snapshot.get("text") or "").strip()
|
||
kind = _classify_dialog_text(dialog_text) if dialog_text else "none"
|
||
if kind == "none":
|
||
kind = str(snapshot.get("kind") or "none")
|
||
if kind == "error":
|
||
return "error", dialog_text or "弹窗报错", False
|
||
if kind == "success":
|
||
return "success", dialog_text or "ok", False
|
||
return _finish_submit_signal(
|
||
browser,
|
||
dialog_text,
|
||
kind == "confirm" or bool(snapshot.get("clicked")),
|
||
trust_body_success,
|
||
)
|
||
|
||
|
||
# 弹窗之后再看正文和 URL,把提交结果收成 error / success / none。
|
||
def _finish_submit_signal(
|
||
browser: BrowserAdapter,
|
||
dialog_text: str,
|
||
saw_confirm: bool,
|
||
trust_body_success: bool,
|
||
) -> tuple[str, str, bool]:
|
||
body_error = _submit_error_from_body(browser.read_body_text() or "")
|
||
if body_error:
|
||
return "error", body_error, saw_confirm
|
||
if _submit_view_page(browser):
|
||
return "success", dialog_text or "view", saw_confirm
|
||
if (trust_body_success or saw_confirm) and _submit_success_text(browser):
|
||
return "success", dialog_text or "ok", saw_confirm
|
||
return "none", dialog_text, saw_confirm
|
||
|
||
|
||
# 有原生 alert 就点确定并返回文案。没有则返回空字符串。
|
||
def _accept_native_alert(browser: BrowserAdapter) -> str:
|
||
accept = getattr(browser, "accept_alert", None)
|
||
if not callable(accept):
|
||
return ""
|
||
try:
|
||
return str(accept() or "").strip()
|
||
except Exception:
|
||
return ""
|
||
|
||
|
||
# 当前 URL 是否已经落到合同详情 view 页。
|
||
def _submit_view_page(browser: BrowserAdapter) -> bool:
|
||
url = _current_url(browser)
|
||
return "method=view" in url and "kmAgreementApply" in url
|
||
|
||
|
||
# 页面正文是否出现提交成功标记,且没有失败文案。
|
||
def _submit_success_text(browser: BrowserAdapter) -> bool:
|
||
body = browser.read_body_text() or ""
|
||
if _submit_error_from_body(body):
|
||
return False
|
||
return any(mark in body for mark in _SUCCESS_MARKS)
|
||
|
||
|
||
# 提交是否已经成功:成功文案或已到详情页,且正文没有失败。
|
||
def _submit_succeeded(browser: BrowserAdapter) -> bool:
|
||
if _submit_error_from_body(browser.read_body_text() or ""):
|
||
return False
|
||
return _submit_success_text(browser) or _submit_view_page(browser)
|
||
|
||
|
||
# 读当前弹窗/正文里的校验失败文案。没有则返回空字符串。
|
||
def _submit_validation_error(browser: BrowserAdapter) -> str:
|
||
kind, text, _confirm = _read_submit_signal(browser, trust_body_success=True)
|
||
if kind == "error":
|
||
return text
|
||
return ""
|
||
|
||
|
||
# 扫描 LUI 弹窗:报错原样返回,确认/成功则点确定。
|
||
def _handle_submit_dialogs(browser: BrowserAdapter) -> dict:
|
||
raw = browser.execute_script(
|
||
"""
|
||
const errorMarks = ['不能为空','请填写','校验失败','提交失败','必填项未填','该文档不存在','操作失败'];
|
||
const successMarks = ['您的操作已成功','操作已成功','提交成功','流程已提交','成功提交'];
|
||
const confirmMarks = ['确定提交','是否提交','确认提交','是否确定','确定要提交'];
|
||
function classify(text) {
|
||
if (errorMarks.some((m) => text.indexOf(m) >= 0)) return 'error';
|
||
if (successMarks.some((m) => text.indexOf(m) >= 0)) return 'success';
|
||
if (confirmMarks.some((m) => text.indexOf(m) >= 0)) return 'confirm';
|
||
return 'none';
|
||
}
|
||
function dialogText(doc) {
|
||
const roots = Array.from(doc.querySelectorAll(
|
||
'.lui_dialog, .lui_message, .lui_alert, .lui_dialog_content, .prompt_frame, .lui_dialog_common'
|
||
));
|
||
return roots.map((el) => (el.innerText || '').trim()).filter(Boolean).join('\\n');
|
||
}
|
||
function clickLabel(doc, labels) {
|
||
const nodes = Array.from(doc.querySelectorAll('div, a, button, span, input'));
|
||
for (const label of labels) {
|
||
const hit = nodes.find((el) => {
|
||
const t = (el.textContent || el.value || '').trim();
|
||
return t === label && (!el.children || el.children.length === 0);
|
||
});
|
||
if (hit) { hit.click(); return label; }
|
||
}
|
||
return '';
|
||
}
|
||
function walk(win) {
|
||
let found = {kind: 'none', text: '', clicked: ''};
|
||
try {
|
||
const text = dialogText(win.document);
|
||
const kind = classify(text);
|
||
if (kind === 'error') return {kind: kind, text: text.slice(0, 200), clicked: ''};
|
||
if (kind === 'confirm') {
|
||
const clicked = clickLabel(win.document, ['确定', '确认', '是', 'OK']);
|
||
return {kind: kind, text: text.slice(0, 200), clicked: clicked};
|
||
}
|
||
if (kind === 'success') {
|
||
const clicked = clickLabel(win.document, ['确定', '关闭', '确认']);
|
||
return {kind: kind, text: text.slice(0, 200), clicked: clicked};
|
||
}
|
||
if (text) {
|
||
const clicked = clickLabel(win.document, ['确定', '确认']);
|
||
if (clicked) return {kind: 'confirm', text: text.slice(0, 200), clicked: clicked};
|
||
}
|
||
} catch (e) {}
|
||
for (let i = 0; i < win.frames.length; i++) {
|
||
try {
|
||
const inner = walk(win.frames[i]);
|
||
if (inner.kind !== 'none' || inner.clicked) return inner;
|
||
} catch (e) {}
|
||
}
|
||
return found;
|
||
}
|
||
return walk(window);
|
||
"""
|
||
)
|
||
if not isinstance(raw, dict):
|
||
return {"kind": "none", "text": "", "clicked": ""}
|
||
return {
|
||
"kind": str(raw.get("kind") or "none"),
|
||
"text": str(raw.get("text") or "").strip(),
|
||
"clicked": str(raw.get("clicked") or ""),
|
||
}
|
||
|
||
|
||
# 等到提交确认弹窗消失或被点掉。
|
||
def _wait_lui_dialog_gone(browser: BrowserAdapter, timeout_seconds: float = 8) -> None:
|
||
deadline = time.time() + timeout_seconds
|
||
while time.time() < deadline:
|
||
snapshot = _handle_submit_dialogs(browser)
|
||
kind = str(snapshot.get("kind") or "none")
|
||
text = str(snapshot.get("text") or "").strip()
|
||
if kind == "confirm":
|
||
return
|
||
if kind in ("none", "") and not text:
|
||
return
|
||
_close_blocking_dialogs(browser)
|
||
time.sleep(0.4)
|
||
_close_blocking_dialogs(browser)
|