优化
This commit is contained in:
@@ -27,6 +27,7 @@ import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||
@@ -35,6 +36,7 @@ import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
@@ -45,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
@@ -54,6 +57,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringJUnitConfig(LoadingPickService.class)
|
||||
@@ -61,6 +65,8 @@ class LoadingPickServiceTest {
|
||||
@MockBean
|
||||
private LoadingPickRepository repository;
|
||||
@MockBean
|
||||
private LoadingCompletionCommandRepository completionCommands;
|
||||
@MockBean
|
||||
private SmartLoadingRepository smartLoadingRepository;
|
||||
@MockBean
|
||||
private OceanBookingRepository oceanBookingRepository;
|
||||
@@ -89,6 +95,9 @@ class LoadingPickServiceTest {
|
||||
booking.setSourceType("SIMULATION");
|
||||
booking.setRecordStatus("ACTIVE");
|
||||
when(repository.activeBookingByTask(20L)).thenReturn(booking);
|
||||
when(completionCommands.insert(any())).thenReturn(1);
|
||||
when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true);
|
||||
when(repository.acceptCompletion(anyLong(), anyInt())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -489,6 +498,7 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
|
||||
void manualCompletionSettlesResolvedItemsCompletesPlanAndReplaysSameOperation() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity item = item();
|
||||
@@ -539,6 +549,7 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
|
||||
void conflictingTruckAppointmentsDoNotBlockPdaCompletion() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity item = item();
|
||||
@@ -569,6 +580,7 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
|
||||
void doesNotPublishCompletedEventWhenInboundFinalizationFails() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
@@ -593,6 +605,7 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
|
||||
void translatesUnexpectedSettlementFailureIntoActionableStageMessage() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
@@ -615,6 +628,7 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
|
||||
void warehouseSettlementConflictLeavesCompletionClaimAndDownstreamFinalizationUnwritten() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
task.setVersion(1);
|
||||
@@ -643,6 +657,7 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@org.junit.jupiter.api.Disabled("Replaced by durable asynchronous completion command contract")
|
||||
void currentCompleteRequestFinishesAfterUniqueResourceSettlementSucceeds() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
task.setVersion(1);
|
||||
@@ -670,6 +685,141 @@ class LoadingPickServiceTest {
|
||||
verify(loadingListService).publishCompletedEvent(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameOperationIdReplayReturnsProcessingWithoutAnotherTaskForUpdate() {
|
||||
LoadingPickTaskEntity loading = task();
|
||||
LoadingPickTaskEntity completing = task();
|
||||
completing.setTaskStatus("COMPLETING");
|
||||
completing.setVersion(1);
|
||||
AtomicReference<com.baiyue.firstleg.entity.LoadingCompletionCommandEntity> accepted = new AtomicReference<>();
|
||||
when(completionCommands.operation("async-complete-1")).thenAnswer(invocation -> accepted.get());
|
||||
doAnswer(invocation -> {
|
||||
accepted.set(invocation.getArgument(0));
|
||||
return 1;
|
||||
}).when(completionCommands).insert(any());
|
||||
when(repository.taskForUpdate(20L)).thenReturn(loading);
|
||||
when(repository.task(20L)).thenReturn(completing);
|
||||
when(repository.items(20L)).thenReturn(List.of(item()));
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||
|
||||
Task first = service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "async-complete-1", "checked"), 9L, "operator");
|
||||
verify(repository).taskForUpdate(20L);
|
||||
clearInvocations(repository);
|
||||
Task replay = org.junit.jupiter.api.Assertions.assertTimeout(java.time.Duration.ofMillis(200), () ->
|
||||
service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "async-complete-1", "checked"), 9L, "operator"));
|
||||
|
||||
assertThat(first.taskStatus()).isEqualTo("COMPLETING");
|
||||
assertThat(first.workflowStatus()).isEqualTo("PROCESSING");
|
||||
assertThat(replay.taskStatus()).isEqualTo("COMPLETING");
|
||||
verify(repository, never()).taskForUpdate(20L);
|
||||
verify(completionCommands, times(1)).insert(any());
|
||||
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentOperationIdIsRejectedImmediatelyWithConflict() {
|
||||
// different operationId is a canonical A0409 CONFLICT while the durable command is active.
|
||||
LoadingPickTaskEntity loading = task();
|
||||
when(repository.taskForUpdate(20L)).thenReturn(loading);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||
var active = new com.baiyue.firstleg.entity.LoadingCompletionCommandEntity();
|
||||
active.setOperationId("already-processing");
|
||||
when(completionCommands.active(20L)).thenReturn(active);
|
||||
|
||||
assertThatThrownBy(() -> service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "different-operation", "checked"), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("其他装柜完成命令");
|
||||
|
||||
verify(completionCommands, never()).insert(any());
|
||||
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedSameOperationIdCanResumeWithoutCreatingAnotherCommand() {
|
||||
LoadingPickTaskEntity failed = task();
|
||||
failed.setTaskStatus("COMPLETION_FAILED");
|
||||
LoadingPickTaskEntity completing = task();
|
||||
completing.setTaskStatus("COMPLETING");
|
||||
var command = completionCommand(902L, "resume-same-operation");
|
||||
command.setRequestHash("48ea6be16079a761f4e77abcf58efaafebc2ab629250ce06118315232e9f3070");
|
||||
command.setCommandStatus("FAILED");
|
||||
command.setExpectedTaskVersion(0);
|
||||
when(completionCommands.operation("resume-same-operation")).thenReturn(command);
|
||||
when(completionCommands.resume(902L)).thenReturn(true);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(failed);
|
||||
when(repository.resumeCompletion(20L)).thenReturn(1);
|
||||
when(repository.task(20L)).thenReturn(completing);
|
||||
when(repository.items(20L)).thenReturn(List.of(item()));
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||
|
||||
Task result = service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "resume-same-operation", "checked"), 9L, "operator");
|
||||
|
||||
assertThat(result.workflowStatus()).isEqualTo("PROCESSING");
|
||||
verify(completionCommands).resume(902L);
|
||||
verify(repository).resumeCompletion(20L);
|
||||
verify(completionCommands, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void workerProcessesFifteenInboundOwnersInStableOrderAndFinalizesOnce() {
|
||||
LoadingPickTaskEntity completing = task();
|
||||
completing.setTaskStatus("COMPLETING");
|
||||
when(repository.task(20L)).thenReturn(completing);
|
||||
List<LoadingPickItemMapper.InboundOwner> owners = java.util.stream.IntStream.range(0, 15)
|
||||
.mapToObj(index -> new LoadingPickItemMapper.InboundOwner(101L, 1014L - index)).toList();
|
||||
when(repository.inboundOwners(20L)).thenReturn(owners);
|
||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||
when(repository.review(20L)).thenReturn(null);
|
||||
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
|
||||
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
|
||||
when(completionCommands.complete(900L, "worker-1")).thenReturn(true);
|
||||
var command = completionCommand(900L, "async-15");
|
||||
|
||||
service.processCompletion(command, "worker-1");
|
||||
|
||||
ArgumentCaptor<Long> inboundIds = ArgumentCaptor.forClass(Long.class);
|
||||
verify(settlementService, times(15)).settleOwner(eq(20L), inboundIds.capture(), eq(9L), eq("operator"));
|
||||
assertThat(inboundIds.getAllValues()).containsExactly(
|
||||
1000L, 1001L, 1002L, 1003L, 1004L, 1005L, 1006L, 1007L,
|
||||
1008L, 1009L, 1010L, 1011L, 1012L, 1013L, 1014L);
|
||||
verify(loadingListService).publishCompletedEvent(10L);
|
||||
verify(repository).completeAccepted(20L, 9L, "operator");
|
||||
verify(completionCommands).complete(900L, "worker-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements() {
|
||||
LoadingPickTaskEntity completing = task();
|
||||
completing.setTaskStatus("COMPLETING");
|
||||
when(repository.task(20L)).thenReturn(completing);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 50L),
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 60L)));
|
||||
when(completionCommands.checkpoint(901L, 101L, 50L)).thenReturn("INBOUND_COMPLETED");
|
||||
when(completionCommands.checkpoint(901L, 101L, 60L)).thenReturn("SETTLED");
|
||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
|
||||
when(repository.completeAccepted(20L, 9L, "operator")).thenReturn(1);
|
||||
when(completionCommands.complete(901L, "worker-2")).thenReturn(true);
|
||||
|
||||
service.processCompletion(completionCommand(901L, "async-resume"), "worker-2");
|
||||
|
||||
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(eq(101L), eq(50L), anyLong(),
|
||||
anyLong(), anyString(), anyString(), anyString());
|
||||
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 60L, 50L, 9L, "operator",
|
||||
"PDA-async-resume-60", "checked");
|
||||
verify(loadingListService).publishCompletedEvent(10L);
|
||||
verify(repository).insert(org.mockito.ArgumentMatchers.<LoadingPickOperationEntity>argThat(
|
||||
operation -> "COMPLETE_LOADING".equals(operation.getActionType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void atomicallySubmitsCompleteBatchAndReplaysSameResult() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -871,6 +1021,21 @@ class LoadingPickServiceTest {
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item));
|
||||
}
|
||||
|
||||
private static com.baiyue.firstleg.entity.LoadingCompletionCommandEntity completionCommand(
|
||||
long id, String operationId) {
|
||||
var command = new com.baiyue.firstleg.entity.LoadingCompletionCommandEntity();
|
||||
command.setId(id);
|
||||
command.setPickTaskId(20L);
|
||||
command.setOperationId(operationId);
|
||||
command.setRequestHash("hash");
|
||||
command.setCommandStatus("PROCESSING");
|
||||
command.setOperatorId(9L);
|
||||
command.setOperatorName("operator");
|
||||
command.setRemark("checked");
|
||||
command.setAttemptCount(1);
|
||||
return command;
|
||||
}
|
||||
|
||||
private static LoadingPickTaskEntity task() {
|
||||
LoadingPickTaskEntity task = new LoadingPickTaskEntity();
|
||||
task.setId(20L);
|
||||
|
||||
Reference in New Issue
Block a user