fix(firstleg): allow late initial container fill
This commit is contained in:
+1
-1
@@ -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<PreloadDetail> updateTransport(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ class LoadingListControllerContractTest {
|
||||
.contains("beforeSnapshot")
|
||||
.contains("containerNo")
|
||||
.contains("voyageNo")
|
||||
.contains("可首次补录真实集装箱号")
|
||||
.contains("首次补录真实集装箱号")
|
||||
.contains("校验ISO 6346结构、设备类别和校验位")
|
||||
.contains("PDA任务尚未领取且无任何物理作业事实")
|
||||
.contains("/loading-plans/{planId}/confirm")
|
||||
|
||||
+12
@@ -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}")
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user