feat(firstleg): add compliant carrier schedule queries
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
package com.baiyue.firstleg.portschedule.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.AttemptRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
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.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.QueryView;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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.util.List;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@Tag(name = "船公司官网查询任务")
|
||||
@RequestMapping("/api/platform/v1/carrier-schedule-queries")
|
||||
public class PlatformCarrierScheduleQueryController {
|
||||
@Resource private CarrierScheduleQueryService service;
|
||||
|
||||
@Operation(summary = "查询六家船公司合规适配策略")
|
||||
@GetMapping("/carriers")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:view')")
|
||||
public ApiResponse<List<CarrierDescriptor>> carriers(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.carriers());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||
public ApiResponse<PageResult<QueryView>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid Query query) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
if (query.customerId() != null) FirstlegScopeGuard.platform(user, query.customerId());
|
||||
return ApiResponse.success(service.page(query));
|
||||
}
|
||||
|
||||
@GetMapping("/{queryId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:view')")
|
||||
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long queryId) {
|
||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||
return ApiResponse.success(service.detail(queryId));
|
||||
}
|
||||
|
||||
@GetMapping("/{queryId}/history")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:view')")
|
||||
public ApiResponse<List<CarrierScheduleQueryHistoryEntity>> history(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long queryId) {
|
||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||
return ApiResponse.success(service.history(queryId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:create')")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_CREATE", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
description = "创建船司官网查询任务", captureBefore = false)
|
||||
public ApiResponse<Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody CreateRequest request) {
|
||||
FirstlegScopeGuard.platform(user, request.customerId());
|
||||
return ApiResponse.success(service.create(user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{queryId}/attempts")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:attempt')")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_ATTEMPT", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
resourceId = "#queryId", description = "执行船司官网查询策略")
|
||||
public ApiResponse<Detail> attempt(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long queryId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody AttemptRequest request) {
|
||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||
return ApiResponse.success(service.attempt(queryId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{queryId}/manual-results")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_RESULT", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
resourceId = "#queryId", description = "关联人工船期查询结果")
|
||||
public ApiResponse<Detail> manualResult(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long queryId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ManualResultRequest request) {
|
||||
FirstlegScopeGuard.platform(user, service.customerId(queryId));
|
||||
return ApiResponse.success(service.attachManualResult(queryId, user.userId(), user.username(), key, request));
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.firstleg.portschedule.dto;
|
||||
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class CarrierScheduleQueryDtos {
|
||||
private 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; }
|
||||
}
|
||||
|
||||
public record CreateRequest(@NotNull @Min(0) @Max(0) Integer expectedVersion,
|
||||
@NotNull @Positive Long customerId,
|
||||
@NotNull @Positive Long oceanBookingId,
|
||||
@Positive Long containerBookingId,
|
||||
@NotBlank @Size(max = 16) String carrierCode,
|
||||
@NotBlank @Size(max = 32) String trackingType,
|
||||
@NotBlank @Size(max = 128) String trackingNo,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record AttemptRequest(@NotNull @Min(0) Integer expectedVersion,
|
||||
@Size(max = 1000) String reason) {}
|
||||
|
||||
public record ManualResultRequest(@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotNull @Positive Long portScheduleId,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
|
||||
public record CarrierDescriptor(String carrierCode, String carrierName, String officialUrl,
|
||||
String accessMode, boolean automated, boolean coverageGap,
|
||||
String coverageGapCode, String manualReason) {}
|
||||
|
||||
public record QueryView(CarrierScheduleQueryEntity query, boolean expired,
|
||||
int maxAttempts, LocalDateTime nextAllowedAt) {}
|
||||
|
||||
public record Detail(QueryView query, List<CarrierScheduleQueryAttemptEntity> attempts,
|
||||
List<CarrierScheduleQueryHistoryEntity> histories) {}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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_query_attempt")
|
||||
public class CarrierScheduleQueryAttemptEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long queryId;
|
||||
private Integer windowAttemptNo;
|
||||
private String carrierCode;
|
||||
private String accessMode;
|
||||
private String officialUrl;
|
||||
private String attemptStatus;
|
||||
private Boolean coverageGap;
|
||||
private String coverageGapCode;
|
||||
private String manualReason;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private Long operatorId;
|
||||
private String operatorName;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
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 com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_carrier_schedule_query")
|
||||
public class CarrierScheduleQueryEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String queryNo;
|
||||
private Long customerId;
|
||||
private Long oceanBookingId;
|
||||
private Long containerBookingId;
|
||||
private String carrierCode;
|
||||
private String trackingType;
|
||||
private String trackingNo;
|
||||
private String officialUrl;
|
||||
private String accessMode;
|
||||
private String status;
|
||||
private LocalDateTime windowStartedAt;
|
||||
private Integer attemptCount;
|
||||
private LocalDateTime nextAllowedAt;
|
||||
private LocalDateTime lastAttemptAt;
|
||||
private LocalDateTime lastSuccessAt;
|
||||
private LocalDateTime sourceUpdatedAt;
|
||||
private LocalDateTime expiresAt;
|
||||
private Long resultPortScheduleId;
|
||||
private String lastAttemptStatus;
|
||||
private String manualReason;
|
||||
private String lastErrorCode;
|
||||
private String lastErrorMessage;
|
||||
private String remark;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
private Long updatedBy;
|
||||
private String updatedByName;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
|
||||
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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_query_history")
|
||||
public class CarrierScheduleQueryHistoryEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long queryId;
|
||||
private String action;
|
||||
private Integer fromVersion;
|
||||
private Integer toVersion;
|
||||
private String reason;
|
||||
private String beforeSnapshot;
|
||||
private String afterSnapshot;
|
||||
private Long operatorId;
|
||||
private String operatorName;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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_query_operation")
|
||||
public class CarrierScheduleQueryOperationEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long queryId;
|
||||
private Long operatorId;
|
||||
private String operation;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String status;
|
||||
private String resultJson;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
private LocalDateTime completedAt;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.firstleg.portschedule.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleQueryAttemptMapper extends BaseMapper<CarrierScheduleQueryAttemptEntity> {}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.firstleg.portschedule.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleQueryHistoryMapper extends BaseMapper<CarrierScheduleQueryHistoryEntity> {}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.baiyue.firstleg.portschedule.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleQueryMapper extends BaseMapper<CarrierScheduleQueryEntity> {
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT * FROM fl_carrier_schedule_query
|
||||
WHERE (#{query.customerId} IS NULL OR customer_id=#{query.customerId})
|
||||
AND (#{query.oceanBookingId} IS NULL OR ocean_booking_id=#{query.oceanBookingId})
|
||||
AND (#{query.containerBookingId} IS NULL OR container_booking_id=#{query.containerBookingId})
|
||||
AND (#{query.carrierCode} IS NULL OR carrier_code=UPPER(#{query.carrierCode}))
|
||||
AND (#{query.status} IS NULL OR status=UPPER(#{query.status}))
|
||||
AND (#{query.expired} IS NULL
|
||||
OR (#{query.expired}=TRUE AND (last_success_at IS NULL OR expires_at IS NULL OR expires_at<=CURRENT_TIMESTAMP(3)))
|
||||
OR (#{query.expired}=FALSE AND last_success_at IS NOT NULL AND expires_at>CURRENT_TIMESTAMP(3)))
|
||||
AND (#{query.keyword} IS NULL OR query_no LIKE CONCAT('%',#{query.keyword},'%')
|
||||
OR tracking_no LIKE CONCAT('%',#{query.keyword},'%'))
|
||||
ORDER BY updated_at DESC,id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<CarrierScheduleQueryEntity> selectPage(Page<CarrierScheduleQueryEntity> page,
|
||||
@Param("query") Query query);
|
||||
|
||||
@Select("SELECT * FROM fl_carrier_schedule_query WHERE id=#{id} FOR UPDATE")
|
||||
CarrierScheduleQueryEntity lockById(@Param("id") long id);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_carrier_schedule_query
|
||||
SET official_url=#{value.officialUrl},access_mode=#{value.accessMode},status=#{value.status},
|
||||
window_started_at=#{value.windowStartedAt},attempt_count=#{value.attemptCount},
|
||||
next_allowed_at=#{value.nextAllowedAt},last_attempt_at=#{value.lastAttemptAt},
|
||||
last_success_at=#{value.lastSuccessAt},source_updated_at=#{value.sourceUpdatedAt},
|
||||
expires_at=#{value.expiresAt},result_port_schedule_id=#{value.resultPortScheduleId},
|
||||
last_attempt_status=#{value.lastAttemptStatus},manual_reason=#{value.manualReason},
|
||||
last_error_code=#{value.lastErrorCode},last_error_message=#{value.lastErrorMessage},
|
||||
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),
|
||||
version=version+1
|
||||
WHERE id=#{value.id} AND customer_id=#{value.customerId} AND version=#{expectedVersion}
|
||||
""")
|
||||
int updateState(@Param("value") CarrierScheduleQueryEntity value,
|
||||
@Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.firstleg.portschedule.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryOperationEntity;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleQueryOperationMapper extends BaseMapper<CarrierScheduleQueryOperationEntity> {
|
||||
@Insert("""
|
||||
INSERT IGNORE INTO fl_carrier_schedule_query_operation
|
||||
(id,query_id,operator_id,operation,idempotency_key,request_hash,status,created_at)
|
||||
VALUES (#{id},#{queryId},#{operatorId},#{operation},#{key},#{hash},'PROCESSING',CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int claim(@Param("id") long id, @Param("queryId") Long queryId,
|
||||
@Param("operatorId") long operatorId, @Param("operation") String operation,
|
||||
@Param("key") String key, @Param("hash") String hash);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_carrier_schedule_query_operation
|
||||
SET query_id=#{queryId},status='SUCCESS',result_json=#{resultJson},completed_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE operator_id=#{operatorId} AND idempotency_key=#{key} AND status='PROCESSING'
|
||||
""")
|
||||
int complete(@Param("queryId") long queryId, @Param("operatorId") long operatorId,
|
||||
@Param("key") String key, @Param("resultJson") String resultJson);
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.baiyue.firstleg.portschedule.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
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.mapper.CarrierScheduleQueryAttemptMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryHistoryMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryMapper;
|
||||
import com.baiyue.firstleg.portschedule.mapper.CarrierScheduleQueryOperationMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class CarrierScheduleQueryRepository {
|
||||
@Resource private CarrierScheduleQueryMapper queries;
|
||||
@Resource private CarrierScheduleQueryAttemptMapper attempts;
|
||||
@Resource private CarrierScheduleQueryHistoryMapper histories;
|
||||
@Resource private CarrierScheduleQueryOperationMapper operations;
|
||||
|
||||
public PageResult<CarrierScheduleQueryEntity> page(Query query) {
|
||||
return PageResult.from(queries.selectPage(new Page<>(query.page(), query.size()), query));
|
||||
}
|
||||
public CarrierScheduleQueryEntity find(long id) { return queries.selectById(id); }
|
||||
public CarrierScheduleQueryEntity lock(long id) { return queries.lockById(id); }
|
||||
public void insert(CarrierScheduleQueryEntity value) { queries.insert(value); }
|
||||
public int updateState(CarrierScheduleQueryEntity value, int version, long operatorId, String operatorName) {
|
||||
return queries.updateState(value, version, operatorId, operatorName);
|
||||
}
|
||||
public List<CarrierScheduleQueryAttemptEntity> attempts(long id) {
|
||||
return attempts.selectList(new LambdaQueryWrapper<CarrierScheduleQueryAttemptEntity>()
|
||||
.eq(CarrierScheduleQueryAttemptEntity::getQueryId, id)
|
||||
.orderByAsc(CarrierScheduleQueryAttemptEntity::getCreatedAt)
|
||||
.orderByAsc(CarrierScheduleQueryAttemptEntity::getId));
|
||||
}
|
||||
public void insert(CarrierScheduleQueryAttemptEntity value) { attempts.insert(value); }
|
||||
public List<CarrierScheduleQueryHistoryEntity> histories(long id) {
|
||||
return histories.selectList(new LambdaQueryWrapper<CarrierScheduleQueryHistoryEntity>()
|
||||
.eq(CarrierScheduleQueryHistoryEntity::getQueryId, id)
|
||||
.orderByAsc(CarrierScheduleQueryHistoryEntity::getCreatedAt)
|
||||
.orderByAsc(CarrierScheduleQueryHistoryEntity::getId));
|
||||
}
|
||||
public void insert(CarrierScheduleQueryHistoryEntity value) { histories.insert(value); }
|
||||
public boolean claimOperation(Long queryId, long operatorId, String operation, String key, String hash) {
|
||||
return operations.claim(IdWorker.getId(), queryId, operatorId, operation, key, hash) == 1;
|
||||
}
|
||||
public CarrierScheduleQueryOperationEntity operation(long operatorId, String key) {
|
||||
return operations.selectOne(new LambdaQueryWrapper<CarrierScheduleQueryOperationEntity>()
|
||||
.eq(CarrierScheduleQueryOperationEntity::getOperatorId, operatorId)
|
||||
.eq(CarrierScheduleQueryOperationEntity::getIdempotencyKey, key));
|
||||
}
|
||||
public int completeOperation(long queryId, long operatorId, String key, String resultJson) {
|
||||
return operations.complete(queryId, operatorId, key, resultJson);
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.baiyue.firstleg.portschedule.service;
|
||||
|
||||
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 java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class CarrierScheduleAdapterRegistry {
|
||||
private static final Map<String, Definition> DEFINITIONS = definitions();
|
||||
|
||||
public List<CarrierDescriptor> descriptors() {
|
||||
return DEFINITIONS.values().stream().map(Definition::descriptor).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",
|
||||
definition.coverageGapCode(), definition.manualReason());
|
||||
}
|
||||
|
||||
public String normalize(String carrierCode) {
|
||||
return required(carrierCode).code();
|
||||
}
|
||||
|
||||
private static Definition required(String carrierCode) {
|
||||
String normalized = carrierCode == null ? "" : carrierCode.trim().toUpperCase(Locale.ROOT);
|
||||
Definition value = DEFINITIONS.get(normalized);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"carrierCode仅支持MATSON、SML、ZIM、COSCO、EMC或HEDE");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
return definition.officialUrl();
|
||||
}
|
||||
|
||||
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", "官网仅公开交互表单,当前需人工查询并录入结果"));
|
||||
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,必须人工查询"));
|
||||
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", "官网未提供稳定公开结构化接口,当前需人工查询并录入结果"));
|
||||
return Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
private static void add(Map<String, Definition> values, Definition value) { values.put(value.code(), value); }
|
||||
|
||||
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,
|
||||
coverageGapCode, manualReason);
|
||||
}
|
||||
}
|
||||
|
||||
public record PreparedAttempt(String carrierCode, String accessMode, String officialUrl,
|
||||
String status, String coverageGapCode, String manualReason) {}
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
package com.baiyue.firstleg.portschedule.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.AttemptRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
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.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.QueryView;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
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.PortScheduleEntity;
|
||||
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
|
||||
import com.baiyue.firstleg.portschedule.repository.PortScheduleRepository;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleAdapterRegistry.PreparedAttempt;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class CarrierScheduleQueryService {
|
||||
static final int MAX_ATTEMPTS = 3;
|
||||
static final int WINDOW_DAYS = 3;
|
||||
private static final Set<String> TRACKING_TYPES = Set.of("BOOKING_NO", "BILL_OF_LADING", "CONTAINER_NO");
|
||||
|
||||
@Resource private CarrierScheduleQueryRepository repository;
|
||||
@Resource private PortScheduleRepository portSchedules;
|
||||
@Resource private CarrierScheduleAdapterRegistry adapters;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
private Clock clock = Clock.systemDefaultZone();
|
||||
|
||||
public List<CarrierDescriptor> carriers() { return adapters.descriptors(); }
|
||||
|
||||
public PageResult<QueryView> page(Query query) {
|
||||
PageResult<CarrierScheduleQueryEntity> source = repository.page(query);
|
||||
LocalDateTime now = now();
|
||||
return new PageResult<>(source.records().stream().map(value -> view(value, now)).toList(),
|
||||
source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
public Detail detail(long id) { return detail(required(id)); }
|
||||
|
||||
public List<CarrierScheduleQueryHistoryEntity> history(long id) {
|
||||
required(id);
|
||||
return repository.histories(id);
|
||||
}
|
||||
|
||||
public long customerId(long id) { return required(id).getCustomerId(); }
|
||||
|
||||
@Transactional
|
||||
public Detail create(long operatorId, String operatorName, String key, CreateRequest request) {
|
||||
String operation = "CREATE";
|
||||
Detail replay = claimOrReplay(null, operatorId, operation, key, hash(operation, null, request));
|
||||
if (replay != null) return replay;
|
||||
validateLinks(request.customerId(), request.oceanBookingId(), request.containerBookingId());
|
||||
String carrierCode = adapters.normalize(request.carrierCode());
|
||||
String trackingType = normalizeTrackingType(request.trackingType());
|
||||
PreparedAttempt strategy = adapters.prepare(carrierCode, trackingType, request.trackingNo());
|
||||
LocalDateTime now = now();
|
||||
CarrierScheduleQueryEntity value = new CarrierScheduleQueryEntity();
|
||||
value.setId(IdWorker.getId()); value.setQueryNo("CSQ" + value.getId());
|
||||
value.setCustomerId(request.customerId()); value.setOceanBookingId(request.oceanBookingId());
|
||||
value.setContainerBookingId(request.containerBookingId()); value.setCarrierCode(carrierCode);
|
||||
value.setTrackingType(trackingType); value.setTrackingNo(request.trackingNo().trim());
|
||||
value.setOfficialUrl(strategy.officialUrl()); value.setAccessMode(strategy.accessMode());
|
||||
value.setStatus("READY"); value.setWindowStartedAt(now); value.setAttemptCount(0);
|
||||
value.setNextAllowedAt(now); value.setManualReason(strategy.manualReason()); value.setRemark(trim(request.remark()));
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
|
||||
try { repository.insert(value); }
|
||||
catch (DuplicateKeyException ex) { throw conflict("同一订舱、柜次、船司和跟踪号的查询任务已存在"); }
|
||||
addHistory(value.getId(), operation, null, 0, request.remark(), null, value, operatorId, operatorName);
|
||||
return complete(value.getId(), operatorId, key, detail(value));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail attempt(long id, long operatorId, String operatorName, String key, AttemptRequest request) {
|
||||
String operation = "ATTEMPT";
|
||||
Detail replay = claimOrReplay(id, operatorId, operation, key, hash(operation, id, request));
|
||||
if (replay != null) return replay;
|
||||
CarrierScheduleQueryEntity before = requiredLockedVersion(id, request.expectedVersion());
|
||||
LocalDateTime now = now();
|
||||
LocalDateTime windowStart = before.getWindowStartedAt();
|
||||
if (windowStart == null || !now.isBefore(windowStart.plusDays(WINDOW_DAYS))) {
|
||||
windowStart = now;
|
||||
before.setAttemptCount(0);
|
||||
}
|
||||
if (before.getLastSuccessAt() != null && before.getNextAllowedAt() != null
|
||||
&& now.isBefore(before.getNextAllowedAt())) {
|
||||
throw conflict("船期查询每3天执行一次,下次允许时间:" + before.getNextAllowedAt());
|
||||
}
|
||||
int attemptCount = count(before.getAttemptCount());
|
||||
LocalDateTime windowEnd = windowStart.plusDays(WINDOW_DAYS);
|
||||
if (attemptCount >= MAX_ATTEMPTS && now.isBefore(windowEnd)) {
|
||||
throw conflict("当前3天窗口最多查询3次,下次允许时间:" + windowEnd);
|
||||
}
|
||||
PreparedAttempt strategy = adapters.prepare(before.getCarrierCode(), before.getTrackingType(), before.getTrackingNo());
|
||||
CarrierScheduleQueryEntity snapshot = copy(before);
|
||||
int nextCount = attemptCount + 1;
|
||||
before.setOfficialUrl(strategy.officialUrl()); before.setAccessMode(strategy.accessMode());
|
||||
before.setStatus(strategy.status()); before.setWindowStartedAt(windowStart); before.setAttemptCount(nextCount);
|
||||
before.setLastAttemptAt(now); before.setLastAttemptStatus(strategy.status());
|
||||
before.setManualReason(strategy.manualReason()); before.setLastErrorCode(strategy.coverageGapCode());
|
||||
before.setLastErrorMessage(strategy.manualReason());
|
||||
before.setNextAllowedAt(nextCount >= MAX_ATTEMPTS ? windowEnd : now);
|
||||
if (repository.updateState(before, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
|
||||
CarrierScheduleQueryEntity after = required(id);
|
||||
CarrierScheduleQueryAttemptEntity attempt = new CarrierScheduleQueryAttemptEntity();
|
||||
attempt.setId(IdWorker.getId()); attempt.setQueryId(id); attempt.setWindowAttemptNo(nextCount);
|
||||
attempt.setCarrierCode(after.getCarrierCode()); attempt.setAccessMode(strategy.accessMode());
|
||||
attempt.setOfficialUrl(strategy.officialUrl()); attempt.setAttemptStatus(strategy.status());
|
||||
attempt.setCoverageGap(true); attempt.setCoverageGapCode(strategy.coverageGapCode());
|
||||
attempt.setManualReason(strategy.manualReason()); attempt.setErrorCode(strategy.coverageGapCode());
|
||||
attempt.setErrorMessage(strategy.manualReason()); attempt.setOperatorId(operatorId); attempt.setOperatorName(operatorName);
|
||||
repository.insert(attempt);
|
||||
addHistory(id, operation, snapshot.getVersion(), after.getVersion(), request.reason(), snapshot, after,
|
||||
operatorId, operatorName);
|
||||
return complete(id, operatorId, key, detail(after));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail attachManualResult(long id, long operatorId, String operatorName, String key,
|
||||
ManualResultRequest request) {
|
||||
String operation = "MANUAL_RESULT";
|
||||
Detail replay = claimOrReplay(id, operatorId, operation, key, hash(operation, id, request));
|
||||
if (replay != null) return replay;
|
||||
CarrierScheduleQueryEntity value = requiredLockedVersion(id, request.expectedVersion());
|
||||
PortScheduleEntity schedule = portSchedules.find(request.portScheduleId());
|
||||
if (schedule == null) throw new BizException(ResultCode.NOT_FOUND, "人工船期结果不存在");
|
||||
if (!Objects.equals(schedule.getCustomerId(), value.getCustomerId())
|
||||
|| !Objects.equals(schedule.getOceanBookingId(), value.getOceanBookingId())
|
||||
|| !Objects.equals(schedule.getContainerBookingId(), value.getContainerBookingId())) {
|
||||
throw badRequest("人工船期结果与查询任务的客户、订舱或柜次不一致");
|
||||
}
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
LocalDateTime now = now();
|
||||
value.setStatus("SUCCESS"); value.setResultPortScheduleId(schedule.getId());
|
||||
value.setLastSuccessAt(now); value.setSourceUpdatedAt(schedule.getObservedAt());
|
||||
value.setExpiresAt(schedule.getExpiresAt()); value.setNextAllowedAt(now.plusDays(WINDOW_DAYS));
|
||||
value.setWindowStartedAt(now); value.setLastAttemptStatus("MANUAL_COMPLETED");
|
||||
value.setManualReason(trim(request.reason())); value.setLastErrorCode(null); value.setLastErrorMessage(null);
|
||||
if (repository.updateState(value, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
|
||||
CarrierScheduleQueryEntity after = required(id);
|
||||
addHistory(id, operation, before.getVersion(), after.getVersion(), request.reason(), before, after,
|
||||
operatorId, operatorName);
|
||||
return complete(id, operatorId, key, detail(after));
|
||||
}
|
||||
|
||||
private void validateLinks(long customerId, long oceanBookingId, Long containerBookingId) {
|
||||
OceanBookingEntity booking = portSchedules.oceanBooking(oceanBookingId);
|
||||
if (booking == null) throw new BizException(ResultCode.NOT_FOUND, "关联订舱不存在");
|
||||
if (!Objects.equals(booking.getCustomerId(), customerId)) throw badRequest("customerId与订舱客户不一致");
|
||||
if (containerBookingId != null) {
|
||||
if (portSchedules.containerBooking(containerBookingId) == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "关联柜次不存在");
|
||||
}
|
||||
if (portSchedules.allocation(oceanBookingId, containerBookingId) == null) {
|
||||
throw badRequest("containerBookingId未分配至该订舱");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Detail claimOrReplay(Long id, long operatorId, String operation, String key, String requestHash) {
|
||||
validateKey(key);
|
||||
if (repository.claimOperation(id, operatorId, operation, key, requestHash)) return null;
|
||||
CarrierScheduleQueryOperationEntity existing = repository.operation(operatorId, key);
|
||||
if (existing == null || !requestHash.equals(existing.getRequestHash())) throw conflict("幂等键已用于其他请求");
|
||||
if (!"SUCCESS".equals(existing.getStatus()) || !StringUtils.hasText(existing.getResultJson())) {
|
||||
throw conflict("相同请求正在处理中,请稍后重试");
|
||||
}
|
||||
try { return objectMapper.readValue(existing.getResultJson(), Detail.class); }
|
||||
catch (JsonProcessingException ex) { throw new IllegalStateException("无法读取船司查询幂等结果", ex); }
|
||||
}
|
||||
|
||||
private Detail complete(long id, long operatorId, String key, Detail result) {
|
||||
try {
|
||||
if (repository.completeOperation(id, operatorId, key, objectMapper.writeValueAsString(result)) != 1) {
|
||||
throw conflict("船司查询幂等请求完成状态冲突");
|
||||
}
|
||||
return result;
|
||||
} catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存船司查询幂等结果", ex); }
|
||||
}
|
||||
|
||||
private void addHistory(long id, String action, Integer fromVersion, Integer toVersion, String reason,
|
||||
Object before, Object after, long operatorId, String operatorName) {
|
||||
CarrierScheduleQueryHistoryEntity history = new CarrierScheduleQueryHistoryEntity();
|
||||
history.setId(IdWorker.getId()); history.setQueryId(id); history.setAction(action);
|
||||
history.setFromVersion(fromVersion); history.setToVersion(toVersion); history.setReason(trim(reason));
|
||||
history.setBeforeSnapshot(json(before)); history.setAfterSnapshot(json(after));
|
||||
history.setOperatorId(operatorId); history.setOperatorName(operatorName); repository.insert(history);
|
||||
}
|
||||
|
||||
private Detail detail(CarrierScheduleQueryEntity value) {
|
||||
return new Detail(view(value, now()), repository.attempts(value.getId()), repository.histories(value.getId()));
|
||||
}
|
||||
|
||||
private static QueryView view(CarrierScheduleQueryEntity value, LocalDateTime now) {
|
||||
boolean expired = value.getLastSuccessAt() == null || value.getExpiresAt() == null
|
||||
|| !value.getExpiresAt().isAfter(now);
|
||||
return new QueryView(value, expired, MAX_ATTEMPTS, value.getNextAllowedAt());
|
||||
}
|
||||
|
||||
private CarrierScheduleQueryEntity required(long id) {
|
||||
CarrierScheduleQueryEntity value = repository.find(id);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "船司查询任务不存在");
|
||||
return value;
|
||||
}
|
||||
|
||||
private CarrierScheduleQueryEntity requiredLockedVersion(long id, int version) {
|
||||
CarrierScheduleQueryEntity value = repository.lock(id);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "船司查询任务不存在");
|
||||
if (!Integer.valueOf(version).equals(value.getVersion())) throw concurrent();
|
||||
return value;
|
||||
}
|
||||
|
||||
private static CarrierScheduleQueryEntity copy(CarrierScheduleQueryEntity source) {
|
||||
CarrierScheduleQueryEntity value = new CarrierScheduleQueryEntity();
|
||||
value.setId(source.getId()); value.setQueryNo(source.getQueryNo()); value.setCustomerId(source.getCustomerId());
|
||||
value.setOceanBookingId(source.getOceanBookingId()); value.setContainerBookingId(source.getContainerBookingId());
|
||||
value.setCarrierCode(source.getCarrierCode()); value.setTrackingType(source.getTrackingType());
|
||||
value.setTrackingNo(source.getTrackingNo()); value.setOfficialUrl(source.getOfficialUrl());
|
||||
value.setAccessMode(source.getAccessMode()); value.setStatus(source.getStatus());
|
||||
value.setWindowStartedAt(source.getWindowStartedAt()); value.setAttemptCount(source.getAttemptCount());
|
||||
value.setNextAllowedAt(source.getNextAllowedAt()); value.setLastAttemptAt(source.getLastAttemptAt());
|
||||
value.setLastSuccessAt(source.getLastSuccessAt()); value.setSourceUpdatedAt(source.getSourceUpdatedAt());
|
||||
value.setExpiresAt(source.getExpiresAt()); value.setResultPortScheduleId(source.getResultPortScheduleId());
|
||||
value.setLastAttemptStatus(source.getLastAttemptStatus()); value.setManualReason(source.getManualReason());
|
||||
value.setLastErrorCode(source.getLastErrorCode()); value.setLastErrorMessage(source.getLastErrorMessage());
|
||||
value.setRemark(source.getRemark()); value.setVersion(source.getVersion());
|
||||
return value;
|
||||
}
|
||||
|
||||
private String hash(String operation, Long id, Object request) {
|
||||
try {
|
||||
byte[] bytes = (operation + '|' + id + '|' + objectMapper.writeValueAsString(request))
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||
} catch (Exception ex) { throw new IllegalStateException("无法计算船司查询请求摘要", ex); }
|
||||
}
|
||||
|
||||
private String json(Object value) {
|
||||
if (value == null) return null;
|
||||
try { return objectMapper.writeValueAsString(value); }
|
||||
catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存船司查询历史快照", ex); }
|
||||
}
|
||||
|
||||
private static String normalizeTrackingType(String value) {
|
||||
String normalized = value.trim().toUpperCase(Locale.ROOT);
|
||||
if (!TRACKING_TYPES.contains(normalized)) {
|
||||
throw badRequest("trackingType仅支持BOOKING_NO、BILL_OF_LADING或CONTAINER_NO");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private LocalDateTime now() { return LocalDateTime.now(clock); }
|
||||
private static int count(Integer value) { return value == null ? 0 : value; }
|
||||
private static void validateKey(String key) {
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw badRequest("Idempotency-Key不能为空且不能超过128个字符");
|
||||
}
|
||||
private static String trim(String value) {
|
||||
if (value == null) return null;
|
||||
String result = value.trim();
|
||||
return result.isEmpty() ? null : result;
|
||||
}
|
||||
private static BizException badRequest(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
private static BizException concurrent() { return conflict("数据已被其他人修改,请刷新后重试"); }
|
||||
}
|
||||
Reference in New Issue
Block a user