229 lines
7.2 KiB
Python
229 lines
7.2 KiB
Python
"""返利中心查号:打开、登录、进采购协议、按合同号搜。不录协议。"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Protocol
|
|
|
|
from accept import AcceptanceReport, evidence_shot
|
|
from errors import LoginFailed
|
|
|
|
FLOW = "REBATE_FIND"
|
|
DEFAULT_URL = "http://192.168.170.144:8001"
|
|
SENTINEL_CONTRACT_NO = "__NO_SUCH_CONTRACT__"
|
|
|
|
_ACCEPT = AcceptanceReport(flow=FLOW)
|
|
|
|
_PORTAL_MARKERS = ("采购协议", "待审批", "待审批事项", "待办")
|
|
EMPTY_MARKERS = ("暂无数据", "没有找到", "查询无结果", "无匹配数据", "未找到相关")
|
|
|
|
LOGIN = {
|
|
"user": [
|
|
'input[name="username"]',
|
|
'input[name="userName"]',
|
|
'input[placeholder*="用户名"]',
|
|
'input[placeholder*="账号"]',
|
|
'input[placeholder*="工号"]',
|
|
'input[type="text"]',
|
|
],
|
|
"password": [
|
|
'input[name="password"]',
|
|
'input[placeholder*="密码"]',
|
|
'input[type="password"]',
|
|
],
|
|
"submit": [
|
|
"text=登录",
|
|
"button:contains(登录)",
|
|
'button[type="submit"]',
|
|
'input[type="submit"]',
|
|
'input[value="登录"]',
|
|
],
|
|
}
|
|
|
|
MENU = {
|
|
"entry": [
|
|
"text=采购协议管理",
|
|
"text=采购协议",
|
|
"a:contains(采购协议管理)",
|
|
"span:contains(采购协议管理)",
|
|
],
|
|
}
|
|
|
|
SEARCH = {
|
|
"contract_no": [
|
|
"label=合同编号",
|
|
"label=合同号",
|
|
'input[placeholder*="合同编号"]',
|
|
'input[placeholder*="合同号"]',
|
|
'input[name*="contractNo"]',
|
|
'input[name*="contract"]',
|
|
],
|
|
"button": [
|
|
"text=查询",
|
|
"button:contains(查询)",
|
|
"a:contains(查询)",
|
|
"text=搜索",
|
|
"button:contains(搜索)",
|
|
],
|
|
}
|
|
|
|
_TABLE_SCRIPT = """
|
|
const nodes = Array.from(document.querySelectorAll(
|
|
'table tbody, table, .el-table__body, .el-table, .ant-table-tbody, .ant-table'
|
|
));
|
|
return nodes.map((n) => (n.innerText || '').trim()).filter(Boolean).join('\\n');
|
|
"""
|
|
|
|
|
|
class BrowserAdapter(Protocol):
|
|
def open_url(self, url: str) -> None: ...
|
|
def fill_first(self, selectors: list[str], value: str) -> None: ...
|
|
def click_first(self, selectors: list[str]) -> None: ...
|
|
def wait_for_text(self, text: str, timeout_seconds: int = 15) -> None: ...
|
|
def wait_for_element(self, selectors: list[str], timeout_seconds: int = 15) -> None: ...
|
|
def element_exists(self, selectors: list[str]) -> bool: ...
|
|
def read_body_text(self) -> str: ...
|
|
def execute_script(self, script: str, *args): ...
|
|
def screenshot(self, name: str) -> str: ...
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RebateFlowContext:
|
|
rebate_url: str
|
|
username: str
|
|
password: str
|
|
|
|
|
|
def classify_search_result(result_text: str, contract_no: str, *, from_table: bool = True) -> bool:
|
|
number = (contract_no or "").strip()
|
|
if not number:
|
|
return False
|
|
text = result_text or ""
|
|
if from_table:
|
|
if any(marker in text for marker in EMPTY_MARKERS) and number not in text:
|
|
return False
|
|
return number in text
|
|
if any(marker in text for marker in EMPTY_MARKERS):
|
|
return False
|
|
return text.count(number) >= 2
|
|
|
|
|
|
def read_result_table(browser: BrowserAdapter) -> tuple[str, bool]:
|
|
try:
|
|
text = browser.execute_script(_TABLE_SCRIPT)
|
|
except Exception:
|
|
text = None
|
|
if isinstance(text, str) and text.strip():
|
|
return text, True
|
|
return (browser.read_body_text() or ""), False
|
|
|
|
|
|
def open_rebate_center(browser: BrowserAdapter, ctx: RebateFlowContext) -> None:
|
|
browser.open_url(ctx.rebate_url)
|
|
|
|
|
|
def _is_logged_in(browser: BrowserAdapter) -> bool:
|
|
body = browser.read_body_text() or ""
|
|
return any(marker in body for marker in _PORTAL_MARKERS)
|
|
|
|
|
|
def login_rebate_center(browser: BrowserAdapter, ctx: RebateFlowContext) -> None:
|
|
try:
|
|
if _is_logged_in(browser):
|
|
return
|
|
if not (ctx.username or "").strip() or ctx.password == "":
|
|
raise LoginFailed("未配置 REBATE_USERNAME / REBATE_PASSWORD")
|
|
browser.fill_first(LOGIN["user"], ctx.username)
|
|
browser.fill_first(LOGIN["password"], ctx.password)
|
|
browser.click_first(LOGIN["submit"])
|
|
deadline = time.time() + 20
|
|
while time.time() < deadline:
|
|
if _is_logged_in(browser):
|
|
return
|
|
time.sleep(0.5)
|
|
raise TimeoutError("登录后未进入返利中心")
|
|
except LoginFailed:
|
|
raise
|
|
except Exception as exc:
|
|
raise LoginFailed(f"返利中心登录失败:{exc}") from exc
|
|
|
|
|
|
def enter_purchase_agreement(browser: BrowserAdapter) -> None:
|
|
body = browser.read_body_text() or ""
|
|
if "合同编号" in body or "合同号" in body:
|
|
return
|
|
browser.click_first(MENU["entry"])
|
|
browser.wait_for_text("查询", timeout_seconds=15)
|
|
|
|
|
|
def search_contract(browser: BrowserAdapter, contract_no: str) -> bool:
|
|
number = (contract_no or "").strip()
|
|
if not number:
|
|
return False
|
|
browser.fill_first(SEARCH["contract_no"], number)
|
|
browser.click_first(SEARCH["button"])
|
|
deadline = time.time() + 15
|
|
last_text = ""
|
|
last_from_table = False
|
|
while time.time() <= deadline:
|
|
text, from_table = read_result_table(browser)
|
|
last_text, last_from_table = text, from_table
|
|
found = classify_search_result(text, number, from_table=from_table)
|
|
empty = any(marker in (text or "") for marker in EMPTY_MARKERS)
|
|
if found or empty:
|
|
return found
|
|
time.sleep(0.4)
|
|
return classify_search_result(last_text, number, from_table=last_from_table)
|
|
|
|
|
|
def run_find_flow(
|
|
browser: BrowserAdapter,
|
|
ctx: RebateFlowContext,
|
|
contract_no: str,
|
|
*,
|
|
expect_found: bool | None = None,
|
|
) -> bool:
|
|
_ACCEPT.reset()
|
|
try:
|
|
open_rebate_center(browser, ctx)
|
|
_ACCEPT.note("S02", "打开返利中心", {"url": ctx.rebate_url}, ok=True)
|
|
try:
|
|
login_rebate_center(browser, ctx)
|
|
_ACCEPT.note("S03", "登录", {"user": ctx.username}, ok=True)
|
|
except Exception as exc:
|
|
evidence_shot(browser, "rebate-login-failed", failed=True)
|
|
_ACCEPT.note(
|
|
"S03",
|
|
"登录",
|
|
{"user": ctx.username, "error": exc},
|
|
ok=False,
|
|
raise_error=False,
|
|
)
|
|
raise
|
|
enter_purchase_agreement(browser)
|
|
_ACCEPT.note("S04", "进入采购协议", {"menu": "采购协议管理"}, ok=True)
|
|
found = search_contract(browser, contract_no)
|
|
found_text = "yes" if found else "no"
|
|
if expect_found is None:
|
|
_ACCEPT.note(
|
|
"S05",
|
|
"查号",
|
|
{"contractNo": contract_no, "found": found_text},
|
|
ok=True,
|
|
)
|
|
else:
|
|
_ACCEPT.check(
|
|
"S05",
|
|
"查号",
|
|
[
|
|
("contractNo", contract_no, contract_no),
|
|
("found", found_text, "yes" if expect_found else "no"),
|
|
],
|
|
browser=browser,
|
|
screenshot_on_fail="rebate-find-mismatch",
|
|
)
|
|
return found
|
|
finally:
|
|
print(_ACCEPT.summary_text(), end="", flush=True)
|
|
_ACCEPT.dump()
|