优化
This commit is contained in:
+4
@@ -20,9 +20,13 @@ public interface CarrierScheduleIntegrationClient {
|
||||
}
|
||||
record QueryRequest(String carrierCode, String trackingType, String trackingNo) { }
|
||||
record QueryResult(String providerCode, String outcome, List<Observation> observations,
|
||||
List<ExceptionObservation> exceptions,
|
||||
String errorCode, String errorMessage, LocalDateTime providerObservedAt) { }
|
||||
record Observation(String providerEventId, String providerNodeCode, String standardNode,
|
||||
LocalDateTime observedAt, LocalDateTime estimatedAt, LocalDateTime actualAt,
|
||||
String locationCode, String locationName, String sourceReference,
|
||||
String rawPayloadHash) { }
|
||||
record ExceptionObservation(String providerEventId, String providerStatus, String exceptionCode,
|
||||
boolean active, LocalDateTime observedAt, String locationCode,
|
||||
String locationName, String sourceReference, String rawPayloadHash) { }
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package com.baiyue.firstleg.portschedule.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/firstleg/v1/carrier-schedule-queries")
|
||||
public class InternalCarrierScheduleQueryController {
|
||||
private final CarrierScheduleQueryService service;
|
||||
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
||||
|
||||
public InternalCarrierScheduleQueryController(CarrierScheduleQueryService service) { this.service = service; }
|
||||
|
||||
@PostMapping("/{queryId}/stop")
|
||||
public ApiResponse<Detail> stop(@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable @Positive long queryId,
|
||||
@Valid @RequestBody StopRequest request) {
|
||||
authenticate(token);
|
||||
return ApiResponse.success(service.stop(queryId, request.reason()));
|
||||
}
|
||||
|
||||
private void authenticate(String token) {
|
||||
if (token == null || expectedToken == null || expectedToken.isBlank()
|
||||
|| !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8),
|
||||
expectedToken.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务令牌无效");
|
||||
}
|
||||
}
|
||||
|
||||
public record StopRequest(@NotBlank @Size(max = 500) String reason) { }
|
||||
}
|
||||
+13
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDesc
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.QueryView;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.History;
|
||||
@@ -93,4 +94,16 @@ public class PlatformCarrierScheduleQueryController {
|
||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||
return ApiResponse.success(service.attachManualResult(queryId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{queryId}/manual-nodes")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_NODE", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
resourceId = "#queryId", description = "人工补录船节点事实")
|
||||
public ApiResponse<Detail> manualNode(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long queryId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ManualNodeRequest request) {
|
||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||
return ApiResponse.success(service.addManualNode(queryId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.portschedule.domain;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum CarrierScheduleExceptionCode {
|
||||
CUSTOMS_HOLD,
|
||||
TERMINAL_HOLD,
|
||||
DOCUMENT_HOLD,
|
||||
UNKNOWN_HOLD;
|
||||
|
||||
public static CarrierScheduleExceptionCode require(String value) {
|
||||
try { return valueOf(value == null ? "" : value.trim().toUpperCase(Locale.ROOT)); }
|
||||
catch (IllegalArgumentException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "不支持的船节点异常:" + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -9,10 +9,14 @@ import java.util.Locale;
|
||||
public enum CarrierScheduleNode {
|
||||
DEPARTED_WAREHOUSE(10),
|
||||
AT_ORIGIN_PORT(20),
|
||||
DEPARTED_ORIGIN_PORT(30),
|
||||
IN_TRANSIT(40),
|
||||
ARRIVED(50),
|
||||
PICKUP_READY(60);
|
||||
LOADED_ON_VESSEL(30),
|
||||
DEPARTED_ORIGIN_PORT(40),
|
||||
IN_TRANSIT(50),
|
||||
ARRIVED_DESTINATION_PORT(60),
|
||||
DISCHARGED_FROM_VESSEL(70),
|
||||
PICKUP_READY(80),
|
||||
PICKED_UP(90),
|
||||
EMPTY_RETURNED(100);
|
||||
|
||||
private final int sequence;
|
||||
|
||||
|
||||
+33
-6
@@ -16,9 +16,9 @@ public final class CarrierScheduleQueryDtos {
|
||||
public record Query(@Positive Long customerId, @Positive Long oceanBookingId,
|
||||
@Positive Long containerBookingId, @Size(max = 16) String carrierCode,
|
||||
@Size(max = 24) String status, Boolean expired,
|
||||
@Size(max = 128) String keyword, @Min(1) long page,
|
||||
@Min(1) @Max(200) long size) {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
@Size(max = 128) String keyword, @Min(1) Long page,
|
||||
@Min(1) @Max(200) Long size) {
|
||||
public Query { if (page == null || page == 0) page = 1L; if (size == null || size == 0) size = 20L; }
|
||||
}
|
||||
|
||||
public record CreateRequest(@NotNull @Min(0) @Max(0) Integer expectedVersion,
|
||||
@@ -34,6 +34,15 @@ public final class CarrierScheduleQueryDtos {
|
||||
@NotNull @Positive Long portScheduleId,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
|
||||
public record ManualNodeRequest(@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 32) String standardNode,
|
||||
@NotNull LocalDateTime observedAt,
|
||||
LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt,
|
||||
@Size(max = 32) String locationCode,
|
||||
@Size(max = 128) String locationName,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
|
||||
public record ProviderObservationRequest(@NotBlank @Size(max = 32) String providerCode,
|
||||
@NotBlank @Size(max = 128) String providerEventId,
|
||||
@NotBlank @Size(max = 64) String providerNodeCode,
|
||||
@@ -46,6 +55,17 @@ public final class CarrierScheduleQueryDtos {
|
||||
@Size(max = 500) String sourceReference,
|
||||
@NotBlank @Size(min = 64, max = 64) String rawPayloadHash) {}
|
||||
|
||||
public record ProviderExceptionRequest(@NotBlank @Size(max = 32) String providerCode,
|
||||
@NotBlank @Size(max = 128) String providerEventId,
|
||||
@NotBlank @Size(max = 128) String providerStatus,
|
||||
@NotBlank @Size(max = 32) String exceptionCode,
|
||||
boolean active,
|
||||
@NotNull LocalDateTime observedAt,
|
||||
@Size(max = 32) String locationCode,
|
||||
@Size(max = 128) String locationName,
|
||||
@Size(max = 500) String sourceReference,
|
||||
@NotBlank @Size(min = 64, max = 64) String rawPayloadHash) {}
|
||||
|
||||
public record CarrierDescriptor(String carrierCode, String carrierName, String officialUrl,
|
||||
String accessMode, boolean automated, boolean coverageGap,
|
||||
String coverageGapCode, String manualReason) {}
|
||||
@@ -53,21 +73,28 @@ public final class CarrierScheduleQueryDtos {
|
||||
public record Task(Long id, String queryNo, Long customerId, Long oceanBookingId,
|
||||
Long containerBookingId, String carrierCode, String trackingType, String trackingNo,
|
||||
String officialUrl, String accessMode, String status, String triggerSource,
|
||||
String latestStandardNode, LocalDateTime latestNodeObservedAt,
|
||||
String latestActualNode, LocalDateTime latestActualAt,
|
||||
LocalDateTime lastSuccessAt, LocalDateTime sourceUpdatedAt, LocalDateTime expiresAt,
|
||||
Long resultPortScheduleId, String manualReason, String lastErrorCode,
|
||||
String lastErrorMessage, LocalDateTime stoppedAt, String stopReason, Integer version) {}
|
||||
|
||||
public record QueryView(Task query, boolean expired) {}
|
||||
|
||||
public record NodeFact(Long id, String providerCode, String providerNodeCode, String standardNode,
|
||||
public record NodeFact(Long id, String providerCode, String providerEventId,
|
||||
String providerNodeCode, String standardNode,
|
||||
int nodeSequence, LocalDateTime observedAt, LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash, LocalDateTime createdAt) {}
|
||||
|
||||
public record ExceptionFact(Long id, String providerCode, String providerEventId,
|
||||
String providerStatus, String exceptionCode, boolean active,
|
||||
LocalDateTime observedAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash, LocalDateTime createdAt) {}
|
||||
|
||||
public record History(Long id, String action, Integer fromVersion, Integer toVersion, String reason,
|
||||
Long operatorId, String operatorName, LocalDateTime createdAt) {}
|
||||
|
||||
/** Frontend detail intentionally excludes scheduler leases and raw query attempts. */
|
||||
public record Detail(QueryView query, List<NodeFact> nodes, List<History> histories) {}
|
||||
public record Detail(QueryView query, List<NodeFact> nodes, List<ExceptionFact> exceptions,
|
||||
List<History> histories) {}
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.firstleg.portschedule.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_carrier_schedule_exception_fact")
|
||||
public class CarrierScheduleExceptionFactEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long queryId;
|
||||
private String eventKey;
|
||||
private String providerCode;
|
||||
private String providerEventId;
|
||||
private String providerStatus;
|
||||
private String exceptionCode;
|
||||
private Boolean active;
|
||||
private LocalDateTime observedAt;
|
||||
private String locationCode;
|
||||
private String locationName;
|
||||
private String sourceReference;
|
||||
private String rawPayloadHash;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
}
|
||||
+1
@@ -18,6 +18,7 @@ public class CarrierScheduleNodeFactEntity {
|
||||
private Long queryId;
|
||||
private String eventKey;
|
||||
private String providerCode;
|
||||
private String providerEventId;
|
||||
private String providerNodeCode;
|
||||
private String standardNode;
|
||||
private Integer nodeSequence;
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.firstleg.portschedule.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleExceptionFactEntity;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleExceptionFactMapper extends BaseMapper<CarrierScheduleExceptionFactEntity> {
|
||||
@Insert("""
|
||||
INSERT INTO fl_carrier_schedule_exception_fact
|
||||
(id,query_id,event_key,provider_code,provider_event_id,provider_status,exception_code,active,
|
||||
observed_at,location_code,location_name,source_reference,raw_payload_hash,created_at)
|
||||
VALUES
|
||||
(#{value.id},#{value.queryId},#{value.eventKey},#{value.providerCode},#{value.providerEventId},
|
||||
#{value.providerStatus},#{value.exceptionCode},#{value.active},#{value.observedAt},
|
||||
#{value.locationCode},#{value.locationName},#{value.sourceReference},#{value.rawPayloadHash},
|
||||
CURRENT_TIMESTAMP(3))
|
||||
ON DUPLICATE KEY UPDATE id=id
|
||||
""")
|
||||
int insertIfAbsent(@Param("value") CarrierScheduleExceptionFactEntity value);
|
||||
}
|
||||
+2
-2
@@ -10,10 +10,10 @@ import org.apache.ibatis.annotations.Param;
|
||||
public interface CarrierScheduleNodeFactMapper extends BaseMapper<CarrierScheduleNodeFactEntity> {
|
||||
@Insert("""
|
||||
INSERT INTO fl_carrier_schedule_node_fact
|
||||
(id,query_id,event_key,provider_code,provider_node_code,standard_node,node_sequence,observed_at,
|
||||
(id,query_id,event_key,provider_code,provider_event_id,provider_node_code,standard_node,node_sequence,observed_at,
|
||||
estimated_at,actual_at,location_code,location_name,source_reference,raw_payload_hash,created_at)
|
||||
VALUES
|
||||
(#{value.id},#{value.queryId},#{value.eventKey},#{value.providerCode},#{value.providerNodeCode},
|
||||
(#{value.id},#{value.queryId},#{value.eventKey},#{value.providerCode},#{value.providerEventId},#{value.providerNodeCode},
|
||||
#{value.standardNode},#{value.nodeSequence},#{value.observedAt},#{value.estimatedAt},#{value.actualAt},
|
||||
#{value.locationCode},#{value.locationName},#{value.sourceReference},#{value.rawPayloadHash},
|
||||
CURRENT_TIMESTAMP(3))
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ public interface CarrierScheduleQueryMapper extends BaseMapper<CarrierScheduleQu
|
||||
|
||||
@Select("""
|
||||
SELECT * FROM fl_carrier_schedule_query
|
||||
WHERE status IN ('READY','MANUAL_REQUIRED','ARRIVED','PICKUP_READY')
|
||||
WHERE status IN ('READY','MANUAL_REQUIRED')
|
||||
AND next_allowed_at IS NOT NULL AND next_allowed_at<=#{now}
|
||||
AND (lease_until IS NULL OR lease_until<#{now})
|
||||
ORDER BY next_allowed_at,id LIMIT #{limit}
|
||||
@@ -58,7 +58,7 @@ public interface CarrierScheduleQueryMapper extends BaseMapper<CarrierScheduleQu
|
||||
@Update("""
|
||||
UPDATE fl_carrier_schedule_query SET lease_owner=#{owner},lease_until=#{leaseUntil},
|
||||
updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND status IN ('READY','MANUAL_REQUIRED','ARRIVED','PICKUP_READY')
|
||||
WHERE id=#{id} AND status IN ('READY','MANUAL_REQUIRED')
|
||||
AND next_allowed_at IS NOT NULL AND next_allowed_at<=#{now}
|
||||
AND (lease_until IS NULL OR lease_until<#{now})
|
||||
""")
|
||||
|
||||
+12
@@ -10,11 +10,13 @@ import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryOperationEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleExceptionFactEntity;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryAttemptMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryHistoryMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryOperationMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleNodeFactMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleExceptionFactMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -28,6 +30,7 @@ public class CarrierScheduleQueryRepository {
|
||||
@Resource private CarrierScheduleQueryHistoryMapper histories;
|
||||
@Resource private CarrierScheduleQueryOperationMapper operations;
|
||||
@Resource private CarrierScheduleNodeFactMapper nodes;
|
||||
@Resource private CarrierScheduleExceptionFactMapper exceptions;
|
||||
|
||||
public PageResult<CarrierScheduleQueryEntity> page(Query query) {
|
||||
return PageResult.from(queries.selectPage(new Page<>(query.page(), query.size()), query));
|
||||
@@ -67,6 +70,15 @@ public class CarrierScheduleQueryRepository {
|
||||
.orderByAsc(CarrierScheduleNodeFactEntity::getId));
|
||||
}
|
||||
public boolean insertIfAbsent(CarrierScheduleNodeFactEntity value) { return nodes.insertIgnore(value) == 1; }
|
||||
public List<CarrierScheduleExceptionFactEntity> exceptions(long id) {
|
||||
return exceptions.selectList(new LambdaQueryWrapper<CarrierScheduleExceptionFactEntity>()
|
||||
.eq(CarrierScheduleExceptionFactEntity::getQueryId, id)
|
||||
.orderByAsc(CarrierScheduleExceptionFactEntity::getObservedAt)
|
||||
.orderByAsc(CarrierScheduleExceptionFactEntity::getId));
|
||||
}
|
||||
public boolean insertIfAbsent(CarrierScheduleExceptionFactEntity value) {
|
||||
return exceptions.insertIfAbsent(value) == 1;
|
||||
}
|
||||
public boolean claimOperation(Long queryId, long operatorId, String operation, String key, String hash) {
|
||||
return operations.claim(IdWorker.getId(), queryId, operatorId, operation, key, hash) == 1;
|
||||
}
|
||||
|
||||
+38
-14
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -16,15 +17,27 @@ import java.util.Map;
|
||||
@Component
|
||||
public class CarrierScheduleAdapterRegistry {
|
||||
private static final Map<String, Definition> DEFINITIONS = definitions();
|
||||
@Value("${baiyue.carrier-schedule.providers.matson-automated:false}")
|
||||
private boolean matsonAutomated;
|
||||
@Value("${baiyue.carrier-schedule.providers.hede-automated:false}")
|
||||
private boolean hedeAutomated;
|
||||
@Value("${baiyue.carrier-schedule.providers.sml-automated:false}")
|
||||
private boolean smlAutomated;
|
||||
@Value("${baiyue.carrier-schedule.providers.zim-official-api-enabled:false}")
|
||||
private boolean zimOfficialApiEnabled;
|
||||
@Value("${baiyue.carrier-schedule.providers.cosco-official-api-enabled:false}")
|
||||
private boolean coscoOfficialApiEnabled;
|
||||
@Value("${baiyue.carrier-schedule.providers.emc-automated:false}")
|
||||
private boolean emcAutomated;
|
||||
|
||||
public List<CarrierDescriptor> descriptors() {
|
||||
return DEFINITIONS.values().stream().map(Definition::descriptor).toList();
|
||||
return DEFINITIONS.values().stream().map(value -> value.descriptor(automated(value))).toList();
|
||||
}
|
||||
|
||||
public PreparedAttempt prepare(String carrierCode, String trackingType, String trackingNo) {
|
||||
Definition definition = required(carrierCode);
|
||||
return new PreparedAttempt(definition.code(), definition.accessMode(),
|
||||
officialUrl(definition, trackingType, trackingNo), "MANUAL_REQUIRED",
|
||||
officialUrl(definition, trackingType, trackingNo), automated(definition) ? "READY" : "MANUAL_REQUIRED",
|
||||
definition.coverageGapCode(), definition.manualReason());
|
||||
}
|
||||
|
||||
@@ -52,9 +65,6 @@ public class CarrierScheduleAdapterRegistry {
|
||||
|
||||
private static String officialUrl(Definition definition, String trackingType, String trackingNo) {
|
||||
String encoded = URLEncoder.encode(trackingNo.trim(), StandardCharsets.UTF_8);
|
||||
if ("ZIM".equals(definition.code())) {
|
||||
return definition.officialUrl() + "?consnumber=" + encoded;
|
||||
}
|
||||
if ("EMC".equals(definition.code())) {
|
||||
String type = "CONTAINER_NO".equalsIgnoreCase(trackingType) ? "cntr" : "bl";
|
||||
return definition.officialUrl() + "?c=" + encoded + "&t=" + type + "&url=EVERGREEN";
|
||||
@@ -65,26 +75,40 @@ public class CarrierScheduleAdapterRegistry {
|
||||
private static Map<String, Definition> definitions() {
|
||||
Map<String, Definition> values = new LinkedHashMap<>();
|
||||
add(values, new Definition("MATSON", "美森", "https://www.matson.com/shipment-tracking.html",
|
||||
"PUBLIC_FORM", "NO_DOCUMENTED_STRUCTURED_API", "官网仅公开交互表单,当前需人工查询并录入结果"));
|
||||
"PUBLIC_WEB_PROXY", null, null));
|
||||
add(values, new Definition("SML", "SML", "https://esvc.smlines.com/smline/CUP_HOM_3301.do?sessLocale=zh",
|
||||
"PUBLIC_FORM", "NO_DOCUMENTED_STRUCTURED_API", "官网未提供稳定公开结构化接口,当前需人工查询并录入结果"));
|
||||
add(values, new Definition("ZIM", "ZIM", "https://www.zimchina.com/tools/track-a-shipment",
|
||||
"HCAPTCHA", "HCAPTCHA_REQUIRED", "官网启用hCaptcha,禁止绕过验证码,必须人工查询"));
|
||||
add(values, new Definition("COSCO", "COSCO", "https://elines.coscoshipping.com/ebusiness/",
|
||||
"DYNAMIC_IFRAME", "DYNAMIC_IFRAME_REQUIRED", "官网使用动态页面/iframe,未发现稳定公开API,必须人工查询"));
|
||||
"PUBLIC_FORM", null, null));
|
||||
add(values, new Definition("ZIM", "ZIM", "https://api.zim.com/",
|
||||
"OFFICIAL_API", "OFFICIAL_API_CREDENTIALS_REQUIRED",
|
||||
"ZIM Cargo Tracking API 待配置订阅凭据及来源授权,启用前必须人工查询"));
|
||||
add(values, new Definition("COSCO", "COSCO", "https://cop.lines.coscoshipping.com/copPortal/",
|
||||
"OFFICIAL_API", "OFFICIAL_API_CREDENTIALS_REQUIRED",
|
||||
"COSCO Shipping Open Platform Cargo Tracking API 待注册订阅及凭据,启用前必须人工查询"));
|
||||
add(values, new Definition("EMC", "EMC", "https://www.evergreen-shipping.cn/servlet/TDB1_CargoTracking.do",
|
||||
"PUBLIC_FORM", "NO_DOCUMENTED_STRUCTURED_API", "官网仅公开查询表单,当前需人工确认页面结果后录入"));
|
||||
add(values, new Definition("HEDE", "合德", "http://elines.hedehk.com/cargoDynamic",
|
||||
"PUBLIC_FORM", "NO_DOCUMENTED_STRUCTURED_API", "官网未提供稳定公开结构化接口,当前需人工查询并录入结果"));
|
||||
"PUBLIC_HTML", null, null));
|
||||
return Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
private static void add(Map<String, Definition> values, Definition value) { values.put(value.code(), value); }
|
||||
|
||||
private boolean automated(Definition definition) {
|
||||
return switch (definition.code()) {
|
||||
case "MATSON" -> matsonAutomated;
|
||||
case "SML" -> smlAutomated;
|
||||
case "ZIM" -> zimOfficialApiEnabled;
|
||||
case "COSCO" -> coscoOfficialApiEnabled;
|
||||
case "EMC" -> emcAutomated;
|
||||
case "HEDE" -> hedeAutomated;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private record Definition(String code, String name, String officialUrl, String accessMode,
|
||||
String coverageGapCode, String manualReason) {
|
||||
CarrierDescriptor descriptor() {
|
||||
return new CarrierDescriptor(code, name, officialUrl, accessMode, false, true,
|
||||
CarrierDescriptor descriptor(boolean automated) {
|
||||
return new CarrierDescriptor(code, name, officialUrl, accessMode, automated, !automated,
|
||||
coverageGapCode, manualReason);
|
||||
}
|
||||
boolean matches(String value) {
|
||||
|
||||
+9
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.portschedule.service;
|
||||
import com.baiyue.firstleg.client.CarrierScheduleIntegrationClient;
|
||||
import com.baiyue.firstleg.client.CarrierScheduleIntegrationClient.QueryRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderObservationRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderExceptionRequest;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -61,6 +62,14 @@ public class CarrierScheduleQueryScheduler {
|
||||
observation.actualAt(), observation.locationCode(), observation.locationName(),
|
||||
observation.sourceReference(), observation.rawPayloadHash()));
|
||||
}
|
||||
List<CarrierScheduleIntegrationClient.ExceptionObservation> exceptions = result.exceptions() == null
|
||||
? List.of() : result.exceptions();
|
||||
for (var exception : exceptions) {
|
||||
service.applyProviderException(candidate.getId(), new ProviderExceptionRequest(
|
||||
result.providerCode(), exception.providerEventId(), exception.providerStatus(),
|
||||
exception.exceptionCode(), exception.active(), exception.observedAt(), exception.locationCode(),
|
||||
exception.locationName(), exception.sourceReference(), exception.rawPayloadHash()));
|
||||
}
|
||||
service.completeScheduledAttempt(candidate.getId(), result.outcome(), result.errorCode(),
|
||||
result.errorMessage());
|
||||
} catch (RuntimeException exception) {
|
||||
|
||||
+111
-26
@@ -9,17 +9,23 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDesc
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.QueryView;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderObservationRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.NodeFact;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.History;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ExceptionFact;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderExceptionRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Task;
|
||||
import com.baiyue.firstleg.portschedule.domain.CarrierScheduleNode;
|
||||
import com.baiyue.firstleg.portschedule.domain.CarrierScheduleExceptionCode;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryOperationEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleExceptionFactEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.PortScheduleEntity;
|
||||
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
|
||||
@@ -128,7 +134,6 @@ public class CarrierScheduleQueryService {
|
||||
@Transactional
|
||||
public Detail applyProviderObservation(long id, ProviderObservationRequest request) {
|
||||
CarrierScheduleQueryEntity value = requiredLocked(id);
|
||||
if ("STOPPED".equals(value.getStatus())) return detail(value);
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
if (!addNode(id, request.providerCode(), request.providerEventId(), request.providerNodeCode(), node,
|
||||
@@ -136,40 +141,83 @@ public class CarrierScheduleQueryService {
|
||||
request.locationName(), request.sourceReference(), request.rawPayloadHash())) {
|
||||
return detail(value);
|
||||
}
|
||||
CarrierScheduleNode current = value.getLatestStandardNode() == null ? null
|
||||
: CarrierScheduleNode.require(value.getLatestStandardNode());
|
||||
if (current == null || node.sequence() >= current.sequence()) {
|
||||
value.setLatestStandardNode(node.name()); value.setLatestNodeObservedAt(request.observedAt());
|
||||
value.setLastSuccessAt(now()); value.setSourceUpdatedAt(request.observedAt());
|
||||
value.setLastAttemptStatus("SUCCESS"); value.setLastErrorCode(null); value.setLastErrorMessage(null);
|
||||
value.setStatus(node == CarrierScheduleNode.ARRIVED ? "ARRIVED"
|
||||
: node == CarrierScheduleNode.PICKUP_READY ? "PICKUP_READY" : "READY");
|
||||
value.setNextAllowedAt(now().plusDays(WINDOW_DAYS));
|
||||
if (repository.updateState(value, value.getVersion(), 0L, "carrier-scheduler") != 1) throw concurrent();
|
||||
}
|
||||
CarrierScheduleQueryEntity after = required(id);
|
||||
boolean advanced = advanceActualNode(value, node, request.actualAt(), request.observedAt(),
|
||||
0L, "carrier-scheduler");
|
||||
CarrierScheduleQueryEntity after = advanced ? required(id) : value;
|
||||
addHistory(id, "NODE_OBSERVED", before.getVersion(), after.getVersion(), request.standardNode(), before,
|
||||
after, 0L, "carrier-scheduler");
|
||||
return detail(after);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail stop(long id, String reason) {
|
||||
public Detail applyProviderException(long id, ProviderExceptionRequest request) {
|
||||
CarrierScheduleQueryEntity value = requiredLocked(id);
|
||||
if ("STOPPED".equals(value.getStatus())) return detail(value);
|
||||
CarrierScheduleExceptionCode code = CarrierScheduleExceptionCode.require(request.exceptionCode());
|
||||
if (!addException(id, request.providerCode(), request.providerEventId(), request.providerStatus(), code,
|
||||
request.active(), request.observedAt(), request.locationCode(), request.locationName(),
|
||||
request.sourceReference(), request.rawPayloadHash())) {
|
||||
return detail(value);
|
||||
}
|
||||
addHistory(id, "EXCEPTION_OBSERVED", value.getVersion(), value.getVersion(), code.name(), value, value,
|
||||
0L, "carrier-scheduler");
|
||||
return detail(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail addManualNode(long id, long operatorId, String operatorName, String key,
|
||||
ManualNodeRequest request) {
|
||||
String operation = "MANUAL_NODE";
|
||||
Detail replay = claimOrReplay(id, operatorId, operation, key, hash(operation, id, request));
|
||||
if (replay != null) return replay;
|
||||
if (request.estimatedAt() == null && request.actualAt() == null) {
|
||||
throw badRequest("estimatedAt和actualAt至少提供一个");
|
||||
}
|
||||
CarrierScheduleQueryEntity value = requiredLockedVersion(id, request.expectedVersion());
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
value.setStatus("STOPPED"); value.setStoppedAt(now()); value.setStopReason(trim(reason));
|
||||
value.setLeaseOwner(null); value.setLeaseUntil(null); value.setNextAllowedAt(null);
|
||||
if (repository.updateState(value, value.getVersion(), 0L, "carrier-scheduler") != 1) throw concurrent();
|
||||
CarrierScheduleQueryEntity after = required(id);
|
||||
addHistory(id, "STOP", before.getVersion(), after.getVersion(), reason, before, after, 0L,
|
||||
"carrier-scheduler");
|
||||
return detail(after);
|
||||
String providerEventId = sha256("MANUAL|" + id + '|' + key);
|
||||
if (!addNode(id, "MANUAL", providerEventId, node.name(), node, request.observedAt(),
|
||||
request.estimatedAt(), request.actualAt(), request.locationCode(), request.locationName(),
|
||||
"MANUAL:" + operatorId, hash(operation, id, request))) {
|
||||
return complete(id, operatorId, key, detail(value));
|
||||
}
|
||||
boolean advanced = advanceActualNode(value, node, request.actualAt(), request.observedAt(),
|
||||
operatorId, operatorName);
|
||||
CarrierScheduleQueryEntity after = advanced ? required(id) : value;
|
||||
addHistory(id, operation, before.getVersion(), after.getVersion(), request.reason(), before, after,
|
||||
operatorId, operatorName);
|
||||
return complete(id, operatorId, key, detail(after));
|
||||
}
|
||||
|
||||
private boolean advanceActualNode(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
|
||||
LocalDateTime actualAt, LocalDateTime observedAt,
|
||||
long operatorId, String operatorName) {
|
||||
if (actualAt == null || "STOPPED".equals(value.getStatus())) return false;
|
||||
CarrierScheduleNode current = value.getLatestStandardNode() == null ? null
|
||||
: CarrierScheduleNode.require(value.getLatestStandardNode());
|
||||
boolean advances = current == null || node.sequence() > current.sequence()
|
||||
|| node.sequence() == current.sequence() && (value.getLatestNodeObservedAt() == null
|
||||
|| actualAt.isAfter(value.getLatestNodeObservedAt()));
|
||||
if (advances) {
|
||||
value.setLatestStandardNode(node.name()); value.setLatestNodeObservedAt(actualAt);
|
||||
value.setLastSuccessAt(now()); value.setSourceUpdatedAt(observedAt);
|
||||
value.setLastAttemptStatus("SUCCESS"); value.setLastErrorCode(null); value.setLastErrorMessage(null);
|
||||
if (node == CarrierScheduleNode.EMPTY_RETURNED) {
|
||||
value.setStatus("STOPPED"); value.setStoppedAt(now()); value.setStopReason("已还柜");
|
||||
value.setLeaseOwner(null); value.setLeaseUntil(null); value.setNextAllowedAt(null);
|
||||
} else {
|
||||
value.setStatus("READY"); value.setNextAllowedAt(now().plusDays(WINDOW_DAYS));
|
||||
}
|
||||
if (repository.updateState(value, value.getVersion(), operatorId, operatorName) != 1) throw concurrent();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void completeScheduledAttempt(long id, String outcome, String errorCode, String errorMessage) {
|
||||
CarrierScheduleQueryEntity value = requiredLocked(id);
|
||||
insertAttempt(value, outcome, errorCode, errorMessage);
|
||||
if ("STOPPED".equals(value.getStatus())) return;
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
LocalDateTime now = now();
|
||||
@@ -177,6 +225,7 @@ public class CarrierScheduleQueryService {
|
||||
value.setLastAttemptStatus(outcome); value.setLastErrorCode(trim(errorCode));
|
||||
value.setLastErrorMessage(trim(errorMessage));
|
||||
if ("SUCCESS".equals(outcome) || "NO_CHANGE".equals(outcome)) {
|
||||
value.setStatus("READY");
|
||||
value.setConsecutiveFailures(0); value.setAttemptCount(0); value.setWindowStartedAt(now);
|
||||
value.setNextAllowedAt(now.plusDays(WINDOW_DAYS));
|
||||
} else {
|
||||
@@ -189,9 +238,7 @@ public class CarrierScheduleQueryService {
|
||||
value.setWindowStartedAt(windowStart); value.setAttemptCount(Math.min(MAX_ATTEMPTS, attempts));
|
||||
value.setConsecutiveFailures(failures);
|
||||
if ("MANUAL_REQUIRED".equals(outcome) || "PERMANENT_FAILURE".equals(outcome)) {
|
||||
if (!"ARRIVED".equals(value.getStatus()) && !"PICKUP_READY".equals(value.getStatus())) {
|
||||
value.setStatus("MANUAL_REQUIRED");
|
||||
}
|
||||
value.setStatus("MANUAL_REQUIRED");
|
||||
value.setNextAllowedAt(now.plusDays(WINDOW_DAYS));
|
||||
} else {
|
||||
long minutes = Math.min(24L * 60L, 15L << Math.min(failures - 1, 6));
|
||||
@@ -208,6 +255,20 @@ public class CarrierScheduleQueryService {
|
||||
"carrier-scheduler");
|
||||
}
|
||||
|
||||
private void insertAttempt(CarrierScheduleQueryEntity value, String outcome,
|
||||
String errorCode, String errorMessage) {
|
||||
CarrierScheduleQueryAttemptEntity attempt = new CarrierScheduleQueryAttemptEntity();
|
||||
attempt.setId(IdWorker.getId()); attempt.setQueryId(value.getId());
|
||||
attempt.setWindowAttemptNo(Math.max(1, Math.min(MAX_ATTEMPTS, count(value.getAttemptCount()) + 1)));
|
||||
attempt.setCarrierCode(value.getCarrierCode()); attempt.setAccessMode(value.getAccessMode());
|
||||
attempt.setOfficialUrl(value.getOfficialUrl()); attempt.setAttemptStatus(outcome);
|
||||
boolean manual = "MANUAL_REQUIRED".equals(outcome);
|
||||
attempt.setCoverageGap(manual); attempt.setCoverageGapCode(manual ? trim(errorCode) : null);
|
||||
attempt.setManualReason(manual ? trim(errorMessage) : null); attempt.setErrorCode(trim(errorCode));
|
||||
attempt.setErrorMessage(trim(errorMessage)); attempt.setOperatorId(0L);
|
||||
attempt.setOperatorName("carrier-scheduler"); repository.insert(attempt);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail create(long operatorId, String operatorName, String key, CreateRequest request) {
|
||||
String operation = "CREATE";
|
||||
@@ -309,16 +370,24 @@ public class CarrierScheduleQueryService {
|
||||
|
||||
private Detail detail(CarrierScheduleQueryEntity value) {
|
||||
return new Detail(view(value, now()), repository.nodes(value.getId()).stream().map(this::node).toList(),
|
||||
repository.exceptions(value.getId()).stream().map(this::exception).toList(),
|
||||
repository.histories(value.getId()).stream().map(this::history).toList());
|
||||
}
|
||||
|
||||
private NodeFact node(CarrierScheduleNodeFactEntity value) {
|
||||
return new NodeFact(value.getId(), value.getProviderCode(), value.getProviderNodeCode(),
|
||||
return new NodeFact(value.getId(), value.getProviderCode(), value.getProviderEventId(), value.getProviderNodeCode(),
|
||||
value.getStandardNode(), value.getNodeSequence(), value.getObservedAt(), value.getEstimatedAt(),
|
||||
value.getActualAt(), value.getLocationCode(), value.getLocationName(), value.getSourceReference(),
|
||||
value.getRawPayloadHash(), value.getCreatedAt());
|
||||
}
|
||||
|
||||
private ExceptionFact exception(CarrierScheduleExceptionFactEntity value) {
|
||||
return new ExceptionFact(value.getId(), value.getProviderCode(), value.getProviderEventId(),
|
||||
value.getProviderStatus(), value.getExceptionCode(), Boolean.TRUE.equals(value.getActive()),
|
||||
value.getObservedAt(), value.getLocationCode(), value.getLocationName(), value.getSourceReference(),
|
||||
value.getRawPayloadHash(), value.getCreatedAt());
|
||||
}
|
||||
|
||||
private History history(CarrierScheduleQueryHistoryEntity value) {
|
||||
return new History(value.getId(), value.getAction(), value.getFromVersion(), value.getToVersion(),
|
||||
value.getReason(), value.getOperatorId(), value.getOperatorName(), value.getCreatedAt());
|
||||
@@ -332,6 +401,7 @@ public class CarrierScheduleQueryService {
|
||||
fact.setId(IdWorker.getId()); fact.setQueryId(queryId);
|
||||
fact.setEventKey(providerCode.trim().toUpperCase(Locale.ROOT) + ':' + providerEventId.trim());
|
||||
fact.setProviderCode(providerCode.trim().toUpperCase(Locale.ROOT));
|
||||
fact.setProviderEventId(providerEventId.trim());
|
||||
fact.setProviderNodeCode(providerNodeCode.trim()); fact.setStandardNode(node.name());
|
||||
fact.setNodeSequence(node.sequence()); fact.setObservedAt(observedAt); fact.setEstimatedAt(estimatedAt);
|
||||
fact.setActualAt(actualAt); fact.setLocationCode(trim(locationCode)); fact.setLocationName(trim(locationName));
|
||||
@@ -339,6 +409,21 @@ public class CarrierScheduleQueryService {
|
||||
return repository.insertIfAbsent(fact);
|
||||
}
|
||||
|
||||
private boolean addException(long queryId, String providerCode, String providerEventId,
|
||||
String providerStatus, CarrierScheduleExceptionCode code, boolean active,
|
||||
LocalDateTime observedAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash) {
|
||||
CarrierScheduleExceptionFactEntity fact = new CarrierScheduleExceptionFactEntity();
|
||||
fact.setId(IdWorker.getId()); fact.setQueryId(queryId);
|
||||
fact.setEventKey(providerCode.trim().toUpperCase(Locale.ROOT) + ':' + providerEventId.trim());
|
||||
fact.setProviderCode(providerCode.trim().toUpperCase(Locale.ROOT));
|
||||
fact.setProviderEventId(providerEventId.trim()); fact.setProviderStatus(providerStatus.trim());
|
||||
fact.setExceptionCode(code.name()); fact.setActive(active); fact.setObservedAt(observedAt);
|
||||
fact.setLocationCode(trim(locationCode)); fact.setLocationName(trim(locationName));
|
||||
fact.setSourceReference(trim(sourceReference)); fact.setRawPayloadHash(rawPayloadHash);
|
||||
return repository.insertIfAbsent(fact);
|
||||
}
|
||||
|
||||
private static QueryView view(CarrierScheduleQueryEntity value, LocalDateTime now) {
|
||||
boolean expired = value.getLastSuccessAt() == null || value.getExpiresAt() == null
|
||||
|| !value.getExpiresAt().isAfter(now);
|
||||
|
||||
Reference in New Issue
Block a user