feat: gate overseas receiving and harden phase4 workflows
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
||||
package com.baiyue.lastleg.shipping.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_container_shipping_gate")
|
||||
public class ContainerShippingGate {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private String containerBookingId;
|
||||
private String customerId;
|
||||
private String oceanBookingId;
|
||||
private String containerNo;
|
||||
private String status;
|
||||
private String standardNode;
|
||||
private Integer nodeSequence;
|
||||
private LocalDateTime observedAt;
|
||||
private LocalDateTime actualAt;
|
||||
private String locationCode;
|
||||
private String locationName;
|
||||
private String providerCode;
|
||||
private String providerEventId;
|
||||
private String providerNodeCode;
|
||||
private String sourceReference;
|
||||
private String rawPayloadHash;
|
||||
private Long aggregateVersion;
|
||||
private Long handoverVersion;
|
||||
private String lastEventId;
|
||||
private String payloadHash;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.lastleg.shipping.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ContainerShippingGateMapper extends BaseMapper<ContainerShippingGate> {
|
||||
@Select("SELECT * FROM ll_container_shipping_gate WHERE container_booking_id=#{containerBookingId} FOR UPDATE")
|
||||
ContainerShippingGate lock(@Param("containerBookingId") String containerBookingId);
|
||||
|
||||
@Insert("INSERT IGNORE INTO ll_container_shipping_gate(container_booking_id,customer_id,ocean_booking_id," +
|
||||
"container_no,status,node_sequence,aggregate_version,handover_version,created_at,updated_at) VALUES(" +
|
||||
"#{containerBookingId},#{customerId},#{oceanBookingId},#{containerNo},'AWAITING_PICKUP_READY',0," +
|
||||
"#{aggregateVersion},#{handoverVersion},NOW(3),NOW(3))")
|
||||
int initialize(@Param("containerBookingId") String containerBookingId, @Param("customerId") String customerId,
|
||||
@Param("oceanBookingId") String oceanBookingId, @Param("containerNo") String containerNo,
|
||||
@Param("aggregateVersion") long aggregateVersion, @Param("handoverVersion") long handoverVersion);
|
||||
|
||||
@Select("SELECT CASE WHEN COUNT(DISTINCT o.source_container_booking_id)=1 " +
|
||||
"AND MIN(CASE WHEN g.status='RECEIVING_AVAILABLE' THEN 1 ELSE 0 END)=1 THEN 1 ELSE 0 END " +
|
||||
"FROM ll_order o LEFT JOIN ll_container_shipping_gate g " +
|
||||
"ON g.container_booking_id=o.source_container_booking_id AND g.customer_id=o.customer_id " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.container_no=#{containerNo}")
|
||||
int receivingAvailable(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.baiyue.lastleg.shipping.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.FirstLegContainerShippingNodeRecordedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.entity.LastlegEventInbox;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventInboxMapper;
|
||||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||||
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
||||
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.Isolation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
@Service
|
||||
public class ContainerShippingNodeConsumer {
|
||||
public static final int PICKUP_READY_SEQUENCE = 80;
|
||||
@Resource private LastlegEventInboxMapper inboxes;
|
||||
@Resource private ContainerShippingGateMapper gates;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||
public int consume(EventEnvelope envelope) {
|
||||
validate(envelope);
|
||||
FirstLegContainerShippingNodeRecordedV1 payload = payload(envelope);
|
||||
validateEnvelopePayload(envelope, payload);
|
||||
LastlegEventInbox existing = inboxes.selectById(envelope.eventId());
|
||||
if (existing != null) {
|
||||
if (!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("船节点事件摘要冲突");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
LastlegEventInbox inbox = inbox(envelope);
|
||||
try { inboxes.insert(inbox); }
|
||||
catch (DuplicateKeyException raced) {
|
||||
LastlegEventInbox concurrent = inboxes.selectById(envelope.eventId());
|
||||
if (concurrent == null || !concurrent.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) {
|
||||
throw new IllegalArgumentException("船节点事件摘要冲突", raced);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
gates.initialize(payload.containerBookingId(), payload.customerId(), payload.oceanBookingId(),
|
||||
payload.containerNo(), 0, 1);
|
||||
ContainerShippingGate gate = gates.lock(payload.containerBookingId());
|
||||
if (gate == null || !payload.customerId().equals(gate.getCustomerId())
|
||||
|| !payload.containerNo().equals(gate.getContainerNo())) {
|
||||
throw new IllegalArgumentException("船节点柜号快照与装柜交接不一致");
|
||||
}
|
||||
apply(gate, envelope, payload);
|
||||
gates.updateById(gate);
|
||||
inbox.setStatus("PROCESSED"); inbox.setProcessedAt(LocalDateTime.now()); inboxes.updateById(inbox);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void apply(ContainerShippingGate gate, EventEnvelope envelope,
|
||||
FirstLegContainerShippingNodeRecordedV1 payload) {
|
||||
long currentVersion = gate.getAggregateVersion() == null ? 0 : gate.getAggregateVersion();
|
||||
int currentSequence = gate.getNodeSequence() == null ? 0 : gate.getNodeSequence();
|
||||
LocalDateTime actualAt = LocalDateTime.ofInstant(payload.actualAt(), ZoneOffset.UTC);
|
||||
boolean newerVersion = envelope.aggregateVersion() >= currentVersion;
|
||||
boolean newerNode = payload.nodeSequence() > currentSequence;
|
||||
boolean laterSameNode = payload.nodeSequence() == currentSequence
|
||||
&& (gate.getActualAt() == null || actualAt.isAfter(gate.getActualAt()));
|
||||
boolean accepted = newerVersion && (newerNode || laterSameNode);
|
||||
if (accepted) {
|
||||
gate.setOceanBookingId(payload.oceanBookingId());
|
||||
gate.setStandardNode(payload.standardNode()); gate.setNodeSequence(payload.nodeSequence());
|
||||
gate.setObservedAt(LocalDateTime.ofInstant(payload.observedAt(), ZoneOffset.UTC)); gate.setActualAt(actualAt);
|
||||
gate.setLocationCode(payload.locationCode()); gate.setLocationName(payload.locationName());
|
||||
gate.setProviderCode(payload.providerCode()); gate.setProviderEventId(payload.providerEventId());
|
||||
gate.setProviderNodeCode(payload.providerNodeCode()); gate.setSourceReference(payload.sourceReference());
|
||||
gate.setRawPayloadHash(payload.rawPayloadHash()); gate.setLastEventId(envelope.eventId());
|
||||
gate.setPayloadHash(envelope.payloadHash());
|
||||
}
|
||||
if (accepted && payload.nodeSequence() >= PICKUP_READY_SEQUENCE) {
|
||||
gate.setStatus("RECEIVING_AVAILABLE");
|
||||
}
|
||||
gate.setAggregateVersion(Math.max(currentVersion, envelope.aggregateVersion()));
|
||||
gate.setHandoverVersion(Math.max(gate.getHandoverVersion() == null ? 0 : gate.getHandoverVersion(),
|
||||
envelope.handoverVersion()));
|
||||
gate.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
private void validate(EventEnvelope envelope) {
|
||||
envelope.requireValid();
|
||||
if (!EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED.equals(envelope.eventType())
|
||||
|| envelope.eventVersion() != 1 || envelope.sourceRegion() != RegionCode.CN
|
||||
|| envelope.targetRegion() != RegionCode.OVERSEAS
|
||||
|| !"CONTAINER_BOOKING".equals(envelope.aggregateType())
|
||||
|| envelope.handoverVersion() == null
|
||||
|| envelope.handoverVersion() != envelope.aggregateVersion() + 1) {
|
||||
throw new IllegalArgumentException("船节点交接信封无效");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateEnvelopePayload(EventEnvelope envelope, FirstLegContainerShippingNodeRecordedV1 payload) {
|
||||
if (!envelope.tenantId().equals(payload.customerId())
|
||||
|| !envelope.aggregateId().equals(payload.containerBookingId())) {
|
||||
throw new IllegalArgumentException("船节点交接客户或聚合标识不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private FirstLegContainerShippingNodeRecordedV1 payload(EventEnvelope envelope) {
|
||||
try { return objectMapper.treeToValue(envelope.payload(), FirstLegContainerShippingNodeRecordedV1.class); }
|
||||
catch (Exception exception) { throw new IllegalArgumentException("船节点交接负载无效", exception); }
|
||||
}
|
||||
|
||||
private LastlegEventInbox inbox(EventEnvelope envelope) {
|
||||
LastlegEventInbox value = new LastlegEventInbox(); value.setEventId(envelope.eventId());
|
||||
value.setCustomerId(envelope.tenantId()); value.setEventType(envelope.eventType());
|
||||
value.setEventVersion(envelope.eventVersion()); value.setPayloadHash(envelope.payloadHash());
|
||||
value.setPayloadJson(envelope.payload().toString()); value.setStatus("RECEIVED");
|
||||
value.setReceivedAt(LocalDateTime.now()); return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user