fix(firstleg): decouple manual container nodes from tracking
This commit is contained in:
+1
-23
@@ -14,9 +14,6 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.QueryView;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.History;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -44,7 +41,6 @@ import java.util.List;
|
||||
@RequestMapping("/api/platform/v1/carrier-schedule-queries")
|
||||
public class PlatformCarrierScheduleQueryController {
|
||||
@Resource private CarrierScheduleQueryService service;
|
||||
@Resource private OutboundDispatchService outboundDispatchService;
|
||||
|
||||
@Operation(summary = "查询六家船公司合规适配策略")
|
||||
@GetMapping("/carriers")
|
||||
@@ -101,23 +97,6 @@ public class PlatformCarrierScheduleQueryController {
|
||||
return ApiResponse.success(service.create(user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@PostMapping("/containers/{containerBookingId}/ensure")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@Operation(summary = "为已确认离仓货柜补建船节点任务后进入人工补录")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_ENSURE", resourceType = "CONTAINER_BOOKING",
|
||||
resourceId = "#containerBookingId", description = "补建货柜船节点任务")
|
||||
public ApiResponse<Detail> ensureForManualEntry(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId,
|
||||
@RequestHeader("Idempotency-Key") String key) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
var outbound = outboundDispatchService.detail(containerBookingId).aggregate();
|
||||
if (!"WAITING_FOR_LOADING".equals(outbound.dispatchStatus()) || outbound.oceanBookingId() == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "货柜尚未确认国内出仓,不能人工补录船节点");
|
||||
}
|
||||
return ApiResponse.success(service.ensureForManualEntry(outbound.oceanBookingId(), containerBookingId,
|
||||
user.userId(), user.username(), key));
|
||||
}
|
||||
|
||||
@PostMapping("/{queryId}/manual-results")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_RESULT", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
@@ -132,8 +111,7 @@ public class PlatformCarrierScheduleQueryController {
|
||||
|
||||
@PostMapping("/{queryId}/manual-nodes")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@Operation(summary = "人工补录预计或实际标准船节点",
|
||||
description = "预计节点不推进实际进度;人工实际节点必须严格晚于当前最新实际节点,同级或更早节点返回 A0409。")
|
||||
@Operation(summary = "在跟踪任务内人工补录预计或实际标准船节点")
|
||||
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_NODE", resourceType = "CARRIER_SCHEDULE_QUERY",
|
||||
resourceId = "#queryId", description = "人工补录船节点事实")
|
||||
public ApiResponse<Detail> manualNode(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.portschedule.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerNodeDetail;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
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.*;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/container-shipping-nodes")
|
||||
public class PlatformContainerShippingNodeController {
|
||||
@Resource private CarrierScheduleQueryService service;
|
||||
|
||||
@GetMapping("/{containerBookingId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||
public ApiResponse<ContainerNodeDetail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.containerNodeDetail(containerBookingId));
|
||||
}
|
||||
|
||||
@PostMapping("/{containerBookingId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
|
||||
@BusinessAudit(event = "CONTAINER_SHIPPING_NODE_MANUAL", resourceType = "CONTAINER_BOOKING",
|
||||
resourceId = "#containerBookingId", description = "人工修改货柜当前船节点")
|
||||
public ApiResponse<ContainerNodeDetail> add(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ContainerManualNodeRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.addContainerManualNode(containerBookingId, user.userId(),
|
||||
user.username(), key, request));
|
||||
}
|
||||
}
|
||||
+11
@@ -43,6 +43,14 @@ public final class CarrierScheduleQueryDtos {
|
||||
@Size(max = 128) String locationName,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
|
||||
public record ContainerManualNodeRequest(@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,
|
||||
@@ -97,6 +105,9 @@ public final class CarrierScheduleQueryDtos {
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash, LocalDateTime createdAt) {}
|
||||
|
||||
public record ContainerNodeDetail(Long containerBookingId, String currentNode,
|
||||
LocalDateTime currentNodeAt, List<NodeFact> nodes) {}
|
||||
|
||||
public record ExceptionFact(Long id, String providerCode, String providerEventId,
|
||||
String providerStatus, String exceptionCode, boolean active,
|
||||
LocalDateTime observedAt, String locationCode, String locationName,
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ import java.time.LocalDateTime;
|
||||
public class CarrierScheduleNodeFactEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private Long queryId;
|
||||
private Long containerBookingId;
|
||||
private String eventKey;
|
||||
private String providerCode;
|
||||
private String providerEventId;
|
||||
|
||||
+12
-2
@@ -5,19 +5,29 @@ import com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface CarrierScheduleNodeFactMapper extends BaseMapper<CarrierScheduleNodeFactEntity> {
|
||||
@Insert("""
|
||||
INSERT INTO fl_carrier_schedule_node_fact
|
||||
(id,query_id,event_key,provider_code,provider_event_id,provider_node_code,standard_node,node_sequence,observed_at,
|
||||
(id,query_id,container_booking_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.providerEventId},#{value.providerNodeCode},
|
||||
(#{value.id},#{value.queryId},#{value.containerBookingId},#{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))
|
||||
ON DUPLICATE KEY UPDATE id=id
|
||||
""")
|
||||
int insertIgnore(@Param("value") CarrierScheduleNodeFactEntity value);
|
||||
|
||||
@Select("SELECT * FROM fl_carrier_schedule_node_fact WHERE container_booking_id=#{containerBookingId} ORDER BY observed_at,id")
|
||||
List<CarrierScheduleNodeFactEntity> selectByContainer(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@Select("SELECT * FROM fl_carrier_schedule_node_fact WHERE container_booking_id=#{containerBookingId} AND event_key=#{eventKey} LIMIT 1")
|
||||
CarrierScheduleNodeFactEntity selectByContainerEvent(@Param("containerBookingId") long containerBookingId,
|
||||
@Param("eventKey") String eventKey);
|
||||
}
|
||||
|
||||
+14
-14
@@ -49,26 +49,26 @@ public interface CarrierScheduleQueryMapper extends BaseMapper<CarrierScheduleQu
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT q.container_booking_id AS containerBookingId,q.id AS queryId,q.version AS queryVersion,
|
||||
q.status AS queryStatus,q.latest_standard_node AS latestActualNode,
|
||||
q.latest_node_observed_at AS latestActualAt,
|
||||
SELECT b.id AS containerBookingId,q.id AS queryId,q.version AS queryVersion,
|
||||
q.status AS queryStatus,current.standard_node AS latestActualNode,
|
||||
current.actual_at AS latestActualAt,
|
||||
MAX(CASE WHEN f.standard_node='DEPARTED_WAREHOUSE' THEN f.actual_at END) AS departedWarehouseAt,
|
||||
MAX(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT' THEN f.actual_at END) AS departedOriginPortAt,
|
||||
MAX(CASE WHEN f.standard_node='ARRIVED_DESTINATION_PORT' THEN f.actual_at END) AS arrivedDestinationPortAt,
|
||||
MAX(CASE WHEN f.standard_node='PICKUP_READY' THEN f.actual_at END) AS pickupReadyAt,
|
||||
MAX(CASE WHEN f.standard_node='PICKED_UP' THEN f.actual_at END) AS pickedUpAt,
|
||||
MAX(CASE WHEN f.standard_node='EMPTY_RETURNED' THEN f.actual_at END) AS emptyReturnedAt
|
||||
FROM fl_carrier_schedule_query q
|
||||
JOIN (
|
||||
SELECT container_booking_id,MAX(id) AS query_id
|
||||
FROM fl_carrier_schedule_query
|
||||
WHERE container_booking_id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY container_booking_id
|
||||
) latest ON latest.query_id=q.id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact f ON f.query_id=q.id AND f.actual_at IS NOT NULL
|
||||
GROUP BY q.container_booking_id,q.id,q.version,q.status,q.latest_standard_node,q.latest_node_observed_at
|
||||
ORDER BY q.container_booking_id
|
||||
FROM fl_container_booking b
|
||||
JOIN fl_carrier_schedule_node_fact current ON current.id=(
|
||||
SELECT f2.id FROM fl_carrier_schedule_node_fact f2
|
||||
WHERE f2.container_booking_id=b.id AND f2.actual_at IS NOT NULL
|
||||
ORDER BY f2.observed_at DESC,f2.id DESC LIMIT 1)
|
||||
LEFT JOIN fl_carrier_schedule_query q ON q.id=current.query_id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact f ON f.container_booking_id=b.id AND f.actual_at IS NOT NULL
|
||||
WHERE b.id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY b.id,q.id,q.version,q.status,current.standard_node,current.actual_at
|
||||
ORDER BY b.id
|
||||
</script>
|
||||
""")
|
||||
List<ContainerActualFacts> selectContainerActualFacts(
|
||||
|
||||
+6
@@ -79,6 +79,12 @@ public class CarrierScheduleQueryRepository {
|
||||
.eq(CarrierScheduleNodeFactEntity::getQueryId, queryId)
|
||||
.eq(CarrierScheduleNodeFactEntity::getEventKey, eventKey));
|
||||
}
|
||||
public List<CarrierScheduleNodeFactEntity> nodesByContainer(long containerBookingId) {
|
||||
return nodes.selectByContainer(containerBookingId);
|
||||
}
|
||||
public CarrierScheduleNodeFactEntity nodeByContainer(long containerBookingId, String eventKey) {
|
||||
return nodes.selectByContainerEvent(containerBookingId, eventKey);
|
||||
}
|
||||
public boolean hasActualNode(long queryId, String standardNode) {
|
||||
return nodes.selectCount(new LambdaQueryWrapper<CarrierScheduleNodeFactEntity>()
|
||||
.eq(CarrierScheduleNodeFactEntity::getQueryId, queryId)
|
||||
|
||||
+18
-6
@@ -42,28 +42,40 @@ public class CarrierScheduleNodeOutboxService {
|
||||
|| !normalizedContainerNo.equals(query.getTrackingNo())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "船节点任务跟踪柜号与跨区事件柜号不一致");
|
||||
}
|
||||
publish(query.getId(), query.getOceanBookingId(), query.getContainerBookingId(), query.getVersion(),
|
||||
fact, normalizedContainerNo, "CARRIER_NODE:" + query.getId() + ':' + fact.getId());
|
||||
}
|
||||
|
||||
public void publishContainerNode(Long oceanBookingId, long containerBookingId,
|
||||
CarrierScheduleNodeFactEntity fact, String containerNo) {
|
||||
if (fact.getActualAt() == null) return;
|
||||
String normalizedContainerNo = ContainerNumbers.normalizeRequired(containerNo);
|
||||
publish(null, oceanBookingId, containerBookingId, 0, fact, normalizedContainerNo,
|
||||
"CONTAINER_NODE:" + containerBookingId + ':' + fact.getId());
|
||||
}
|
||||
|
||||
private void publish(Long queryId, Long oceanBookingId, Long containerBookingId, Integer version,
|
||||
CarrierScheduleNodeFactEntity fact, String normalizedContainerNo, String eventKey) {
|
||||
ZoneId zone = ZoneId.of(scheduleZone);
|
||||
var payload = new FirstLegContainerShippingNodeRecordedV1(
|
||||
string(query.getId()), string(query.getOceanBookingId()),
|
||||
string(query.getContainerBookingId()), normalizedContainerNo,
|
||||
string(queryId), string(oceanBookingId), string(containerBookingId), normalizedContainerNo,
|
||||
fact.getStandardNode(), fact.getNodeSequence(),
|
||||
fact.getObservedAt().atZone(zone).toInstant(), fact.getActualAt().atZone(zone).toInstant(),
|
||||
fact.getLocationCode(), fact.getLocationName(), fact.getProviderCode(), fact.getProviderEventId(),
|
||||
fact.getProviderNodeCode(), fact.getSourceReference(), fact.getRawPayloadHash());
|
||||
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||
String payloadHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||
long aggregateVersion = Math.max(0, query.getVersion() == null ? 0 : query.getVersion());
|
||||
String eventKey = "CARRIER_NODE:" + query.getId() + ':' + fact.getId();
|
||||
long aggregateVersion = Math.max(0, version == null ? 0 : version);
|
||||
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(),
|
||||
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED, 1,
|
||||
fact.getActualAt().atZone(zone).toInstant(), RegionCode.CN, RegionCode.OVERSEAS,
|
||||
"0", "CONTAINER_BOOKING", string(query.getContainerBookingId()),
|
||||
"0", "CONTAINER_BOOKING", string(containerBookingId),
|
||||
aggregateVersion, aggregateVersion + 1, EventIdentifiers.newUuid(), eventKey, null,
|
||||
payloadNode, payloadHash).requireValid();
|
||||
|
||||
InboundOutboxEntity row = new InboundOutboxEntity();
|
||||
row.setId(IdWorker.getId()); row.setTenantId(0L);
|
||||
row.setAggregateType(envelope.aggregateType()); row.setAggregateId(query.getContainerBookingId());
|
||||
row.setAggregateType(envelope.aggregateType()); row.setAggregateId(containerBookingId);
|
||||
row.setEventType(envelope.eventType()); row.setEventKey(eventKey); row.setEventVersion(1);
|
||||
row.setAggregateSequence(Math.toIntExact(aggregateVersion)); row.setPayloadHash(payloadHash);
|
||||
row.setPayloadJson(json(envelope)); row.setPublishStatus("PENDING"); row.setRetryCount(0);
|
||||
|
||||
+54
-20
@@ -8,6 +8,8 @@ import com.baiyue.common.event.ContainerNumbers;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerNodeDetail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
@@ -48,6 +50,7 @@ import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -147,7 +150,7 @@ public class CarrierScheduleQueryService {
|
||||
throw exception;
|
||||
}
|
||||
persistenceStage = "NODE_FACT";
|
||||
CarrierScheduleNodeFactEntity departed = addNode(value.getId(), "FIRSTLEG", "OUTBOUND_CONFIRM", "OUTBOUND_CONFIRM",
|
||||
CarrierScheduleNodeFactEntity departed = addNode(value, "FIRSTLEG", "OUTBOUND_CONFIRM", "OUTBOUND_CONFIRM",
|
||||
CarrierScheduleNode.DEPARTED_WAREHOUSE, now, null, now, null, null, "OUTBOUND_CONFIRM",
|
||||
sha256("OUTBOUND_CONFIRM|" + containerBookingId));
|
||||
if (departed != null) {
|
||||
@@ -164,13 +167,6 @@ public class CarrierScheduleQueryService {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail ensureForManualEntry(long oceanBookingId, long containerBookingId,
|
||||
long operatorId, String operatorName, String key) {
|
||||
validateKey(key);
|
||||
return detail(ensureFromOutboundConfirm(null, oceanBookingId, containerBookingId, operatorId, operatorName));
|
||||
}
|
||||
|
||||
private static String outboundManualReason(PreparedAttempt strategy, OceanBookingEntity booking) {
|
||||
if (!"MANUAL_REQUIRED".equals(strategy.status())) return null;
|
||||
String reason = strategy.manualReason();
|
||||
@@ -185,7 +181,7 @@ public class CarrierScheduleQueryService {
|
||||
CarrierScheduleQueryEntity value = requiredLocked(id);
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(id, request.providerCode(), request.providerEventId(),
|
||||
CarrierScheduleNodeFactEntity fact = addNode(value, request.providerCode(), request.providerEventId(),
|
||||
request.providerNodeCode(), node,
|
||||
request.observedAt(), request.estimatedAt(), request.actualAt(), request.locationCode(),
|
||||
request.locationName(), request.sourceReference(), request.rawPayloadHash());
|
||||
@@ -226,10 +222,9 @@ public class CarrierScheduleQueryService {
|
||||
}
|
||||
CarrierScheduleQueryEntity value = requiredLockedVersion(id, request.expectedVersion());
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
requireManualActualAdvance(value, node, request.actualAt());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
String providerEventId = sha256("MANUAL|" + id + '|' + key);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(id, "MANUAL", providerEventId, node.name(), node,
|
||||
CarrierScheduleNodeFactEntity fact = addNode(value, "MANUAL", providerEventId, node.name(), node,
|
||||
request.observedAt(),
|
||||
request.estimatedAt(), request.actualAt(), request.locationCode(), request.locationName(),
|
||||
"MANUAL:" + operatorId, hash(operation, id, request));
|
||||
@@ -248,13 +243,42 @@ public class CarrierScheduleQueryService {
|
||||
return complete(id, operatorId, key, detail(after));
|
||||
}
|
||||
|
||||
private void requireManualActualAdvance(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
|
||||
LocalDateTime actualAt) {
|
||||
if (actualAt == null || value.getLatestStandardNode() == null) return;
|
||||
CarrierScheduleNode current = CarrierScheduleNode.require(value.getLatestStandardNode());
|
||||
if (node.sequence() <= current.sequence()) {
|
||||
throw conflict("人工实际节点必须严格晚于当前最新实际节点");
|
||||
public ContainerNodeDetail containerNodeDetail(long containerBookingId) {
|
||||
if (portSchedules.containerBooking(containerBookingId) == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
}
|
||||
return containerNodeDetail(containerBookingId, repository.nodesByContainer(containerBookingId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerNodeDetail addContainerManualNode(long containerBookingId, long operatorId,
|
||||
String operatorName, String key,
|
||||
ContainerManualNodeRequest request) {
|
||||
validateKey(key);
|
||||
if (request.estimatedAt() == null && request.actualAt() == null) {
|
||||
throw badRequest("estimatedAt和actualAt至少提供一个");
|
||||
}
|
||||
ContainerBookingEntity container = portSchedules.containerBooking(containerBookingId);
|
||||
if (container == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
String providerEventId = sha256("CONTAINER_MANUAL|" + containerBookingId + '|' + key);
|
||||
String payloadHash = hash("CONTAINER_MANUAL_NODE", containerBookingId, request);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(null, containerBookingId, "MANUAL", providerEventId,
|
||||
node.name(), node, request.observedAt(), request.estimatedAt(), request.actualAt(),
|
||||
request.locationCode(), request.locationName(), "MANUAL:" + operatorId, payloadHash);
|
||||
if (fact != null) nodeOutbox.publishContainerNode(container.getOceanBookingId(), containerBookingId,
|
||||
fact, normalizedContainerNo(container));
|
||||
return containerNodeDetail(containerBookingId, repository.nodesByContainer(containerBookingId));
|
||||
}
|
||||
|
||||
private ContainerNodeDetail containerNodeDetail(long containerBookingId,
|
||||
List<CarrierScheduleNodeFactEntity> facts) {
|
||||
CarrierScheduleNodeFactEntity current = facts.stream()
|
||||
.max(Comparator.comparing(CarrierScheduleNodeFactEntity::getObservedAt)
|
||||
.thenComparing(CarrierScheduleNodeFactEntity::getId)).orElse(null);
|
||||
return new ContainerNodeDetail(containerBookingId, current == null ? null : current.getStandardNode(),
|
||||
current == null ? null : current.getActualAt() != null ? current.getActualAt() : current.getEstimatedAt(),
|
||||
facts.stream().map(this::node).toList());
|
||||
}
|
||||
|
||||
private boolean advanceActualNode(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
|
||||
@@ -500,13 +524,23 @@ public class CarrierScheduleQueryService {
|
||||
value.getReason(), value.getOperatorId(), value.getOperatorName(), value.getCreatedAt());
|
||||
}
|
||||
|
||||
private CarrierScheduleNodeFactEntity addNode(long queryId, String providerCode, String providerEventId,
|
||||
private CarrierScheduleNodeFactEntity addNode(CarrierScheduleQueryEntity query, String providerCode, String providerEventId,
|
||||
String providerNodeCode,
|
||||
CarrierScheduleNode node, LocalDateTime observedAt, LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash) {
|
||||
return addNode(query.getId(), query.getContainerBookingId(), providerCode, providerEventId,
|
||||
providerNodeCode, node, observedAt, estimatedAt, actualAt, locationCode, locationName,
|
||||
sourceReference, rawPayloadHash);
|
||||
}
|
||||
|
||||
private CarrierScheduleNodeFactEntity addNode(Long queryId, long containerBookingId,
|
||||
String providerCode, String providerEventId, String providerNodeCode,
|
||||
CarrierScheduleNode node, LocalDateTime observedAt, LocalDateTime estimatedAt,
|
||||
LocalDateTime actualAt, String locationCode, String locationName,
|
||||
String sourceReference, String rawPayloadHash) {
|
||||
CarrierScheduleNodeFactEntity fact = new CarrierScheduleNodeFactEntity();
|
||||
fact.setId(IdWorker.getId()); fact.setQueryId(queryId);
|
||||
fact.setId(IdWorker.getId()); fact.setQueryId(queryId); fact.setContainerBookingId(containerBookingId);
|
||||
fact.setEventKey(providerCode.trim().toUpperCase(Locale.ROOT) + ':' + providerEventId.trim());
|
||||
fact.setProviderCode(providerCode.trim().toUpperCase(Locale.ROOT));
|
||||
fact.setProviderEventId(providerEventId.trim());
|
||||
@@ -515,7 +549,7 @@ public class CarrierScheduleQueryService {
|
||||
fact.setActualAt(actualAt); fact.setLocationCode(trim(locationCode)); fact.setLocationName(trim(locationName));
|
||||
fact.setSourceReference(trim(sourceReference)); fact.setRawPayloadHash(rawPayloadHash);
|
||||
if (repository.insertIfAbsent(fact)) return fact;
|
||||
CarrierScheduleNodeFactEntity existing = repository.node(queryId, fact.getEventKey());
|
||||
CarrierScheduleNodeFactEntity existing = repository.nodeByContainer(containerBookingId, fact.getEventKey());
|
||||
if (existing != null && !Objects.equals(existing.getRawPayloadHash(), rawPayloadHash)) {
|
||||
throw conflict("相同providerEventId对应不同rawPayloadHash,拒绝覆盖既有节点事实");
|
||||
}
|
||||
|
||||
+11
-1
@@ -18,7 +18,11 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
SELECT b.id AS containerBookingId,b.container_no AS containerNo,b.ocean_booking_id AS oceanBookingId,
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
WHERE b.id=#{containerBookingId} FOR UPDATE
|
||||
WHERE b.id=#{containerBookingId}
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
FOR UPDATE
|
||||
""")
|
||||
Context lockContext(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@@ -27,6 +31,9 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
WHERE b.id=#{containerBookingId}
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
""")
|
||||
Context context(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@@ -37,6 +44,9 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
FROM fl_container_booking b
|
||||
WHERE b.id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
AND EXISTS(SELECT 1 FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING')
|
||||
ORDER BY b.id
|
||||
</script>
|
||||
""")
|
||||
|
||||
+18
-16
@@ -5,7 +5,7 @@ import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.portschedule.domain.CarrierScheduleNode;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.ActualTimeRequest;
|
||||
@@ -123,7 +123,7 @@ public class TerminalDrayageService {
|
||||
requireVersion(value, request.expectedVersion());
|
||||
if (value.getFleetSupplierId() == null) throw conflict("请先选择有效码头车队");
|
||||
if (actualPickupAt(context, value) != null) throw conflict("实际提柜后不可修改预约提柜时间");
|
||||
if (context.facts().arrivedDestinationPortAt() != null
|
||||
if (context.facts() != null && context.facts().arrivedDestinationPortAt() != null
|
||||
&& request.pickupAppointmentAt().isBefore(context.facts().arrivedDestinationPortAt())) {
|
||||
throw badRequest("预约提柜时间不能早于实际到港时间");
|
||||
}
|
||||
@@ -149,7 +149,8 @@ public class TerminalDrayageService {
|
||||
throw conflict("选择码头车队并预约提柜后才能确认实际提柜");
|
||||
}
|
||||
if (actualPickupAt(context, value) != null) throw conflict("实际提柜时间已确认");
|
||||
if (context.facts().latestActualAt() != null && request.actualAt().isBefore(context.facts().latestActualAt())) {
|
||||
if (context.facts() != null && context.facts().latestActualAt() != null
|
||||
&& request.actualAt().isBefore(context.facts().latestActualAt())) {
|
||||
throw badRequest("实际提柜时间不能早于当前实际船节点时间");
|
||||
}
|
||||
advanceNode(context, CarrierScheduleNode.PICKED_UP, request.actualAt(), action, key,
|
||||
@@ -192,14 +193,11 @@ public class TerminalDrayageService {
|
||||
|
||||
private void advanceNode(WorkContext context, CarrierScheduleNode target, LocalDateTime actualAt,
|
||||
String action, String key, String reason, long operatorId, String operatorName) {
|
||||
if (context.facts().queryId() == null || context.facts().queryVersion() == null) {
|
||||
throw conflict("货柜尚未建立船节点跟踪任务");
|
||||
}
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
if (current != null && current.sequence() >= target.sequence()) return;
|
||||
carrierSchedules.addManualNode(context.facts().queryId(), operatorId, operatorName,
|
||||
carrierSchedules.addContainerManualNode(context.base().containerBookingId(), operatorId, operatorName,
|
||||
"td-" + hash(context.base().containerBookingId() + "|" + action + "|" + key.trim()),
|
||||
new ManualNodeRequest(context.facts().queryVersion(), target.name(), LocalDateTime.now(clock), null,
|
||||
new ContainerManualNodeRequest(target.name(), LocalDateTime.now(clock), null,
|
||||
actualAt, null, null, StringUtils.hasText(reason) ? reason.trim() : "码头拖车操作同步"));
|
||||
}
|
||||
|
||||
@@ -292,7 +290,7 @@ public class TerminalDrayageService {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (value != null && value.getFleetSupplierId() != null && actualPickupAt(context, value) == null
|
||||
&& actualEmptyReturnAt(context, value) == null && context.base().unloadingCompletedAt() == null) result.add("SEND_PICKUP_EMAIL");
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
if (current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence()
|
||||
&& current.sequence() < CarrierScheduleNode.PICKED_UP.sequence()
|
||||
&& actualPickupAt(context, value) == null) {
|
||||
@@ -312,7 +310,7 @@ public class TerminalDrayageService {
|
||||
if (context.base().unloadingCompletedAt() != null) return "UNLOADED";
|
||||
if (actualPickupAt(context, value) != null) return "PICKED_UP";
|
||||
if (value != null && value.getPickupAppointmentAt() != null) return "PICKUP_SCHEDULED";
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
return current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence()
|
||||
? "ARRIVED" : "WAITING_FOR_ARRIVAL";
|
||||
}
|
||||
@@ -322,7 +320,7 @@ public class TerminalDrayageService {
|
||||
}
|
||||
|
||||
private void requireAtLeast(WorkContext context, CarrierScheduleNode required, String message) {
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
CarrierScheduleNode current = currentNode(context);
|
||||
if (current == null || current.sequence() < required.sequence()) throw conflict(message);
|
||||
}
|
||||
|
||||
@@ -330,13 +328,18 @@ public class TerminalDrayageService {
|
||||
return StringUtils.hasText(value) ? CarrierScheduleNode.require(value) : null;
|
||||
}
|
||||
|
||||
private CarrierScheduleNode currentNode(WorkContext context) {
|
||||
return context.facts() == null ? null : node(context.facts().latestActualNode());
|
||||
}
|
||||
|
||||
private LocalDateTime actualPickupAt(WorkContext context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualPickupAt() != null ? value.getActualPickupAt() : context.facts().pickedUpAt();
|
||||
return value != null && value.getActualPickupAt() != null ? value.getActualPickupAt()
|
||||
: context.facts() == null ? null : context.facts().pickedUpAt();
|
||||
}
|
||||
|
||||
private LocalDateTime actualEmptyReturnAt(WorkContext context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualEmptyReturnAt() != null
|
||||
? value.getActualEmptyReturnAt() : context.facts().emptyReturnedAt();
|
||||
? value.getActualEmptyReturnAt() : context.facts() == null ? null : context.facts().emptyReturnedAt();
|
||||
}
|
||||
|
||||
private WorkContext requiredContext(long id) {
|
||||
@@ -362,8 +365,7 @@ public class TerminalDrayageService {
|
||||
}
|
||||
|
||||
private WorkContext context(Context base, ContainerActualFacts facts) {
|
||||
if (base == null || !"ACTIVE".equals(base.recordStatus()) || facts == null
|
||||
|| facts.departedWarehouseAt() == null) return null;
|
||||
if (base == null || !"ACTIVE".equals(base.recordStatus())) return null;
|
||||
return new WorkContext(base, facts);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user