"""选品班次:领取表 1 → 按条形码搜平台 → 统一拆量写表 2。""" from __future__ import annotations 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 MappingRow from rules import Candidate, PurchaseLine, validate_purchase Searcher = Callable[[str], list[Candidate]] @dataclass class JobSummary: claimed: int = 0 added: int = 0 skipped: int = 0 failed: int = 0 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: rows = repo.claim_pending(limit=limit) summary = JobSummary(claimed=len(rows)) for row in rows: purchase = mapping_to_purchase(row) selection = _select_row(purchase, platforms, searchers, login_errors) result = build_result(purchase, selection) _count(summary, result.cart_rows) repo.save_result( result.oms_id, result.deal_flag, result.deal_result, result.cart_rows ) return summary def _select_row( purchase: PurchaseLine, platforms: list[str], searchers: dict[str, Searcher], login_errors: dict[str, str], ) -> SelectionResult: row_reason = validate_purchase(purchase) if row_reason: return SelectionResult( kind=KIND_BUSINESS_FAIL, reason=row_reason, allocations=[], rejected=[], malls=list(platforms), ) if not purchase.bar_code: return SelectionResult( kind=KIND_BUSINESS_FAIL, reason="NO_BAR_CODE", allocations=[], rejected=[], malls=list(platforms), ) outcomes = [ _search_one(purchase, mall, searchers, login_errors) for mall in platforms ] return run_selection(purchase, outcomes) def _search_one( purchase: PurchaseLine, mall: str, searchers: dict[str, Searcher], login_errors: dict[str, str], ) -> MallOutcome: login_error = login_errors.get(mall) if login_error: return MallOutcome(mall, failed_reason=f"LOGIN_FAILED:{login_error}") searcher = searchers.get(mall) if searcher is None: return MallOutcome(mall, failed_reason="LOGIN_FAILED:未登录") try: candidates = searcher(purchase.bar_code) except Exception as exc: return MallOutcome(mall, failed_reason=f"QUERY_FAILED:{exc}") return MallOutcome(mall, candidates=candidates) 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