# 单行两平台统一判定:合并候选后按仓序拆量。 # # 凑齐计划量才成功。一边故障且剩下的凑不齐是系统故障,两边都搜到但不够是库存不足。 # 不在这里搜平台、不写表。 from __future__ import annotations from dataclasses import dataclass, field from datetime import date from rules import ( REASON_EXPIRY, REASON_EXPIRY_UNPARSEABLE, REASON_MARGIN, REASON_NOT_SELF, Allocation, Candidate, PurchaseLine, Rejected, allocate, validate_purchase, ) KIND_SUCCESS = "SUCCESS" KIND_BUSINESS_FAIL = "BUSINESS_FAIL" KIND_SYSTEM_FAIL = "SYSTEM_FAIL" # 一个平台的搜索结果:候选列表,或失败短码。 @dataclass class MallOutcome: mall_name: str candidates: list[Candidate] | None = None failed_reason: str | None = None # 一行选品的判定:成功拆量、业务失败或系统故障。 @dataclass class SelectionResult: kind: str reason: str allocations: list[Allocation] rejected: list[Rejected] malls: list[str] mall_errors: dict[str, str] = field(default_factory=dict) # 把各平台结果合并后拆量。mall_errors 非空且凑不齐时标系统故障。 def run_selection( purchase: PurchaseLine, mall_outcomes: list[MallOutcome], today: date | None = None, ) -> SelectionResult: malls = [item.mall_name for item in mall_outcomes] mall_errors = { item.mall_name: item.failed_reason for item in mall_outcomes if item.failed_reason } row_reason = validate_purchase(purchase) if row_reason: return SelectionResult( kind=KIND_BUSINESS_FAIL, reason=row_reason, allocations=[], rejected=[], malls=malls, mall_errors=mall_errors, ) merged: list[Candidate] = [] had_hit = False for item in mall_outcomes: if item.failed_reason: continue rows = item.candidates or [] if rows: had_hit = True merged.extend(rows) allocation = allocate(merged, purchase, today=today) if allocation.allocations: return SelectionResult( kind=KIND_SUCCESS, reason="SELECTED", allocations=allocation.allocations, rejected=allocation.rejected, malls=malls, mall_errors=mall_errors, ) if mall_errors: return SelectionResult( kind=KIND_SYSTEM_FAIL, reason="SYSTEM_FAULT", allocations=[], rejected=allocation.rejected, malls=malls, mall_errors=mall_errors, ) return SelectionResult( kind=KIND_BUSINESS_FAIL, reason=_business_reason(allocation.passed_before_fold, allocation.rejected, had_hit), allocations=[], rejected=allocation.rejected, malls=malls, mall_errors={}, ) # 没有拆出量时,把同时命中的库存、毛利、效期、非自营都收进原因。 def _business_reason( passed_before_fold: list[Candidate], rejected: list[Rejected], had_hit: bool, ) -> str: reasons = {item.reason for item in rejected} codes: list[str] = [] if passed_before_fold: codes.append("STOCK_INSUFFICIENT") if REASON_MARGIN in reasons: codes.append(REASON_MARGIN) if REASON_EXPIRY in reasons or REASON_EXPIRY_UNPARSEABLE in reasons: codes.append(REASON_EXPIRY) if REASON_NOT_SELF in reasons: codes.append(REASON_NOT_SELF) if codes: return ";".join(codes) if not had_hit: return "NO_HIT" return "NO_CANDIDATE_AFTER_RULES"