diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index dd1d9ab43..40341327f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -252,9 +252,13 @@ public class SmartLoadingService { try { if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改"); } catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); } - history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, - json(values("version", request.expectedVersion())), - json(values("requestHash", requestHash)), operatorId, operatorName); + try { + history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, + json(values("version", request.expectedVersion())), + json(values("requestHash", requestHash)), operatorId, operatorName); + } catch (DuplicateKeyException exception) { + throw conflict("Idempotency-Key已用于不同推送请求"); + } LoadingPlanEntity pushed = repository.plan(planId); loadingModelBridge.bindAllocatedBeforeLoading(planId, operatorId, operatorName, key); loadingPickService.beginIfReady(pushed.getContainerBookingId(), operatorId, operatorName); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java index 8b1bd09be..64cefd94a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java @@ -26,6 +26,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; +import org.springframework.dao.DuplicateKeyException; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.math.BigDecimal; @@ -477,7 +478,7 @@ class SmartLoadingServiceTest { } @Test - void rollsBackPushOrchestrationBeforePdaWhenBindingFails() { + void bindingFailureStopsBeforePdaAndPropagatesForTransactionRollback() { LoadingPlanEntity editing = plan("EDITING", 3); LoadingPlanEntity pushed = plan("EDITING", 4); pushed.setPushIdempotencyKey("key"); @@ -493,6 +494,26 @@ class SmartLoadingServiceTest { assertThat(error.getMessage()).contains("预装计划不存在"); verify(repository).push(1, "key", 3, 7, "u"); + verify(repository).insert(any(LoadingPlanHistoryEntity.class)); + verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString()); + } + + @Test + void concurrentPushKeyClaimReturnsConflictBeforeBridgeAndPda() { + LoadingPlanEntity editing = plan("EDITING", 3); + when(repository.plan(1)).thenReturn(editing); + when(repository.items(1)).thenReturn(List.of(item(8, 9, 20, 1))); + when(repository.booking(99)).thenReturn(booking()); + when(repository.push(1, "race-key", 3, 7, "u")).thenReturn(1); + doThrow(new DuplicateKeyException("duplicate operation_id")) + .when(repository).insert(any(LoadingPlanHistoryEntity.class)); + + BizException error = catchThrowableOfType( + () -> service.pushPreload(1, "race-key", new PushRequest(3), 7, "u"), BizException.class); + + assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT); + assertThat(error.getMessage()).contains("不同推送请求"); + verify(loadingModelBridge, never()).bindAllocatedBeforeLoading(anyLong(), anyLong(), anyString(), anyString()); verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString()); }