| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- from __future__ import annotations
- from datetime import date, datetime, time, timedelta, timezone
- from pathlib import Path
- from uuid import uuid4
- from pydantic import BaseModel, Field
- from rdflib import Literal, Namespace, RDF, RDFS, XSD
- from app.market import MarketService
- from app.repository import RDFRepository
- GESLI = Namespace("https://gesli.example/ontology#")
- MOCK = Namespace("https://gesli.example/runtime-mock/")
- class MockBatchRequest(BaseModel):
- origin: str
- destination: str
- supplier_count: int = Field(default=4, ge=1, le=8)
- customer_count: int = Field(default=2, ge=0, le=5)
- evaluation_date: date = date(2026, 7, 20)
- class LocalMockDataService:
- def __init__(
- self,
- root: Path,
- repository: RDFRepository,
- market_service: MarketService,
- ):
- self.root = root
- self.repository = repository
- self.market_service = market_service
- def catalog(self) -> dict:
- candidate_rows = self.repository.query_file(
- self.root / "queries" / "market-candidates.rq"
- )["rows"]
- context_rows = self.repository.query_file(
- self.root / "queries" / "market-context.rq"
- )["rows"]
- customers = self.market_service.customers()
- evaluation_date = date(2026, 7, 20)
- routes: dict[tuple[str, str], dict] = {}
- for row in candidate_rows:
- origin = row.get("originLocation", "")
- destination = row.get("destinationLocation", "")
- if not origin or not destination:
- continue
- key = (origin, destination)
- route = routes.setdefault(key, {
- "origin": origin,
- "destination": destination,
- "products": set(),
- "suppliers": set(),
- "quotes": set(),
- "current_quotes": 0,
- "transport_modes": set(),
- })
- route["products"].add(row.get("product", ""))
- route["suppliers"].add(row.get("supplier", ""))
- route["quotes"].add(row.get("quote", ""))
- if row.get("transportMode"):
- route["transport_modes"].add(row["transportMode"])
- if _within(evaluation_date, row.get("validFrom"), row.get("validTo")):
- route["current_quotes"] += 1
- route_rows = [
- {
- "origin": route["origin"],
- "destination": route["destination"],
- "transport_modes": " / ".join(sorted(route["transport_modes"])),
- "product_count": len(route["products"]),
- "supplier_count": len(route["suppliers"]),
- "quote_count": len(route["quotes"]),
- "current_quote_count": route["current_quotes"],
- }
- for route in routes.values()
- ]
- route_rows.sort(key=lambda item: (item["origin"], item["destination"]))
- quote_rows = []
- for row in candidate_rows:
- valid_from = row.get("validFrom", "")
- valid_to = row.get("validTo", "")
- if _within(evaluation_date, valid_from, valid_to):
- status = "当前有效"
- elif valid_to and valid_to < evaluation_date.isoformat():
- status = "已过期"
- else:
- status = "尚未生效"
- quote_rows.append({
- "quote_no": row.get("quoteNo", ""),
- "route": f"{row.get('originLocation', '')} → {row.get('destinationLocation', '')}",
- "transport_mode": row.get("transportMode", ""),
- "product_name": row.get("productName", ""),
- "supplier_name": row.get("supplierName", ""),
- "supplier_cost_amount": _as_float(row.get("supplierCostAmount")),
- "currency": row.get("currency", ""),
- "valid_to": valid_to,
- "status": status,
- "dangerous_goods": row.get("supportsDangerousGoods", "false") == "true",
- "relationship_tier": row.get("relationshipTier") or "基础合作",
- "relationship_strength": _as_float(row.get("relationshipStrength"), 0.60),
- "low_price_access_probability": _as_float(
- row.get("lowPriceAccessProbability"), 0.50
- ),
- "priority_space_probability": _as_float(
- row.get("prioritySpaceProbability"), 0.60
- ),
- "relationship_evidence": (
- row.get("relationshipEvidence") or "按标准合作机制执行"
- ),
- "spot_space_status": row.get("spotSpaceStatus") or "需确认",
- "spot_space_teu": _as_float(row.get("spotSpaceTeu")),
- "runtime_mock": row.get("quote", "").startswith(str(MOCK)),
- })
- quote_rows.sort(
- key=lambda item: (
- item["route"], item["status"], item["supplier_cost_amount"]
- )
- )
- strategies = [
- {
- "name": row.get("strategyName", ""),
- "objective": row.get("strategyObjective", ""),
- "route": f"{row.get('origin', '')} → {row.get('destination', '')}",
- "adjustment_rate": _as_float(row.get("strategyAdjustmentRate")),
- "valid_to": row.get("validTo", ""),
- }
- for row in context_rows
- if row.get("kind") == "strategy"
- ]
- return {
- "summary": {
- "triple_count": self.repository.triple_count,
- "in_memory_triple_count": self.repository.in_memory_triple_count,
- "customer_count": len(customers),
- "supplier_count": len({row.get("supplier") for row in candidate_rows}),
- "product_count": len({row.get("product") for row in candidate_rows}),
- "quote_count": len({row.get("quote") for row in candidate_rows}),
- "route_count": len(route_rows),
- "strategy_count": len(strategies),
- },
- "routes": route_rows,
- "customers": customers,
- "quotes": quote_rows,
- "strategies": strategies,
- "loaded_files": self.repository.loaded_file_names,
- }
- def generate(self, request: MockBatchRequest) -> dict:
- route_context = next(
- (
- route
- for route in self.market_service.form_options()["routes"]
- if route["origin"] == request.origin
- and route["destination"] == request.destination
- ),
- None,
- )
- if not route_context:
- raise ValueError("只能为已经配置市场基准和策略的路线生成Mock数据")
- token = uuid4().hex[:8]
- benchmark = float(route_context["target_amount"])
- transport_mode = self._route_transport_mode(
- request.origin, request.destination
- )
- triples = []
- generated_suppliers = []
- generated_customers = []
- for index in range(request.supplier_count):
- suffix = f"{token}-{index + 1}"
- brand = MOCK[f"brand-{suffix}"]
- supplier = MOCK[f"supplier-{suffix}"]
- product = MOCK[f"product-{suffix}"]
- offering = MOCK[f"offering-{suffix}"]
- schedule = MOCK[f"schedule-{suffix}"]
- quote = MOCK[f"quote-{suffix}"]
- performance = MOCK[f"performance-{suffix}"]
- profile = self._supplier_profile(index)
- supplier_name = (
- f"印度航线Mock{profile['supplier_label']} {token[-3:].upper()}{index + 1}"
- )
- product_name = (
- f"{request.origin}-{request.destination}{profile['product_label']}方案{index + 1}"
- )
- departure = request.evaluation_date + timedelta(days=8 + index)
- transit_days = self._transit_days(transport_mode, index)
- arrival = departure + timedelta(days=transit_days)
- cost = round(benchmark * profile["cost_ratio"], -2)
- success_rate = profile["booking_success_rate"]
- product_class = (
- GESLI.AirFreightProduct
- if transport_mode == "空运"
- else GESLI.ContainerOceanProduct
- )
- route_type = "直达" if transport_mode == "空运" and index < 2 else profile["route_type"]
- triples.extend([
- (brand, RDF.type, GESLI.ServiceBrand),
- (brand, GESLI.brandCode, Literal(f"MOCK-{token}-{index + 1}")),
- (brand, GESLI.brandName, Literal(f"Mock品牌 {index + 1}")),
- (supplier, RDF.type, profile["supplier_class"]),
- (supplier, RDFS.label, Literal(supplier_name, lang="zh")),
- (supplier, GESLI.platformSupplierType, Literal(profile["supplier_type"])),
- (supplier, GESLI.isActivePartner, Literal(True)),
- (product, RDF.type, product_class),
- (product, GESLI.productCode, Literal(f"MOCK-PRODUCT-{suffix}")),
- (product, GESLI.productName, Literal(product_name)),
- (product, GESLI.hasServiceBrand, brand),
- (product, GESLI.hasProductPerformanceProfile, performance),
- (product, GESLI.transportMode, Literal(transport_mode)),
- (product, GESLI.serviceForm, Literal(profile["service_form"])),
- (product, GESLI.routeType, Literal(route_type)),
- (product, GESLI.originLocation, Literal(request.origin)),
- (product, GESLI.destinationLocation, Literal(request.destination)),
- (product, GESLI.estimatedTransitDays, Literal(transit_days, datatype=XSD.decimal)),
- (performance, RDF.type, GESLI.ProductPerformanceProfile),
- (performance, GESLI.etdAtdOnTimeRate, Literal(success_rate, datatype=XSD.decimal)),
- (performance, GESLI.etaAtaOnTimeRate, Literal(success_rate - 0.02, datatype=XSD.decimal)),
- (offering, RDF.type, GESLI.SupplierProductOffering),
- (offering, GESLI.offeringCode, Literal(f"MOCK-OFFER-{suffix}")),
- (offering, GESLI.offeredBySupplier, supplier),
- (offering, GESLI.offeringProduct, product),
- (offering, GESLI.offeringChannel, Literal(profile["offering_channel"])),
- (offering, GESLI.agencyLevel, Literal(profile["agency_level"])),
- (offering, GESLI.capacityCommitment, Literal(profile["capacity_commitment"])),
- (offering, GESLI.capacityAvailable, Literal(True)),
- (offering, GESLI.spotSpaceStatus, Literal(profile["spot_space_status"])),
- (offering, GESLI.spotSpaceTeu, Literal(profile["spot_space_teu"], datatype=XSD.decimal)),
- (offering, GESLI.supportsDangerousGoods, Literal(index == 0)),
- (offering, GESLI.solutionAssuranceScore, Literal(profile["solution_assurance"], datatype=XSD.decimal)),
- (offering, GESLI.serviceSupportScore, Literal(profile["service_support_score"], datatype=XSD.decimal)),
- (offering, GESLI.relationshipTier, Literal(profile["relationship_tier"])),
- (offering, GESLI.relationshipStrength, Literal(profile["relationship_strength"], datatype=XSD.decimal)),
- (offering, GESLI.lowPriceAccessProbability, Literal(profile["low_price_access_probability"], datatype=XSD.decimal)),
- (offering, GESLI.prioritySpaceProbability, Literal(profile["priority_space_probability"], datatype=XSD.decimal)),
- (offering, GESLI.relationshipEvidence, Literal(profile["relationship_evidence"])),
- (offering, GESLI.bookingSuccessRate, Literal(success_rate, datatype=XSD.decimal)),
- (offering, GESLI.spaceReleaseSuccessRate, Literal(profile["space_release_success_rate"], datatype=XSD.decimal)),
- (schedule, RDF.type, GESLI.ScheduledServiceInstance),
- (schedule, GESLI.scheduleCode, Literal(f"MOCK-SCHEDULE-{suffix}")),
- (schedule, GESLI.scheduleOfProduct, product),
- (schedule, GESLI.documentCutoffTime, _date_time_literal(departure - timedelta(days=2))),
- (schedule, GESLI.estimatedDepartureTime, _date_time_literal(departure)),
- (schedule, GESLI.estimatedArrivalTime, _date_time_literal(arrival)),
- (schedule, GESLI.estimatedTransitDays, Literal(transit_days, datatype=XSD.decimal)),
- (quote, RDF.type, GESLI.SupplierQuote),
- (quote, GESLI.quoteNo, Literal(f"MOCK-QUOTE-{suffix}")),
- (quote, GESLI.quotedBySupplier, supplier),
- (quote, GESLI.quotedForOffering, offering),
- (quote, GESLI.quoteAppliesToSchedule, schedule),
- (quote, GESLI.supplierCostAmount, Literal(cost, datatype=XSD.decimal)),
- (quote, GESLI.currency, Literal("CNY")),
- (quote, GESLI.validFrom, Literal(request.evaluation_date, datatype=XSD.date)),
- (quote, GESLI.validTo, Literal(request.evaluation_date + timedelta(days=30), datatype=XSD.date)),
- ])
- generated_suppliers.append(supplier_name)
- for index in range(request.customer_count):
- suffix = f"{token}-{index + 1}"
- customer = MOCK[f"customer-{suffix}"]
- trade = MOCK[f"customer-trade-{suffix}"]
- credit = MOCK[f"customer-credit-{suffix}"]
- customer_name = f"印度目的港模拟客户 {token[-3:].upper()}{index + 1}"
- triples.extend([
- (customer, RDF.type, GESLI.Customer),
- (customer, RDFS.label, Literal(customer_name, lang="zh")),
- (customer, GESLI.hasTradeProfile, trade),
- (customer, GESLI.hasCreditProfile, credit),
- (trade, RDF.type, GESLI.CustomerTradeProfile),
- (trade, GESLI.industry, Literal("印度市场Mock行业")),
- (trade, GESLI.routeName, Literal(f"{request.origin}-{request.destination}")),
- (trade, GESLI.repeatPurchaseCount, Literal(4 + index * 6)),
- (trade, GESLI.quoteWinRate, Literal(0.5 + index * 0.08, datatype=XSD.decimal)),
- (trade, GESLI.customerPriceSensitivity, Literal(0.45 + index * 0.15, datatype=XSD.decimal)),
- (trade, GESLI.customerTimeSensitivity, Literal(0.75 - index * 0.1, datatype=XSD.decimal)),
- (trade, GESLI.customerReliabilitySensitivity, Literal(0.7 + index * 0.1, datatype=XSD.decimal)),
- (credit, RDF.type, GESLI.CustomerCreditProfile),
- (credit, GESLI.overdueDays, Literal(index)),
- ])
- generated_customers.append(customer_name)
- added_triples = self.repository.add_triples(triples)
- self.market_service.reset_runtime()
- return {
- "batch_id": token,
- "added_triples": added_triples,
- "suppliers": generated_suppliers,
- "customers": generated_customers,
- "catalog": self.catalog(),
- }
- def reset(self) -> dict:
- self.repository.reload()
- self.market_service.reset_runtime()
- return self.catalog()
- def _route_transport_mode(self, origin: str, destination: str) -> str:
- rows = self.repository.query_file(
- self.root / "queries" / "market-candidates.rq"
- )["rows"]
- return next(
- (
- row.get("transportMode", "海运")
- for row in rows
- if row.get("originLocation") == origin
- and row.get("destinationLocation") == destination
- ),
- "海运",
- )
- @staticmethod
- def _transit_days(transport_mode: str, index: int) -> int:
- return (3 + index) if transport_mode == "空运" else (16 + index * 2)
- @staticmethod
- def _supplier_profile(index: int) -> dict:
- profiles = [
- {
- "supplier_label": "船公司直营",
- "supplier_class": GESLI.Carrier,
- "supplier_type": "船公司",
- "product_label": "优先直航",
- "route_type": "直航",
- "service_form": "优先舱位包柜",
- "offering_channel": "船公司直营",
- "agency_level": "产品品牌本地分支机构",
- "capacity_commitment": "年度协议优先保舱20TEU",
- "spot_space_status": "可立即订舱",
- "spot_space_teu": 18,
- "relationship_tier": "战略合作",
- "relationship_strength": 0.96,
- "low_price_access_probability": 0.90,
- "priority_space_probability": 0.95,
- "relationship_evidence": "年度协议、价格窗口直连与优先放舱额度",
- "solution_assurance": 0.98,
- "service_support_score": 0.94,
- "booking_success_rate": 0.97,
- "space_release_success_rate": 0.96,
- "cost_ratio": 0.91,
- },
- {
- "supplier_label": "一级订舱代理",
- "supplier_class": GESLI.ShippingAgencyCompany,
- "supplier_type": "订舱代理",
- "product_label": "稳定直航",
- "route_type": "直航",
- "service_form": "稳定舱位包柜",
- "offering_channel": "一级订舱代理",
- "agency_level": "一级代理",
- "capacity_commitment": "月度保舱12TEU",
- "spot_space_status": "现舱有限",
- "spot_space_teu": 8,
- "relationship_tier": "核心合作",
- "relationship_strength": 0.87,
- "low_price_access_probability": 0.82,
- "priority_space_probability": 0.86,
- "relationship_evidence": "核心代理授权与固定订舱窗口",
- "solution_assurance": 0.90,
- "service_support_score": 0.90,
- "booking_success_rate": 0.94,
- "space_release_success_rate": 0.92,
- "cost_ratio": 0.86,
- },
- {
- "supplier_label": "稳定合作代理",
- "supplier_class": GESLI.FreightForwarderSupplier,
- "supplier_type": "货代公司",
- "product_label": "常规直航",
- "route_type": "直航",
- "service_form": "标准包柜",
- "offering_channel": "长期合作货代",
- "agency_level": "稳定合作代理",
- "capacity_commitment": "周度滚动确认8TEU",
- "spot_space_status": "现舱有限",
- "spot_space_teu": 4,
- "relationship_tier": "稳定合作",
- "relationship_strength": 0.74,
- "low_price_access_probability": 0.70,
- "priority_space_probability": 0.72,
- "relationship_evidence": "长期出货记录与周度保舱协同",
- "solution_assurance": 0.83,
- "service_support_score": 0.82,
- "booking_success_rate": 0.90,
- "space_release_success_rate": 0.88,
- "cost_ratio": 0.79,
- },
- {
- "supplier_label": "市场订舱代理",
- "supplier_class": GESLI.ShippingAgencyCompany,
- "supplier_type": "订舱代理",
- "product_label": "中转经济",
- "route_type": "中转",
- "service_form": "经济包柜",
- "offering_channel": "市场订舱代理",
- "agency_level": "二级代理",
- "capacity_commitment": "按单票确认舱位",
- "spot_space_status": "需确认",
- "spot_space_teu": 0,
- "relationship_tier": "常规合作",
- "relationship_strength": 0.52,
- "low_price_access_probability": 0.46,
- "priority_space_probability": 0.48,
- "relationship_evidence": "按市场舱位和临时报价执行",
- "solution_assurance": 0.67,
- "service_support_score": 0.68,
- "booking_success_rate": 0.84,
- "space_release_success_rate": 0.82,
- "cost_ratio": 0.72,
- },
- {
- "supplier_label": "区域货代",
- "supplier_class": GESLI.FreightForwarderSupplier,
- "supplier_type": "货代公司",
- "product_label": "加班船直航",
- "route_type": "直航",
- "service_form": "加班船包柜",
- "offering_channel": "区域货代",
- "agency_level": "合作货代",
- "capacity_commitment": "加班船窗口优先确认6TEU",
- "spot_space_status": "现舱有限",
- "spot_space_teu": 3,
- "relationship_tier": "稳定合作",
- "relationship_strength": 0.69,
- "low_price_access_probability": 0.63,
- "priority_space_probability": 0.70,
- "relationship_evidence": "区域市场协同与加班船资源预留",
- "solution_assurance": 0.80,
- "service_support_score": 0.79,
- "booking_success_rate": 0.89,
- "space_release_success_rate": 0.88,
- "cost_ratio": 0.82,
- },
- ]
- return profiles[index % len(profiles)]
- def _as_float(value: str | None, default: float = 0.0) -> float:
- try:
- return float(value) if value not in (None, "") else default
- except (TypeError, ValueError):
- return default
- def _as_date(value: str | None) -> date | None:
- if not value:
- return None
- try:
- return date.fromisoformat(value[:10])
- except ValueError:
- return None
- def _within(current: date, start: str | None, end: str | None) -> bool:
- start_date = _as_date(start)
- end_date = _as_date(end)
- return (not start_date or start_date <= current) and (
- not end_date or current <= end_date
- )
- def _date_time_literal(value: date) -> Literal:
- dt = datetime.combine(
- value,
- time(hour=12),
- tzinfo=timezone(timedelta(hours=8)),
- )
- return Literal(dt.isoformat(), datatype=XSD.dateTime)
|