diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java index 89f643dfc..59eb7bf0e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformLoadingListController.java @@ -72,7 +72,7 @@ public class PlatformLoadingListController { return ApiResponse.success(service.lifecycle(planId, warehouseId)); } - @Operation(summary = "修改装柜运输字段", description = "已推送且仍为 EDITING、PDA尚未领取或产生物理作业事实时,可首次补录或纠正集装箱号并维护航次;已有柜号的纠正必须提供原因和版本,柜号不能清空。") + @Operation(summary = "修改装柜运输字段", description = "已推送后可在装柜完成前首次补录集装箱号;已有柜号的纠正仅限 EDITING 且 PDA 尚未领取或产生物理作业事实,并必须提供原因和版本;柜号不能清空。") @PatchMapping("/loading-plans/{planId}/transport") @PreAuthorize("hasAuthority('platform:firstleg:preload:edit')") public ApiResponse updateTransport( diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java index 1bb4f1a1c..cadc31d22 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingListQueryMapper.java @@ -265,6 +265,36 @@ public interface LoadingListQueryMapper { @Param("voyageNo") String voyageNo, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); + @Update(""" + UPDATE fl_loading_plan p + JOIN fl_container_booking b ON b.id=p.container_booking_id + SET p.updated_by=#{operatorId}, p.updated_by_name=#{operatorName}, + p.updated_at=CURRENT_TIMESTAMP(3), p.version=p.version+1 + WHERE p.id=#{planId} AND p.plan_status IN ('EDITING','LOADING') + AND p.preload_pushed_at IS NOT NULL AND p.version=#{expectedVersion} + AND b.container_no IS NULL + """) + int touchForInitialContainerFill(@Param("planId") long planId, + @Param("expectedVersion") int expectedVersion, + @Param("operatorId") long operatorId, + @Param("operatorName") String operatorName); + + @Update(""" + UPDATE fl_container_booking b + JOIN fl_loading_plan p ON p.container_booking_id=b.id + SET b.container_no=#{containerNo}, b.voyage_no=#{voyageNo}, + b.updated_by=#{operatorId}, b.updated_by_name=#{operatorName}, + b.updated_at=CURRENT_TIMESTAMP(3), b.version=b.version+1 + WHERE p.id=#{planId} AND p.plan_status IN ('EDITING','LOADING') + AND p.preload_pushed_at IS NOT NULL AND b.container_no IS NULL + AND #{containerNo} IS NOT NULL + """) + int fillInitialContainerNo(@Param("planId") long planId, + @Param("containerNo") String containerNo, + @Param("voyageNo") String voyageNo, + @Param("operatorId") long operatorId, + @Param("operatorName") String operatorName); + @Select(""" SELECT CASE WHEN EXISTS (SELECT 1 FROM fl_loading_pick_task task diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java index 27f5333d6..b1fbd5d5f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/LoadingListRepository.java @@ -86,6 +86,14 @@ public class LoadingListRepository { return mapper.updateTransport(planId, containerNo, voyageNo, operatorId, operatorName); } + public int fillInitialContainerNo(long planId, int expectedVersion, String containerNo, String voyageNo, + long operatorId, String operatorName) { + if (mapper.touchForInitialContainerFill(planId, expectedVersion, operatorId, operatorName) != 1) { + return 0; + } + return mapper.fillInitialContainerNo(planId, containerNo, voyageNo, operatorId, operatorName); + } + public boolean hasPdaWorkStarted(long planId) { return mapper.hasPdaWorkStarted(planId) > 0; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java index bf2838354..ff26f6356 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java @@ -433,9 +433,10 @@ public class LoadingListService { return repository.preloadDetail(before); } - requireTransportMutable(before); String containerNo = targetContainerNo(before, request.containerNo()); String voyageNo = nullableTrim(request.voyageNo()); + boolean initialContainerFill = !StringUtils.hasText(before.getContainerNo()) && containerNo != null; + requireTransportMutable(before, initialContainerFill); boolean correctingContainerNo = StringUtils.hasText(before.getContainerNo()) && !java.util.Objects.equals(before.getContainerNo(), containerNo); if (correctingContainerNo && !StringUtils.hasText(reason)) { @@ -451,8 +452,12 @@ public class LoadingListService { throw conflict("集装箱号已存在"); } try { - if (repository.updateTransport(planId, request.expectedVersion(), containerNo, voyageNo, - operatorId, trim(operatorName)) != 1) { + int updated = initialContainerFill + ? repository.fillInitialContainerNo(planId, request.expectedVersion(), containerNo, voyageNo, + operatorId, trim(operatorName)) + : repository.updateTransport(planId, request.expectedVersion(), containerNo, voyageNo, + operatorId, trim(operatorName)); + if (updated != 1) { throw conflict("预装计划、PDA领取或物理作业状态已变化,请刷新后重试"); } } catch (DuplicateKeyException exception) { @@ -583,12 +588,15 @@ public class LoadingListService { } } - private void requireTransportMutable(AggregateRow row) { - if (!"EDITING".equals(row.getPlanStatus())) { + private void requireTransportMutable(AggregateRow row, boolean initialContainerFill) { + if (initialContainerFill && !Set.of("EDITING", "LOADING").contains(row.getPlanStatus())) { + throw conflict("仅装柜完成前允许首次补录真实集装箱号"); + } + if (!initialContainerFill && !"EDITING".equals(row.getPlanStatus())) { throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号"); } if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段"); - if (repository.hasPdaWorkStarted(row.getPlanId())) { + if (!initialContainerFill && repository.hasPdaWorkStarted(row.getPlanId())) { throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号"); } } diff --git a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml index 139bfb523..e69d52c52 100644 --- a/firstleg/src/main/resources/openapi/loading-lists-v1.yaml +++ b/firstleg/src/main/resources/openapi/loading-lists-v1.yaml @@ -49,7 +49,7 @@ paths: patch: summary: 修改预装运输信息 description: >- - 仅已推送且仍为 EDITING、PDA任务尚未领取且无任何物理作业事实时允许。可首次补录真实集装箱号;已有柜号的纠正必须提供原因和当前版本; + 已推送后可在 EDITING 或 LOADING 阶段首次补录真实集装箱号;已有柜号的纠正仅限仍为 EDITING、PDA任务尚未领取且无任何物理作业事实时,并必须提供原因和当前版本; containerNo 为 null 表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位,空白字符串非法且禁止清空。 x-permission: platform:firstleg:preload:edit parameters: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java index 25b5a0b42..21563a38f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/LoadingListControllerContractTest.java @@ -69,7 +69,7 @@ class LoadingListControllerContractTest { .contains("beforeSnapshot") .contains("containerNo") .contains("voyageNo") - .contains("可首次补录真实集装箱号") + .contains("首次补录真实集装箱号") .contains("校验ISO 6346结构、设备类别和校验位") .contains("PDA任务尚未领取且无任何物理作业事实") .contains("/loading-plans/{planId}/confirm") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java index ba1f02508..ee7e3fb69 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/LoadingListQueryMapperContractTest.java @@ -80,6 +80,8 @@ class LoadingListQueryMapperContractTest { void mutationsShouldUseStatusAndOptimisticVersionConditions() throws Exception { String touch = sql("touchEditable", Update.class); String transport = sql("updateTransport", Update.class); + String touchInitialFill = sql("touchForInitialContainerFill", Update.class); + String initialFill = sql("fillInitialContainerNo", Update.class); String confirmPreload = sql("confirmLinkedPreload", Update.class); String confirmPlan = sql("touchForPreloadConfirmation", Update.class); String advance = sql("advance", Update.class); @@ -98,6 +100,16 @@ class LoadingListQueryMapperContractTest { .contains("fl_loading_pick_task", "fl_loading_pick_assignment") .doesNotContain("DELETE") .doesNotContain("fl_loading_plan_item"); + assertThat(touchInitialFill) + .contains("p.plan_status IN ('EDITING','LOADING')") + .contains("p.version=#{expectedVersion}") + .contains("b.container_no IS NULL") + .doesNotContain("fl_loading_pick_assignment", "item_status"); + assertThat(initialFill) + .contains("p.plan_status IN ('EDITING','LOADING')") + .contains("b.container_no IS NULL") + .contains("#{containerNo} IS NOT NULL") + .doesNotContain("fl_loading_pick_assignment", "item_status"); assertThat(advance) .contains("plan_status = #{fromStatus}") .contains("plan_status = #{toStatus}") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java index 31bf5cfcb..9a7c18f29 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingListServiceTest.java @@ -218,7 +218,7 @@ class LoadingListServiceTest { afterFill.setPreloadPushedAt(missing.getPreloadPushedAt()); afterFill.setContainerNo("CSQU3054383"); when(repository.aggregate(10L)).thenReturn(loading, missing, afterFill); - when(repository.updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator")) + when(repository.fillInitialContainerNo(10L, 3, "CSQU3054383", null, 9L, "operator")) .thenReturn(1); when(repository.preloadDetail(afterFill)).thenReturn( new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of())); @@ -229,10 +229,34 @@ class LoadingListServiceTest { service.updateTransport(10L, 100L, 9L, "operator", "missing-key", new TransportUpdateRequest("CSQU3054383", null, 3, null)); - verify(repository).updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator"); + verify(repository).fillInitialContainerNo(10L, 3, "CSQU3054383", null, 9L, "operator"); verify(loadingPickService).beginIfReady(30L, 9L, "operator"); } + @Test + void loadingPlanWithClaimedAndPickedPdaTaskStillAllowsInitialContainerFill() { + AggregateRow before = row("LOADING", 7); + before.setPreloadPushedAt(java.time.LocalDateTime.now()); + AggregateRow after = row("LOADING", 8); + after.setPreloadPushedAt(before.getPreloadPushedAt()); + after.setContainerNo("CSQU3054383"); + when(repository.aggregate(10L)).thenReturn(before, after); + when(repository.hasPdaWorkStarted(10L)).thenReturn(true); + when(repository.fillInitialContainerNo(10L, 7, "CSQU3054383", "023E", 9L, "operator")) + .thenReturn(1); + when(repository.preloadDetail(after)).thenReturn( + new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of())); + + service.updateTransport(10L, 100L, 9L, "operator", "late-fill-key", + new TransportUpdateRequest("CSQU3054383", "023E", 7, null)); + + verify(repository).fillInitialContainerNo(10L, 7, "CSQU3054383", "023E", 9L, "operator"); + verify(repository, never()).hasPdaWorkStarted(10L); + verify(repository).appendHistory(eq(10L), anyString(), eq("UPDATE_PRELOAD_TRANSPORT"), + eq("LOADING"), eq("LOADING"), anyString(), anyString(), eq(9L), eq("operator"), + org.mockito.ArgumentMatchers.isNull()); + } + @Test void correctingExistingContainerNumberStillRequiresReason() { AggregateRow before = row("EDITING", 3);