This commit is contained in:
2026-09-29 17:55:54 +08:00
parent 41d8387cd5
commit 54ad26c607
111 changed files with 1837 additions and 962 deletions
@@ -8,7 +8,7 @@ import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.common.event.FirstLegLoadingPlanCompletedV1;
import com.baiyue.common.event.EventTypes;
import com.baiyue.common.event.RegionCode;
import com.baiyue.firstleg.client.EventBridgeClient;
import com.baiyue.firstleg.client.LastlegEventClient;
import com.baiyue.firstleg.entity.InboundOutboxEntity;
import com.baiyue.firstleg.repository.LoadingCompletedOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -31,7 +31,7 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LoadingCompletedOutboxRelayTest {
@Mock private LoadingCompletedOutboxRepository repository;
@Mock private EventBridgeClient client;
@Mock private LastlegEventClient client;
private ObjectMapper objectMapper;
private LoadingCompletedOutboxRelay relay;
@@ -44,18 +44,18 @@ class LoadingCompletedOutboxRelayTest {
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
ReflectionTestUtils.setField(relay, "leaseSeconds", 60L);
ReflectionTestUtils.setField(relay, "retrySeconds", 30L);
ReflectionTestUtils.setField(relay, "baseDelaySeconds", 5L);
}
@Test
void claimsExpiredLeasesAndPublishesAfterOverseasBridgeAcknowledgement() throws Exception {
void claimsExpiredLeasesAndPublishesAfterConsumerAcknowledgement() throws Exception {
EventEnvelope event = event();
InboundOutboxEntity row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class))).thenReturn(ApiResponse.success(
new EventBridgeClient.Acknowledgement(event.eventId(), "已接收", "2026-08-20T03:01:00")));
com.baiyue.common.event.DeliveryAck.of(event, com.baiyue.common.event.DeliveryAck.Outcome.APPLIED)));
relay.relay();
@@ -110,11 +110,53 @@ class LoadingCompletedOutboxRelayTest {
relay.relay();
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq(
"IllegalStateException: 事件桥拒绝头程跨区事件: code=S0503, message=bridge inbox unavailable, "
"IllegalStateException: Lastleg拒绝头程业务事件: code=S0503, message=bridge inbox unavailable, "
+ "requestId=request-123, traceId=trace-456"));
verify(repository, never()).published(anyLong(), anyString(), any());
}
@Test
void matchingEventIdWithWrongPayloadHashDoesNotPublish() throws Exception {
EventEnvelope event = event();
InboundOutboxEntity row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class))).thenReturn(ApiResponse.success(
new com.baiyue.common.event.DeliveryAck(event.eventId(), "sha256:" + "0".repeat(64),
com.baiyue.common.event.DeliveryAck.Outcome.APPLIED)));
relay.relay();
verify(repository, never()).published(anyLong(), anyString(), any());
verify(repository).retry(eq(9001L), anyString(), any(), contains("ACK does not match"));
}
@Test
void twelfthFailureGoesDeadInsteadOfSchedulingAnotherRetry() throws Exception {
InboundOutboxEntity row = row(event());
row.setRetryCount(11);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(anyString(), any())).thenThrow(new IllegalStateException("consumer unavailable"));
relay.relay();
verify(repository).dead(eq(9001L), anyString(), eq(12), contains("consumer unavailable"));
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
verify(repository, never()).published(anyLong(), anyString(), any());
}
@Test
void recoveredExhaustedLeaseDoesNotSendAgain() throws Exception {
InboundOutboxEntity row = row(event());
row.setRetryCount(12);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
relay.relay();
verifyNoInteractions(client);
verify(repository).dead(eq(9001L), anyString(), eq(12), contains("租约超时"));
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
}
private InboundOutboxEntity row(EventEnvelope event) throws Exception {
InboundOutboxEntity row = new InboundOutboxEntity();
row.setId(9001L);
@@ -44,6 +44,7 @@ class OverseasContainerEventConsumerTest {
void derivesEstimatedEmptyReportedAtFromUnloadCompletionAndPersistsInbox() {
ContainerBookingEntity container = container();
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container);
when(containers.applyEmptyReport(anyLong(), any(), any(), any(), any(), anyLong())).thenReturn(1);
Instant completedAt = Instant.parse("2026-08-20T03:00:00Z");
Instant uploadedAt = Instant.parse("2026-08-20T02:30:00Z");
@@ -67,10 +68,10 @@ class OverseasContainerEventConsumerTest {
EventEnvelope event = event("00000000-0000-4000-8000-000000000002", 7, payload);
ContainerEventInboxEntity existing = new ContainerEventInboxEntity();
existing.setPayloadHash(event.payloadHash());
existing.setStatus("PROCESSED");
when(inbox.selectById(event.eventId())).thenReturn(existing);
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container());
consumer.consume(event);
assertThat(consumer.consume(event).outcome()).isEqualTo(com.baiyue.common.event.DeliveryAck.Outcome.DUPLICATE);
verify(containers, never()).applyEmptyReport(anyLong(), any(), any(), any(), any(), anyLong());
verify(inbox, never()).insert(any(ContainerEventInboxEntity.class));
@@ -91,6 +92,17 @@ class OverseasContainerEventConsumerTest {
verifyNoInteractions(containers, inbox);
}
@Test
void unappliedUnloadCannotBeAcknowledgedAsSuccessful() {
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container());
EventEnvelope event = event("00000000-0000-4000-8000-000000000004", 7,
new OverseasContainerUnloadedV1("1001", "ZCSU7881970", Instant.parse("2026-08-20T03:00:00Z"),
Instant.parse("2026-08-20T02:30:00Z"), null));
assertThatThrownBy(() -> consumer.consume(event)).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("卸柜事实未写入");
verify(inbox, never()).updateById(any(ContainerEventInboxEntity.class));
}
private EventEnvelope event(String eventId, long aggregateVersion, OverseasContainerUnloadedV1 payload) {
var node = objectMapper.valueToTree(payload);
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
@@ -47,7 +47,8 @@ class OverseasInboundOrderDetainedConsumerTest {
when(inboundOrders.selectActiveByIdForUpdate(1001L)).thenReturn(inbound);
when(inboundOrders.applyOverseasHold(88L, 1001L, true, "等待查验", 7L, 66L)).thenReturn(1);
consumer.consume(event(7, true, "等待查验"));
assertThat(consumer.consume(event(7, true, "等待查验")).outcome())
.isEqualTo(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED);
verify(inboundOrders).applyOverseasHold(88L, 1001L, true, "等待查验", 7L, 66L);
ArgumentCaptor<InboundHoldRecordEntity> history = ArgumentCaptor.forClass(InboundHoldRecordEntity.class);
@@ -61,7 +62,8 @@ class OverseasInboundOrderDetainedConsumerTest {
void staleReleaseCannotOverwriteNewerHoldState() {
when(inboundOrders.selectActiveByIdForUpdate(1001L)).thenReturn(inbound(9L));
consumer.consume(event(7, false, "解除"));
assertThat(consumer.consume(event(7, false, "解除")).outcome())
.isEqualTo(com.baiyue.common.event.DeliveryAck.Outcome.STALE);
verify(inboundOrders, never()).applyOverseasHold(anyLong(), anyLong(), anyBoolean(), any(), anyLong(), anyLong());
verifyNoInteractions(holdRecords);