AutoRpa/rpa_selection/job.py

184 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 选品班次:领取表 1 → 按条形码搜平台 → 统一拆量写表 2。
#
# 崩在 deal_flag=3:超过租约可再领,未过期需把 3 改回 0。
# 查询失败原因列只写 QUERY_FAILED,原文进 rpa_selection_query_audit。
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Callable
from persist import STATUS_OK, build_result
from pipeline import KIND_BUSINESS_FAIL, MallOutcome, SelectionResult, run_selection
from repo import FinishWrite, MappingRow, QueryAudit
from rules import Candidate, PurchaseLine, validate_purchase
Searcher = Callable[[str], list[Candidate]]
logger = logging.getLogger("rpa_selection")
# 一个班次的领取和落表计数。
@dataclass
class JobSummary:
claimed: int = 0
added: int = 0
skipped: int = 0
failed: int = 0
# 把表 1 行收成规则层的采购行。缺价格或数量保持 None。
def mapping_to_purchase(row: MappingRow) -> PurchaseLine:
sale_price = float(row.sale_price) if row.sale_price is not None else None
qty = float(row.plan_purchase_qty) if row.plan_purchase_qty is not None else None
return PurchaseLine(
approval_no=row.approval_no or "",
sale_price=sale_price,
qty=qty,
row_no=row.fid,
generic_name=row.goods_name or "",
spec=row.goods_spec or "",
manufacturer=row.manufacturer or "",
oms_id=row.oms_id,
bar_code=(row.bar_code or "").strip(),
org_code=row.org_code or "",
)
def run_batch(
repo,
*,
platforms: list[str],
searchers: dict[str, Searcher],
login_errors: dict[str, str],
limit: int | None = None,
) -> JobSummary:
"""领取待选品行,按行搜平台、拆量,一行搜完就落这一行。
每个平台开搜前刷新 deal_time。落库前由仓库再确认仍是 deal_flag=3,
且表 2 还没有该 oms_id。limit 交给领取层。查询失败原文在该行落表后另写。
"""
rows = repo.claim_pending(limit=limit)
summary = JobSummary(claimed=len(rows))
for row in rows:
purchase = mapping_to_purchase(row)
def renew(_fid: int = row.fid) -> None:
"""这一行还在搜时,把租约续到现在。"""
touch = getattr(repo, "touch_claim", None)
if touch is not None:
touch(_fid)
selection, row_audits = _select_row(
purchase,
platforms,
searchers,
login_errors,
before_search=renew,
)
result = build_result(purchase, selection)
_count(summary, result.cart_rows)
repo.save_results(
[
FinishWrite(
oms_id=result.oms_id,
fid=row.fid,
deal_flag=result.deal_flag,
deal_result=result.deal_result,
cart_rows=result.cart_rows,
)
]
)
if row_audits:
repo.save_query_audits(row_audits)
return summary
def _select_row(
purchase: PurchaseLine,
platforms: list[str],
searchers: dict[str, Searcher],
login_errors: dict[str, str],
before_search: Callable[[], None] | None = None,
) -> tuple[SelectionResult, list[QueryAudit]]:
"""校验采购行并搜各平台,交给统一拆量。校验不过不搜。
before_search 在每个平台开搜前调用,用来续租。
"""
row_reason = validate_purchase(purchase)
if row_reason:
return (
SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason=row_reason,
allocations=[],
rejected=[],
malls=list(platforms),
),
[],
)
outcomes: list[MallOutcome] = []
audits: list[QueryAudit] = []
for mall in platforms:
if before_search is not None:
before_search()
outcome, audit = _search_one(purchase, mall, searchers, login_errors)
outcomes.append(outcome)
if audit is not None:
audits.append(audit)
return run_selection(purchase, outcomes), audits
# 搜一个平台。登录失败写 LOGIN_FAILED,查询抛错写 QUERY_FAILED。
#
# 异常原文进审计行,不写进失败原因列。
def _search_one(
purchase: PurchaseLine,
mall: str,
searchers: dict[str, Searcher],
login_errors: dict[str, str],
) -> tuple[MallOutcome, QueryAudit | None]:
login_error = login_errors.get(mall)
if login_error:
logger.warning("选品登录失败 mall=%s: %s", mall, login_error)
return MallOutcome(mall, failed_reason="LOGIN_FAILED"), None
searcher = searchers.get(mall)
if searcher is None:
return MallOutcome(mall, failed_reason="LOGIN_FAILED"), None
try:
candidates = searcher(purchase.bar_code)
except Exception as exc:
logger.exception("选品查询失败 mall=%s bar_code=%s", mall, purchase.bar_code)
return MallOutcome(mall, failed_reason="QUERY_FAILED"), _query_audit(
purchase, mall, exc
)
return MallOutcome(mall, candidates=candidates), None
# 有 oms_id 和表 1 fid 时收成一条审计。缺主键则只留日志。
def _query_audit(
purchase: PurchaseLine, mall: str, exc: Exception
) -> QueryAudit | None:
if purchase.oms_id is None or purchase.row_no is None:
return None
return QueryAudit(
oms_id=purchase.oms_id,
mapping_fid=purchase.row_no,
mall_name=mall,
bar_code=purchase.bar_code,
error_text=str(exc),
)
# 按表 2 行状态累加成功、系统故障、业务跳过。
def _count(summary: JobSummary, cart_rows: list[dict]) -> None:
for row in cart_rows:
if row.get("status") == STATUS_OK:
summary.added += 1
continue
reason = str(row.get("fail_reason") or "")
if reason.startswith("系统故障"):
summary.failed += 1
else:
summary.skipped += 1