# 合同班次:领取表 3 → 按供应商、货主和三个日期合单 → runner 录合同 → 写表 4。 # # 崩在 process_status=3:超过租约可再领,未过期需改回 0。 # 未捕获异常原因列只写 PAGE_FAILED。原文进审计表,会话结束后另写。 from __future__ import annotations import logging from contextlib import nullcontext from dataclasses import dataclass from typing import Callable from contract_draft import ContractDraft from errors import DiligenceError, NodeError from group import draft_from_group, group_by_supplier, missing_mdm_reason from persist import KIND_DILIGENCE, KIND_FAIL, KIND_OK, ContractOutcome, build_save from rpa_oracle import ShiftError PAGE_FAILED = "PAGE_FAILED" Runner = Callable[[ContractDraft], ContractOutcome] logger = logging.getLogger("rpa_contract") # 一个合同班次的领取、合单和成败计数。 @dataclass class JobSummary: claimed: int = 0 groups: int = 0 ok: int = 0 failed: int = 0 def _session(repo): """有 session 就借一条,没有就空上下文。页面调用必须在这个块外面。""" session = getattr(repo, "session", None) if session is None: return nullcontext() return session() def run_batch(repo, *, runner: Runner, limit: int | None = None) -> JobSummary: """领取待录合同行,按供应商合单、调 runner、落表。 session 只包领取和每一组的落表。页面在连接归还之后跑。一组落完再处理下一组。 limit 交给领取层。 """ audits: list[ShiftError] = [] with _session(repo): claimed = repo.claim_pending(limit=limit) summary = JobSummary(claimed=len(claimed), groups=0) for rows in group_by_supplier(claimed): summary.groups += 1 outcome = _run_group(rows, runner) with _session(repo): repo.save_group(build_save(rows, outcome)) if outcome.error_text: supplier = rows[0].supplier_id if rows else "" audits.append(ShiftError("合同", str(supplier), PAGE_FAILED, outcome.error_text)) if outcome.kind == KIND_OK and (outcome.contract_no or "").strip(): summary.ok += 1 else: summary.failed += 1 repo.save_shift_errors(audits) return summary # 录一组同一供应商的行。缺 MDM 不调页面;未捕获异常写 PAGE_FAILED。 def _run_group(rows, runner: Runner) -> ContractOutcome: reason = missing_mdm_reason(rows) if reason: return ContractOutcome(KIND_FAIL, None, reason) try: outcome = runner(draft_from_group(rows)) except DiligenceError: return ContractOutcome(KIND_DILIGENCE, None, "客户未尽调") except NodeError as exc: return ContractOutcome(KIND_FAIL, None, str(exc)) except Exception as exc: logger.exception("合同录入未捕获异常 supplier=%s", rows[0].supplier_id if rows else "") return ContractOutcome(KIND_FAIL, None, PAGE_FAILED, _text(exc)) if outcome.kind == KIND_OK and not (outcome.contract_no or "").strip(): return ContractOutcome(KIND_FAIL, None, "submit 模式未读取到真实合同编号") return outcome # 把页面 run() 的合同号或异常收成 ContractOutcome。 # # 尽调失败写客户未尽调;NodeError 写业务文案;其它异常写 PAGE_FAILED,原文放 error_text。 def outcome_from_run(run: Callable[[], str]) -> ContractOutcome: try: number = (run() or "").strip() except DiligenceError: return ContractOutcome(KIND_DILIGENCE, None, "客户未尽调") except NodeError as exc: return ContractOutcome(KIND_FAIL, None, str(exc)) except Exception as exc: logger.exception("合同页面未捕获异常") return ContractOutcome(KIND_FAIL, None, PAGE_FAILED, _text(exc)) if not number: return ContractOutcome(KIND_FAIL, None, "submit 模式未读取到真实合同编号") return ContractOutcome(KIND_OK, number, None) # 取出异常原文。空串不当审计正文。 def _text(exc: BaseException) -> str | None: text = str(exc).strip() return text or None