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));
|
return ApiResponse.success(service.lifecycle(planId, warehouseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "修改装柜运输字段", description = "已推送且仍为 EDITING、PDA尚未领取或产生物理作业事实时,可首次补录或纠正集装箱号并维护航次;已有柜号的纠正必须提供原因和版本,柜号不能清空。")
|
@Operation(summary = "修改装柜运输字段", description = "已推送后可在装柜完成前首次补录集装箱号;已有柜号的纠正仅限 EDITING 且 PDA 尚未领取或产生物理作业事实,并必须提供原因和版本;柜号不能清空。")
|
||||||
@PatchMapping("/loading-plans/{planId}/transport")
|
@PatchMapping("/loading-plans/{planId}/transport")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||||
public ApiResponse<PreloadDetail> updateTransport(
|
public ApiResponse<PreloadDetail> updateTransport(
|
||||||
|
|||||||
@@ -265,6 +265,36 @@ public interface LoadingListQueryMapper {
|
|||||||
@Param("voyageNo") String voyageNo, @Param("operatorId") long operatorId,
|
@Param("voyageNo") String voyageNo, @Param("operatorId") long operatorId,
|
||||||
@Param("operatorName") String operatorName);
|
@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("""
|
||||||
SELECT CASE WHEN
|
SELECT CASE WHEN
|
||||||
EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ public class LoadingListRepository {
|
|||||||
return mapper.updateTransport(planId, containerNo, voyageNo, operatorId, operatorName);
|
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) {
|
public boolean hasPdaWorkStarted(long planId) {
|
||||||
return mapper.hasPdaWorkStarted(planId) > 0;
|
return mapper.hasPdaWorkStarted(planId) > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,9 +433,10 @@ public class LoadingListService {
|
|||||||
return repository.preloadDetail(before);
|
return repository.preloadDetail(before);
|
||||||
}
|
}
|
||||||
|
|
||||||
requireTransportMutable(before);
|
|
||||||
String containerNo = targetContainerNo(before, request.containerNo());
|
String containerNo = targetContainerNo(before, request.containerNo());
|
||||||
String voyageNo = nullableTrim(request.voyageNo());
|
String voyageNo = nullableTrim(request.voyageNo());
|
||||||
|
boolean initialContainerFill = !StringUtils.hasText(before.getContainerNo()) && containerNo != null;
|
||||||
|
requireTransportMutable(before, initialContainerFill);
|
||||||
boolean correctingContainerNo = StringUtils.hasText(before.getContainerNo())
|
boolean correctingContainerNo = StringUtils.hasText(before.getContainerNo())
|
||||||
&& !java.util.Objects.equals(before.getContainerNo(), containerNo);
|
&& !java.util.Objects.equals(before.getContainerNo(), containerNo);
|
||||||
if (correctingContainerNo && !StringUtils.hasText(reason)) {
|
if (correctingContainerNo && !StringUtils.hasText(reason)) {
|
||||||
@@ -451,8 +452,12 @@ public class LoadingListService {
|
|||||||
throw conflict("集装箱号已存在");
|
throw conflict("集装箱号已存在");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (repository.updateTransport(planId, request.expectedVersion(), containerNo, voyageNo,
|
int updated = initialContainerFill
|
||||||
operatorId, trim(operatorName)) != 1) {
|
? 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领取或物理作业状态已变化,请刷新后重试");
|
throw conflict("预装计划、PDA领取或物理作业状态已变化,请刷新后重试");
|
||||||
}
|
}
|
||||||
} catch (DuplicateKeyException exception) {
|
} catch (DuplicateKeyException exception) {
|
||||||
@@ -583,12 +588,15 @@ public class LoadingListService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireTransportMutable(AggregateRow row) {
|
private void requireTransportMutable(AggregateRow row, boolean initialContainerFill) {
|
||||||
if (!"EDITING".equals(row.getPlanStatus())) {
|
if (initialContainerFill && !Set.of("EDITING", "LOADING").contains(row.getPlanStatus())) {
|
||||||
|
throw conflict("仅装柜完成前允许首次补录真实集装箱号");
|
||||||
|
}
|
||||||
|
if (!initialContainerFill && !"EDITING".equals(row.getPlanStatus())) {
|
||||||
throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号");
|
throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号");
|
||||||
}
|
}
|
||||||
if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段");
|
if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段");
|
||||||
if (repository.hasPdaWorkStarted(row.getPlanId())) {
|
if (!initialContainerFill && repository.hasPdaWorkStarted(row.getPlanId())) {
|
||||||
throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ paths:
|
|||||||
patch:
|
patch:
|
||||||
summary: 修改预装运输信息
|
summary: 修改预装运输信息
|
||||||
description: >-
|
description: >-
|
||||||
仅已推送且仍为 EDITING、PDA任务尚未领取且无任何物理作业事实时允许。可首次补录真实集装箱号;已有柜号的纠正必须提供原因和当前版本;
|
已推送后可在 EDITING 或 LOADING 阶段首次补录真实集装箱号;已有柜号的纠正仅限仍为 EDITING、PDA任务尚未领取且无任何物理作业事实时,并必须提供原因和当前版本;
|
||||||
containerNo 为 null 表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位,空白字符串非法且禁止清空。
|
containerNo 为 null 表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位,空白字符串非法且禁止清空。
|
||||||
x-permission: platform:firstleg:preload:edit
|
x-permission: platform:firstleg:preload:edit
|
||||||
parameters:
|
parameters:
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ class LoadingListControllerContractTest {
|
|||||||
.contains("beforeSnapshot")
|
.contains("beforeSnapshot")
|
||||||
.contains("containerNo")
|
.contains("containerNo")
|
||||||
.contains("voyageNo")
|
.contains("voyageNo")
|
||||||
.contains("可首次补录真实集装箱号")
|
.contains("首次补录真实集装箱号")
|
||||||
.contains("校验ISO 6346结构、设备类别和校验位")
|
.contains("校验ISO 6346结构、设备类别和校验位")
|
||||||
.contains("PDA任务尚未领取且无任何物理作业事实")
|
.contains("PDA任务尚未领取且无任何物理作业事实")
|
||||||
.contains("/loading-plans/{planId}/confirm")
|
.contains("/loading-plans/{planId}/confirm")
|
||||||
|
|||||||
+12
@@ -80,6 +80,8 @@ class LoadingListQueryMapperContractTest {
|
|||||||
void mutationsShouldUseStatusAndOptimisticVersionConditions() throws Exception {
|
void mutationsShouldUseStatusAndOptimisticVersionConditions() throws Exception {
|
||||||
String touch = sql("touchEditable", Update.class);
|
String touch = sql("touchEditable", Update.class);
|
||||||
String transport = sql("updateTransport", 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 confirmPreload = sql("confirmLinkedPreload", Update.class);
|
||||||
String confirmPlan = sql("touchForPreloadConfirmation", Update.class);
|
String confirmPlan = sql("touchForPreloadConfirmation", Update.class);
|
||||||
String advance = sql("advance", Update.class);
|
String advance = sql("advance", Update.class);
|
||||||
@@ -98,6 +100,16 @@ class LoadingListQueryMapperContractTest {
|
|||||||
.contains("fl_loading_pick_task", "fl_loading_pick_assignment")
|
.contains("fl_loading_pick_task", "fl_loading_pick_assignment")
|
||||||
.doesNotContain("DELETE")
|
.doesNotContain("DELETE")
|
||||||
.doesNotContain("fl_loading_plan_item");
|
.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)
|
assertThat(advance)
|
||||||
.contains("plan_status = #{fromStatus}")
|
.contains("plan_status = #{fromStatus}")
|
||||||
.contains("plan_status = #{toStatus}")
|
.contains("plan_status = #{toStatus}")
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class LoadingListServiceTest {
|
|||||||
afterFill.setPreloadPushedAt(missing.getPreloadPushedAt());
|
afterFill.setPreloadPushedAt(missing.getPreloadPushedAt());
|
||||||
afterFill.setContainerNo("CSQU3054383");
|
afterFill.setContainerNo("CSQU3054383");
|
||||||
when(repository.aggregate(10L)).thenReturn(loading, missing, afterFill);
|
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);
|
.thenReturn(1);
|
||||||
when(repository.preloadDetail(afterFill)).thenReturn(
|
when(repository.preloadDetail(afterFill)).thenReturn(
|
||||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
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",
|
service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
|
||||||
new TransportUpdateRequest("CSQU3054383", null, 3, null));
|
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");
|
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
|
@Test
|
||||||
void correctingExistingContainerNumberStillRequiresReason() {
|
void correctingExistingContainerNumberStillRequiresReason() {
|
||||||
AggregateRow before = row("EDITING", 3);
|
AggregateRow before = row("EDITING", 3);
|
||||||
|
|||||||
Reference in New Issue
Block a user