| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- 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"] = "时效参考"
|