This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -4,10 +4,16 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.integration.dto.ProviderStatus;
import com.baiyue.integration.security.InternalServiceTokenGuard;
import com.baiyue.integration.service.ProviderStatusService;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryResult;
import com.baiyue.integration.service.CarrierScheduleProviderService;
import jakarta.validation.Valid;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@@ -19,6 +25,8 @@ public class IntegrationProviderController {
private ProviderStatusService providerStatusService;
@Resource
private InternalServiceTokenGuard internalServiceTokenGuard;
@Resource
private CarrierScheduleProviderService carrierScheduleProviderService;
@GetMapping("/providers")
public ApiResponse<List<ProviderStatus>> providers(
@@ -26,4 +34,12 @@ public class IntegrationProviderController {
internalServiceTokenGuard.verify(token);
return ApiResponse.success(providerStatusService.providers());
}
@PostMapping("/carrier-schedules/query")
public ApiResponse<QueryResult> queryCarrierSchedule(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody QueryRequest request) {
internalServiceTokenGuard.verify(token);
return ApiResponse.success(carrierScheduleProviderService.query(request));
}
}
@@ -0,0 +1,17 @@
package com.baiyue.integration.domain;
import java.util.Locale;
public enum CarrierScheduleStandardNode {
DEPARTED_WAREHOUSE,
AT_ORIGIN_PORT,
DEPARTED_ORIGIN_PORT,
IN_TRANSIT,
ARRIVED,
PICKUP_READY;
public static boolean supports(String value) {
try { valueOf(value == null ? "" : value.trim().toUpperCase(Locale.ROOT)); return true; }
catch (IllegalArgumentException exception) { return false; }
}
}
@@ -0,0 +1,23 @@
package com.baiyue.integration.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
public final class CarrierScheduleDtos {
private CarrierScheduleDtos() { }
public record QueryRequest(@NotBlank @Size(max = 16) String carrierCode,
@NotBlank @Size(max = 32) String trackingType,
@NotBlank @Size(max = 128) String trackingNo) { }
public record QueryResult(String providerCode, String outcome, List<Observation> observations,
String errorCode, String errorMessage, LocalDateTime providerObservedAt) { }
public record Observation(String providerEventId, String providerNodeCode, String standardNode,
LocalDateTime observedAt, LocalDateTime estimatedAt, LocalDateTime actualAt,
String locationCode, String locationName, String sourceReference,
String rawPayloadHash) { }
}
@@ -0,0 +1,9 @@
package com.baiyue.integration.provider;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryResult;
public interface CarrierScheduleProviderAdapter {
boolean supports(String carrierCode);
QueryResult query(QueryRequest request);
}
@@ -0,0 +1,35 @@
package com.baiyue.integration.provider;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryResult;
import com.baiyue.integration.dto.ProviderStatus;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Safe fallback for the six confirmed carriers. It never scrapes captcha/dynamic pages and returns
* an explicit provider-unavailable outcome until a documented provider API adapter is configured.
*/
@Component
public class CompliantCarrierScheduleProviderAdapter implements CarrierScheduleProviderAdapter, IntegrationProviderAdapter {
private static final Set<String> CARRIERS = Set.of("MATSON", "SML", "ZIM", "COSCO", "EMC", "HEDE");
@Override
public boolean supports(String carrierCode) {
return carrierCode != null && CARRIERS.contains(carrierCode.trim().toUpperCase(Locale.ROOT));
}
@Override
public QueryResult query(QueryRequest request) {
return new QueryResult("COMPLIANT_MANUAL_FALLBACK", "MANUAL_REQUIRED", List.of(),
"PROVIDER_API_NOT_CONFIGURED", "未配置可审计的结构化轨迹供应商接口,不执行网页抓取",
LocalDateTime.now());
}
@Override
public ProviderStatus status() { return ProviderStatus.of("CARRIER_SCHEDULE", true, false); }
}
@@ -0,0 +1,39 @@
package com.baiyue.integration.service;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest;
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryResult;
import com.baiyue.integration.provider.CarrierScheduleProviderAdapter;
import com.baiyue.integration.domain.CarrierScheduleStandardNode;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
@Service
public class CarrierScheduleProviderService {
private static final Set<String> OUTCOMES = Set.of("SUCCESS", "NO_CHANGE", "RETRYABLE_FAILURE",
"PERMANENT_FAILURE", "MANUAL_REQUIRED");
private final List<CarrierScheduleProviderAdapter> adapters;
public CarrierScheduleProviderService(List<CarrierScheduleProviderAdapter> adapters) {
this.adapters = List.copyOf(adapters);
}
public QueryResult query(QueryRequest request) {
QueryResult result = adapters.stream().filter(adapter -> adapter.supports(request.carrierCode())).findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"没有支持该船司的轨迹供应商适配器:" + request.carrierCode()))
.query(request);
if (result == null || result.observations() == null || !OUTCOMES.contains(result.outcome())) {
throw new IllegalStateException("轨迹供应商返回空结果");
}
if (result.observations().stream().anyMatch(value -> value == null
|| !CarrierScheduleStandardNode.supports(value.standardNode())
|| value.providerEventId() == null || value.providerEventId().isBlank()
|| value.observedAt() == null || value.rawPayloadHash() == null
|| !value.rawPayloadHash().matches("[0-9a-f]{64}"))) {
throw new IllegalStateException("轨迹供应商返回了无效标准节点");
}
return result;
}
}