from __future__ import annotations from dataclasses import dataclass from app.market.models import CandidateRecord, InquiryRequest DECISION_WEIGHTS = { "solution_assurance": 0.27, "relationship_advantage": 0.23, "spot_space": 0.15, "supplier_execution": 0.18, "customer_fit": 0.10, "transit": 0.04, "price": 0.03, } SPOT_SPACE_STATUS_SCORES = { "可立即订舱": 100.0, "现舱有限": 70.0, "需确认": 35.0, "无现舱": 0.0, } @dataclass(frozen=True) class CustomerSignals: price_sensitivity: float time_sensitivity: float reliability_sensitivity: float repeat_purchase_count: int overdue_days: int service_requirement: str def score_candidates( candidates: list[CandidateRecord], inquiry: InquiryRequest, customer: CustomerSignals, ) -> tuple[list[dict], dict[str, float]]: if not candidates: return [], {} costs = [candidate.supplier_cost for candidate in candidates] min_cost, max_cost = min(costs), max(costs) min_transit = min(candidate.transit_days for candidate in candidates) scored: list[dict] = [] for candidate in candidates: if max_cost == min_cost: price_score = 100.0 else: price_score = 100 - ((candidate.supplier_cost - min_cost) / (max_cost - min_cost) * 28) transit_penalty = 12 if candidate.transport_mode == "空运" else 5 transit_score = max( 40.0, 100 - (candidate.transit_days - min_transit) * transit_penalty, ) offering_reliability = ( candidate.booking_success_rate + candidate.space_release_success_rate ) / 2 product_reliability = ( candidate.etd_on_time_rate + candidate.eta_on_time_rate ) / 2 supplier_score = (offering_reliability * 0.7 + product_reliability * 0.3) * 100 route_assurance = 100.0 if candidate.route_type in {"直航", "直达"} else 72.0 solution_score = ( candidate.solution_assurance * 100 * 0.55 + route_assurance * 0.25 + candidate.service_support_score * 100 * 0.20 ) relationship_score = ( candidate.relationship_strength * 0.55 + candidate.low_price_access_probability * 0.25 + candidate.priority_space_probability * 0.20 ) * 100 spot_space_score = ( SPOT_SPACE_STATUS_SCORES.get(candidate.spot_space_status, 35.0) * 0.85 + min(candidate.spot_space_teu / 16, 1.0) * 100 * 0.15 ) customer_score = 55.0 if candidate.route_type == inquiry.preferred_route_type: customer_score += 15 customer_score += candidate.service_support_score * customer.reliability_sensitivity * 15 customer_score += (transit_score / 100) * customer.time_sensitivity * 10 customer_score += (price_score / 100) * customer.price_sensitivity * 5 customer_score = min(100.0, customer_score) total_score = ( solution_score * DECISION_WEIGHTS["solution_assurance"] + relationship_score * DECISION_WEIGHTS["relationship_advantage"] + spot_space_score * DECISION_WEIGHTS["spot_space"] + supplier_score * DECISION_WEIGHTS["supplier_execution"] + customer_score * DECISION_WEIGHTS["customer_fit"] + transit_score * DECISION_WEIGHTS["transit"] + price_score * DECISION_WEIGHTS["price"] ) scored.append({ "candidate": candidate, "price_score": round(price_score, 1), "transit_score": round(transit_score, 1), "supplier_execution_score": round(supplier_score, 1), "customer_fit_score": round(customer_score, 1), "solution_assurance_score": round(solution_score, 1), "relationship_advantage_score": round(relationship_score, 1), "spot_space_score": round(spot_space_score, 1), "total_score": round(total_score, 1), }) assign_solution_types(scored) scored.sort(key=lambda item: item["total_score"], reverse=True) for rank, item in enumerate(scored, start=1): item["rank"] = rank return scored, DECISION_WEIGHTS.copy() def assign_solution_types(scored: list[dict]) -> None: for item in scored: item["solution_type"] = "可执行方案" primary = max(scored, key=lambda item: item["total_score"]) primary["solution_type"] = "方案优选" remaining = [item for item in scored if item is not primary] if remaining: relationship = max(remaining, key=lambda item: item["relationship_advantage_score"]) relationship["solution_type"] = "关系优选" remaining = [item for item in remaining if item is not relationship] if remaining: price = min(remaining, key=lambda item: item["candidate"].supplier_cost) price["solution_type"] = "价格参考" remaining = [item for item in remaining if item is not price] if remaining: fastest = min(remaining, key=lambda item: item["candidate"].transit_days) fastest["solution_type"] = "时效参考"