优化
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
- 内部接口:`controller/IntegrationProviderController.java`、`WeightOcrController.java`
|
||||
- OCR 编排:`service/ocr/WeightOcrBatchService.java`、`ImageCropper.java`、`WeightCandidateSelector.java`
|
||||
- 供应商适配:`provider/IntegrationProviderAdapter.java`、`BaiduOcrAdapter.java`、`AmazonLogisticsAdapter.java`
|
||||
- 四期船节点外部适配:继续进入 `provider/` 和内部接口层;轮询任务、订舱/柜次关联、最新节点和节点历史归 `firstleg/portschedule/`,范围见 `../../docs/四期/README.md`
|
||||
- 四期船节点外部适配:内部契约 `controller/IntegrationProviderController.java` 的 `/internal/integrations/carrier-schedules/query`,统一 DTO `dto/CarrierScheduleDtos.java`,provider SPI `provider/CarrierScheduleProviderAdapter.java`,标准节点 `domain/CarrierScheduleStandardNode.java`;轮询任务、租约、订舱/柜次关联、最新节点和节点历史归 `firstleg/portschedule/`,范围见 `../../docs/四期/README.md`
|
||||
- 安全:`security/InternalServiceTokenGuard.java`;配置 `application.yml`、`../config/nacos/{local,cn}/integration-service.yml`
|
||||
- 测试:`src/test/java/com/baiyue/integration/`;`mvn -pl integration-service -am test`
|
||||
|
||||
|
||||
+16
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -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) { }
|
||||
}
|
||||
+9
@@ -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);
|
||||
}
|
||||
+35
@@ -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); }
|
||||
}
|
||||
+39
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Carrier Schedule Provider Internal API
|
||||
version: 1.0.0
|
||||
description: Internal provider adapter contract. It owns normalization, timeout, rate-limit and provider errors, not business tasks.
|
||||
paths:
|
||||
/internal/integrations/carrier-schedules/query:
|
||||
post:
|
||||
summary: Query one carrier tracking identifier through the configured provider adapter
|
||||
parameters:
|
||||
- name: X-Internal-Service-Token
|
||||
in: header
|
||||
required: true
|
||||
schema: {type: string}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/QueryRequest'}
|
||||
responses:
|
||||
'200': {description: Standard observations or an explicit normalized failure/manual outcome}
|
||||
components:
|
||||
schemas:
|
||||
QueryRequest:
|
||||
type: object
|
||||
required: [carrierCode, trackingType, trackingNo]
|
||||
properties:
|
||||
carrierCode: {type: string, enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]}
|
||||
trackingType: {type: string, enum: [BOOKING_NO, BILL_OF_LADING, CONTAINER_NO]}
|
||||
trackingNo: {type: string, minLength: 1, maxLength: 128}
|
||||
StandardNode:
|
||||
type: string
|
||||
enum: [DEPARTED_WAREHOUSE, AT_ORIGIN_PORT, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED, PICKUP_READY]
|
||||
QueryResult:
|
||||
type: object
|
||||
required: [providerCode, outcome, observations]
|
||||
properties:
|
||||
providerCode: {type: string}
|
||||
outcome: {type: string, enum: [SUCCESS, NO_CHANGE, RETRYABLE_FAILURE, PERMANENT_FAILURE, MANUAL_REQUIRED]}
|
||||
observations:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [providerEventId, providerNodeCode, standardNode, observedAt, rawPayloadHash]
|
||||
properties:
|
||||
providerEventId: {type: string}
|
||||
providerNodeCode: {type: string}
|
||||
standardNode: {$ref: '#/components/schemas/StandardNode'}
|
||||
observedAt: {type: string, format: date-time}
|
||||
estimatedAt: {type: string, format: date-time, nullable: true}
|
||||
actualAt: {type: string, format: date-time, nullable: true}
|
||||
locationCode: {type: string, nullable: true}
|
||||
locationName: {type: string, nullable: true}
|
||||
sourceReference: {type: string, nullable: true}
|
||||
rawPayloadHash: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.integration.service;
|
||||
|
||||
import com.baiyue.integration.dto.CarrierScheduleDtos.Observation;
|
||||
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryRequest;
|
||||
import com.baiyue.integration.dto.CarrierScheduleDtos.QueryResult;
|
||||
import com.baiyue.integration.provider.CarrierScheduleProviderAdapter;
|
||||
import com.baiyue.integration.provider.CompliantCarrierScheduleProviderAdapter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class CarrierScheduleProviderServiceTest {
|
||||
@Test
|
||||
void fallbackNeverPretendsThatWebsiteScrapingSucceeded() {
|
||||
var service = new CarrierScheduleProviderService(List.of(new CompliantCarrierScheduleProviderAdapter()));
|
||||
QueryResult result = service.query(new QueryRequest("COSCO", "CONTAINER_NO", "CSNU1234567"));
|
||||
assertThat(result.outcome()).isEqualTo("MANUAL_REQUIRED");
|
||||
assertThat(result.errorCode()).isEqualTo("PROVIDER_API_NOT_CONFIGURED");
|
||||
assertThat(result.observations()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsProviderSpecificNodesThatWereNotNormalized() {
|
||||
CarrierScheduleProviderAdapter invalid = new CarrierScheduleProviderAdapter() {
|
||||
@Override public boolean supports(String carrierCode) { return true; }
|
||||
@Override public QueryResult query(QueryRequest request) {
|
||||
return new QueryResult("TEST", "SUCCESS", List.of(new Observation("evt", "RAW", "UNKNOWN",
|
||||
LocalDateTime.now(), null, null, null, null, null,
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")), null, null,
|
||||
LocalDateTime.now());
|
||||
}
|
||||
};
|
||||
var service = new CarrierScheduleProviderService(List.of(invalid));
|
||||
assertThatThrownBy(() -> service.query(new QueryRequest("COSCO", "CONTAINER_NO", "CSNU1234567")))
|
||||
.hasMessageContaining("无效标准节点");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user