货柜pda 添加操作人

This commit is contained in:
2026-09-22 17:27:32 +08:00
parent abca6882c3
commit 2555eda797
31 changed files with 384 additions and 59 deletions
@@ -33,8 +33,10 @@ class LoadingListControllerContractTest {
authority("lifecycle", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class);
authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class,
long.class, long.class, String.class, TransportUpdateRequest.class);
authority("confirmPreload", "platform:firstleg:preload:edit", AuthenticatedUser.class,
long.class, String.class, ConfirmPreloadRequest.class);
Method confirm = PlatformLoadingListController.class.getDeclaredMethod("confirmPreload",
AuthenticatedUser.class, long.class, String.class, ConfirmPreloadRequest.class);
assertThat(confirm.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('platform:firstleg:preload:edit') and hasAuthority('platform:firstleg:pda:assign')");
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
ConfirmedQuery.class);
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
@@ -35,7 +35,7 @@ class LoadingPickPdaTaskSqlContractTest {
}
@Test
void pendingTasksAreLimitedToUnassignedOrCurrentOperatorWithoutCustomerInput() throws Exception {
void pendingTasksAreLimitedToAssignedOperatorWithoutCustomerInput() throws Exception {
String sql = LoadingPickTaskMapper.class
.getDeclaredMethod("selectPendingForOperator", long.class, long.class, String.class)
.getAnnotation(Select.class).value()[0];
@@ -54,7 +54,8 @@ class LoadingPickPdaTaskSqlContractTest {
.contains("pick_item.item_status IN ('PENDING', 'PICKING')")
.contains("THEN 'AWAITING_REVIEW'")
.contains("ELSE 'IN_PROGRESS' END AS workflowStatus")
.contains("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
.contains("AND assignment.assignee_id = #{operatorId}")
.doesNotContain("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
.contains("booking.warehouse_id AS warehouseId")
.doesNotContain("tenantId");
}
@@ -12,6 +12,8 @@ import com.baiyue.firstleg.dto.LoadingListDtos.PreloadReadiness;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
@@ -63,6 +65,8 @@ class LoadingListServiceTest {
@MockBean
private LoadingDocumentFileClient documentFiles;
@MockBean
private DomesticPdaOperatorClient domesticPdaOperators;
@MockBean
private LoadingCompletedOutboxService loadingCompletedOutboxService;
@MockBean
private FbaPricingService fbaPricingService;
@@ -784,6 +788,18 @@ class LoadingListServiceTest {
}
@Test
void rejectsInvalidPdaOperatorBeforeConfirmingAnyPreload() {
when(domesticPdaOperators.required(any(), eq(77L)))
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(88L, "其他人")));
assertThatThrownBy(() -> service.confirmPreload(10L, 9L, "operator", "invalid-operator",
new ConfirmPreloadRequest(3, 77L)))
.isInstanceOf(BizException.class).hasMessageContaining("作业员无效");
verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString());
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
}
@Test
void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() {
AggregateRow before = row("EDITING", 3);
@@ -810,9 +826,16 @@ class LoadingListServiceTest {
when(repository.confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator")).thenReturn(1);
when(repository.confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator")).thenReturn(1);
when(repository.preloadDetail(after)).thenReturn(detail);
when(domesticPdaOperators.required(any(), eq(77L)))
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
LoadingPickTaskEntity pickTask = new LoadingPickTaskEntity();
pickTask.setId(80L);
when(pickRepository.taskByPlan(10L)).thenReturn(pickTask);
when(pickRepository.createAssignment(80L, 77L, "作业员", 9L, "operator", "ASSIGNED"))
.thenReturn(1);
PreloadConfirmationResponse response = service.confirmPreload(
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3));
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L));
assertThat(response.confirmationStatus()).isEqualTo("CONFIRMED");
assertThat(response.version()).isEqualTo(4);
@@ -822,6 +845,7 @@ class LoadingListServiceTest {
verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString());
verify(fbaPricingService).recalculateUnlockedContainerCustomerFees(eq(30L), eq(102L), anyString());
verify(loadingPickService, times(1)).beginIfReady(30L, 9L, "operator");
verify(pickRepository).createAssignment(80L, 77L, "作业员", 9L, "operator", "ASSIGNED");
verify(repository).appendHistory(eq(10L), anyString(), eq("CONFIRM_PRELOAD"),
eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L),
eq("operator"), org.mockito.ArgumentMatchers.isNull());
@@ -838,9 +862,11 @@ class LoadingListServiceTest {
when(fbaPricingService.recalculateUnlockedContainerCustomerFees(eq(30L), eq(101L), anyString()))
.thenReturn(new FbaPricingService.ContainerFeeRecalculationResult(
FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0));
when(domesticPdaOperators.required(any(), eq(77L)))
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
assertThatThrownBy(() -> service.confirmPreload(
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)))
.isInstanceOf(BizException.class).hasMessageContaining("初始费用");
verify(repository, never()).touchForPreloadConfirmation(anyLong(), anyInt(), anyLong(), anyString());
@@ -852,7 +878,7 @@ class LoadingListServiceTest {
void exactConfirmationReplayReturnsFrozenResponseWithoutTouchingPlan() throws Exception {
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
String requestHash = sha256("CONFIRM_PRELOAD|10|3");
String requestHash = sha256("CONFIRM_PRELOAD|10|3|77");
HistoryRow replay = new HistoryRow();
ObjectMapper mapper = new ObjectMapper();
replay.setAfterSnapshot(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(
@@ -860,7 +886,7 @@ class LoadingListServiceTest {
when(repository.history(anyString())).thenReturn(replay);
PreloadConfirmationResponse actual = service.confirmPreload(
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3));
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L));
assertThat(actual).isEqualTo(expected);
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
@@ -876,14 +902,14 @@ class LoadingListServiceTest {
void rejectsConfirmationReplayWhenExpectedVersionDiffers() throws Exception {
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
String originalHash = sha256("CONFIRM_PRELOAD|10|3");
String originalHash = sha256("CONFIRM_PRELOAD|10|3|77");
HistoryRow replay = new HistoryRow();
replay.setAfterSnapshot(new ObjectMapper().writeValueAsString(
java.util.Map.of("requestHash", originalHash, "response", expected)));
when(repository.history(anyString())).thenReturn(replay);
assertThatThrownBy(() -> service.confirmPreload(
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
@@ -898,11 +924,13 @@ class LoadingListServiceTest {
AggregateRow confirmed = row("EDITING", 4);
when(repository.history(anyString())).thenReturn(null);
when(repository.aggregate(10L)).thenReturn(confirmed);
when(domesticPdaOperators.required(any(), eq(77L)))
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
when(loadingPickService.requirePreloadConfirmationReady(10L))
.thenThrow(new BizException(ResultCode.CONFLICT, "预装计划已确认"));
assertThatThrownBy(() -> service.confirmPreload(
10L, 9L, "operator", "new-confirm-key", new ConfirmPreloadRequest(4)))
10L, 9L, "operator", "new-confirm-key", new ConfirmPreloadRequest(4, 77L)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
@@ -919,11 +947,13 @@ class LoadingListServiceTest {
AggregateRow before = row("EDITING", 3);
when(repository.history(anyString())).thenReturn(null);
when(repository.aggregate(10L)).thenReturn(before);
when(domesticPdaOperators.required(any(), eq(77L)))
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(77L, "作业员")));
when(loadingPickService.requirePreloadConfirmationReady(10L))
.thenThrow(new BizException(ResultCode.CONFLICT, "第二票订舱尚未放舱"));
assertThatThrownBy(() -> service.confirmPreload(
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3, 77L)))
.isInstanceOf(BizException.class).hasMessageContaining("第二票");
verify(repository, never()).touchForPreloadConfirmation(anyLong(),
@@ -940,7 +970,7 @@ class LoadingListServiceTest {
when(repository.history(anyString())).thenReturn(replay);
assertThatThrownBy(() -> service.confirmPreload(
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4, 77L)))
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
@@ -11,6 +11,8 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
import com.baiyue.firstleg.client.DomesticPdaOperatorClient;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
@@ -106,6 +108,8 @@ class LoadingPickServiceTest {
private InboundHoldGuard inboundHoldGuard;
@MockBean
private DropshipPalletizationMapper dropshipPallets;
@MockBean
private DomesticPdaOperatorClient domesticPdaOperators;
@Resource
private LoadingPickService service;
@@ -565,8 +569,7 @@ class LoadingPickServiceTest {
when(repository.operation("claim-2")).thenReturn(null);
when(repository.task(20L)).thenReturn(task);
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.assignment(20L)).thenReturn(null, assignment);
when(repository.createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED")).thenReturn(1);
when(repository.assignment(20L)).thenReturn(assignment);
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1, 0);
doAnswer(invocation -> {
LoadingPickOperationEntity value = invocation.getArgument(0);
@@ -574,9 +577,9 @@ class LoadingPickServiceTest {
return null;
}).when(repository).insert(any(LoadingPickOperationEntity.class));
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
service.claim(20L, new ClaimRequest(0, "claim-2"), 9L, "operator");
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator");
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-1"), 9L, "operator");
service.claim(20L, new ClaimRequest(assignment.getVersion(), "claim-2"), 9L, "operator");
verify(smartLoadingRepository, times(2)).startPicking(10L, 9L, "operator");
verify(smartLoadingRepository, times(1)).insert(argThat(
@@ -585,7 +588,8 @@ class LoadingPickServiceTest {
&& "EDITING".equals(history.getFromStatus())
&& "LOADING".equals(history.getToStatus())
&& "PDA-START-20".equals(history.getOperationId())));
verify(repository, times(1)).createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED");
verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(), anyLong(),
anyString(), anyString());
}
@Test
@@ -606,6 +610,17 @@ class LoadingPickServiceTest {
verify(smartLoadingRepository, never()).startPicking(anyLong(), anyLong(), anyString());
}
@Test
void unassignedTaskCannotBeClaimedFromPda() {
when(repository.taskForUpdate(20L)).thenReturn(task());
assertThatThrownBy(() -> service.claim(20L, new ClaimRequest(0, "claim-unassigned"),
9L, "operator"))
.isInstanceOf(BizException.class).hasMessageContaining("尚未指定作业员");
verify(repository, never()).createAssignment(anyLong(), anyLong(), anyString(),
anyLong(), anyString(), anyString());
}
@Test
void assignRechecksSameOperationAfterWaitingForTaskLock() {
LoadingPickTaskEntity task = task();
@@ -615,6 +630,8 @@ class LoadingPickServiceTest {
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.task(20L)).thenReturn(task);
when(repository.items(20L)).thenReturn(List.of());
when(domesticPdaOperators.required(any(), eq(11L)))
.thenReturn(ApiResponse.success(new DomesticPdaOperatorClient.Operator(11L, "picker")));
Task replay = service.assign(20L, new AssignRequest(11L, "picker", 0, "assign-race"),
9L, "operator");