第三期装柜验收优化修改

This commit is contained in:
2026-08-12 13:34:26 +08:00
parent d2704f68a6
commit c2b1ab92ff
27 changed files with 258 additions and 71 deletions
@@ -9,7 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class PdaConvergenceOpenApiContractTest {
@Test
void publishesClaimAssignWorkAndReviewRoutes() throws Exception {
void publishesClaimAssignWorkAndManualCompletionRoutes() throws Exception {
String yaml = new ClassPathResource("openapi/phase3-pda-convergence-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(yaml)
@@ -22,11 +22,13 @@ class PdaConvergenceOpenApiContractTest {
.contains("/tasks/{taskId}/items/{itemId}/skip:")
.contains("/tasks/{taskId}/batch:")
.contains("/tasks/{taskId}/review:")
.contains("/tasks/{taskId}/complete:")
.contains("/tasks/{taskId}/operations:")
.contains("expectedAssignmentVersion")
.contains("BatchRequest")
.contains("inventorySnapshotToken")
.contains("InventorySnapshot")
.contains("expectedTaskVersion");
.contains("expectedTaskVersion")
.contains("CompleteLoadingRequest");
}
}
@@ -22,6 +22,7 @@ class PlatformLoadingPdaControllerContractTest {
authority("skip", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, long.class, SkipRequest.class);
authority("batch", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, String.class, BatchRequest.class);
authority("review", "platform:firstleg:pda:review", AuthenticatedUser.class, long.class, ReviewRequest.class);
authority("complete", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, CompleteLoadingRequest.class);
authority("operations", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class);
}
@@ -41,7 +41,7 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("trg_inbound_split_detail_before_insert")
.contains("trg_inbound_split_detail_before_update")
.contains("trg_inbound_split_detail_before_delete")
.contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH'))")
.contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH', 'COMPLETE_LOADING'))")
.contains("phase3_loading_evidence_add_column")
.contains("DROP INDEX uk_loading_evidence_type")
.contains("DROP INDEX uk_loading_evidence_seal")
@@ -116,7 +116,7 @@ class SmartLoadingSqlContractTest {
.contains("IF OLD.plan_status = 'EDITING' AND NEW.plan_status = 'LOADING' THEN")
.contains("ELSEIF OLD.plan_status = 'LOADING' AND NEW.plan_status = 'COMPLETED' THEN")
.contains("LEFT JOIN fl_loading_pick_item pick_item")
.contains("pick_item.picked_package_count <> pick_item.planned_package_count")
.contains("pick_item.picked_package_count <> item.package_count")
.contains("IF OLD.plan_status = 'LOADING' AND NEW.plan_status = 'COMPLETED' THEN")
.contains("SET frozen_at = COALESCE(frozen_at, NEW.confirmed_at, CURRENT_TIMESTAMP(3))")
.contains("已有PDA拣货记录的装柜明细仅允许收敛部分拣货实际箱数")
@@ -7,7 +7,9 @@ import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.entity.LoadingPlanEntity;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
@@ -33,6 +35,8 @@ import static org.mockito.Mockito.when;
class ContainerServiceTest {
@MockBean private ContainerRepository repository;
@MockBean private WarehouseMasterDataClient warehouses;
@MockBean private SmartLoadingRepository smartLoadingRepository;
@MockBean private LoadingPickService loadingPickService;
@Resource private ContainerService service;
@BeforeEach
@@ -118,6 +122,27 @@ class ContainerServiceTest {
.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin");
}
@Test
@DisplayName("预装清单推送后再绑定集装箱号会创建PDA拣货任务")
void startsPdaTaskWhenContainerIsBoundAfterPreloadPush() {
ContainerBookingEntity current = booking("EDITING", 2);
LoadingPlanEntity plan = new LoadingPlanEntity();
plan.setId(31L);
plan.setPlanStatus("EDITING");
plan.setPreloadPushedAt(LocalDateTime.of(2026, 8, 11, 20, 0));
when(repository.find(11L)).thenReturn(current);
when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"))
.thenReturn(1);
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
service.updateTransport(11L, 9L, "admin",
new TransportUpdateRequest("MSCU1234567", "V002", 2));
verify(loadingPickService).begin(org.mockito.ArgumentMatchers.eq(31L),
org.mockito.ArgumentMatchers.eq("MSCU1234567"), org.mockito.ArgumentMatchers.eq(9L),
org.mockito.ArgumentMatchers.eq("admin"), any());
}
@Test
@DisplayName("已确认货柜全部只读")
void rejectsConfirmedUpdate() {
@@ -109,7 +109,7 @@ class LoadingListServiceTest {
verify(repository).advance(10L, "LOADING", "COMPLETED", 2, 9L, "operator");
verify(repository).appendHistory(eq(10L), eq("PDA-COMPLETE-10-21"),
eq("PDA_COMPLETE_LOADING"), eq("LOADING"), eq("COMPLETED"), anyString(), anyString(),
eq(9L), eq("operator"), eq("全部箱数已完成PDA拣货"));
eq(9L), eq("operator"), eq("PDA已人工确认装柜完成"));
}
@Test
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
@@ -67,7 +68,7 @@ class LoadingPickServiceTest {
}
@Test
void touchesTaskBeforeCompletingLastPickedItem() {
void keepsResourcesLockedAfterLastPickedItemUntilManualCompletion() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
preparePick(task, item, "pick-2");
@@ -76,11 +77,10 @@ class LoadingPickServiceTest {
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-2"), 9L, "operator");
InOrder order = inOrder(repository, settlementService, loadingListService);
order.verify(repository).touchTask(20L, 0);
order.verify(settlementService).settle(20L, 9L, "operator");
order.verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
order.verify(repository).syncInboundProjections(20L, 9L, "operator");
verify(repository).touchTask(20L, 0);
verify(settlementService, never()).settle(20L, 9L, "operator");
verify(loadingListService, never()).finishFromPda(10L, 20L, 9L, "operator");
verify(repository).syncInboundProjections(20L, 9L, "operator");
verify(repository).insert(any(LoadingPickHistoryEntity.class));
}
@@ -131,7 +131,7 @@ class LoadingPickServiceTest {
}
@Test
void touchesTaskBeforeCompletingLastPartialItem() {
void keepsPartialPickLockedUntilManualCompletion() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
preparePartial(task, item, "partial-1");
@@ -141,11 +141,10 @@ class LoadingPickServiceTest {
service.partial(20L, 30L, new PartialPickRequest(101L, 1, List.of(), 0, "partial-1"),
9L, "operator");
InOrder order = inOrder(repository, settlementService, loadingListService);
order.verify(repository).touchTask(20L, 0);
order.verify(settlementService).settle(20L, 9L, "operator");
order.verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
order.verify(repository).syncInboundProjections(20L, 9L, "operator");
verify(repository).touchTask(20L, 0);
verify(settlementService, never()).settle(20L, 9L, "operator");
verify(loadingListService, never()).finishFromPda(10L, 20L, 9L, "operator");
verify(repository).syncInboundProjections(20L, 9L, "operator");
}
@Test
@@ -171,7 +170,7 @@ class LoadingPickServiceTest {
}
@Test
void reviewSettlesResolvedItemsAndCompletesPlan() {
void manualCompletionSettlesResolvedItemsAndCompletesPlan() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
item.setPlannedPackageCount(5);
@@ -181,15 +180,15 @@ class LoadingPickServiceTest {
when(repository.task(20L)).thenReturn(task);
when(repository.items(20L)).thenReturn(List.of(item));
when(repository.items(20L, 101L)).thenReturn(List.of(item));
when(repository.settlePartial(40L, 30L, 3, 5, 9L, "operator")).thenReturn(1);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
service.review(20L, new ReviewRequest(0, "review-1", "checked"), 9L, "operator");
service.complete(20L, new CompleteLoadingRequest(0, "complete-1", "checked"), 9L, "operator");
verify(repository).insert(any(LoadingPickOperationEntity.class));
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
verify(settlementService, never()).settle(20L, 9L, "operator");
verify(settlementService).settle(20L, 9L, "operator");
verify(repository).syncInboundProjections(20L, 9L, "operator");
}
@@ -237,8 +236,8 @@ class LoadingPickServiceTest {
verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L);
verify(repository, times(1)).skip(31L, 0, 101L);
verify(repository, times(1)).touchTask(20L, 0);
verify(settlementService, times(1)).settle(20L, 9L, "operator");
verify(loadingListService, times(1)).finishFromPda(10L, 20L, 9L, "operator");
verify(settlementService, never()).settle(20L, 9L, "operator");
verify(loadingListService, never()).finishFromPda(10L, 20L, 9L, "operator");
verify(repository, times(1)).syncInboundProjections(20L, 9L, "operator");
}
@@ -79,6 +79,30 @@ class PdaPickSettlementServiceTest {
verify(splits).convergePartialPlanItem(21L, 9L, "operator");
}
@Test
void reusesWarehouseSettlementTargetWhenRetryingAfterLocalRollback() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 3, 1, "PARTIAL");
resolved(List.of(item));
when(splits.lockParent(31L)).thenReturn(parent(3));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 3)));
when(allocations.lockConfirmed(10001L, 31L)).thenAnswer(invocation ->
List.of(allocation(1L, 1, 501L, 3)));
when(warehouse.settlementTarget("internal-token", 31L, "PDA-SETTLE-1-31"))
.thenReturn(ApiResponse.success(999L));
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 2)))));
writes(2, 1);
service.settle(1L, 9L, "operator");
verify(splits).cloneChildWithPurpose(eq(10001L), eq(31L), eq(31L), eq(999L),
eq(1002L), eq("ORDER-REMAINDER"), eq("IN-001-1"), eq(2),
eq(1), eq("REMAINDER"), eq(9L), eq("operator"));
verify(warehouse).settle(eq("internal-token"), org.mockito.ArgumentMatchers.argThat(request ->
request.targetBusinessId() == 999L && request.targetPackageCount() == 2));
}
@Test
void repeatedSettleWithExistingChildDoesNotConsumeOrTransferAgain() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 10, 6, "PARTIAL");