优化
This commit is contained in:
+2
-2
@@ -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 的预装清单可补录或修改集装箱号与航次;LOADING 阶段按稳定柜次身份规则维护,不能清空已创建 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(
|
||||||
@@ -86,7 +86,7 @@ public class PlatformLoadingListController {
|
|||||||
planId, warehouseId, user.userId(), user.username(), idempotencyKey, request));
|
planId, warehouseId, user.userId(), user.username(), idempotencyKey, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部关联入库预装票据;要求已推送、真实集装箱号已录入、关联订舱已放舱。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部关联入库预装票据;要求已推送且关联订舱已放舱,真实集装箱号可在PDA确认装柜完成前补录。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
||||||
@PostMapping("/loading-plans/{planId}/confirm")
|
@PostMapping("/loading-plans/{planId}/confirm")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||||
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
|
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public final class LoadingListDtos {
|
|||||||
@Size(max = 64) String containerNo,
|
@Size(max = 64) String containerNo,
|
||||||
@Size(max = 64) String voyageNo,
|
@Size(max = 64) String voyageNo,
|
||||||
@NotNull @Min(0) Integer expectedVersion,
|
@NotNull @Min(0) Integer expectedVersion,
|
||||||
@NotBlank @Size(max = 500) String operationReason
|
@Size(max = 500) String operationReason
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ public interface LoadingListQueryMapper {
|
|||||||
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
|
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
|
||||||
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
|
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
|
||||||
WHERE p.id = #{planId} AND p.plan_status='EDITING' AND p.preload_pushed_at IS NOT NULL
|
WHERE p.id = #{planId} AND p.plan_status='EDITING' AND p.preload_pushed_at IS NOT NULL
|
||||||
AND #{containerNo} IS NOT NULL
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
AND NOT EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
||||||
JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
||||||
WHERE task.loading_plan_id=p.id)
|
WHERE task.loading_plan_id=p.id)
|
||||||
|
|||||||
@@ -151,13 +151,12 @@ public class LoadingListRepository {
|
|||||||
java.util.ArrayList<String> blockers = new java.util.ArrayList<>();
|
java.util.ArrayList<String> blockers = new java.util.ArrayList<>();
|
||||||
if (!pushed) blockers.add("PRELOAD_NOT_PUSHED");
|
if (!pushed) blockers.add("PRELOAD_NOT_PUSHED");
|
||||||
if (!editing) blockers.add("LOADING_PLAN_NOT_EDITING");
|
if (!editing) blockers.add("LOADING_PLAN_NOT_EDITING");
|
||||||
if (!containerReady) blockers.add("CONTAINER_NO_MISSING");
|
|
||||||
if (linkCount == 0) blockers.add("PRELOAD_LINK_MISSING");
|
if (linkCount == 0) blockers.add("PRELOAD_LINK_MISSING");
|
||||||
if (!oceanLinked) blockers.add("OCEAN_BOOKING_NOT_LINKED");
|
if (!oceanLinked) blockers.add("OCEAN_BOOKING_NOT_LINKED");
|
||||||
else if (!released) blockers.add("OCEAN_BOOKING_NOT_RELEASED");
|
else if (!released) blockers.add("OCEAN_BOOKING_NOT_RELEASED");
|
||||||
if (taskCreated) blockers.add("PDA_TASK_ALREADY_CREATED");
|
if (taskCreated) blockers.add("PDA_TASK_ALREADY_CREATED");
|
||||||
boolean allConfirmed = linkCount > 0 && confirmedCount == linkCount;
|
boolean allConfirmed = linkCount > 0 && confirmedCount == linkCount;
|
||||||
boolean prerequisitesReady = pushed && editing && containerReady && linkCount > 0 && released;
|
boolean prerequisitesReady = pushed && editing && linkCount > 0 && released;
|
||||||
boolean canConfirm = prerequisitesReady && !taskCreated && !allConfirmed;
|
boolean canConfirm = prerequisitesReady && !taskCreated && !allConfirmed;
|
||||||
if (allConfirmed) blockers.add("PRELOAD_ALREADY_CONFIRMED");
|
if (allConfirmed) blockers.add("PRELOAD_ALREADY_CONFIRMED");
|
||||||
String confirmationStatus = allConfirmed ? "CONFIRMED"
|
String confirmationStatus = allConfirmed ? "CONFIRMED"
|
||||||
|
|||||||
@@ -423,7 +423,6 @@ public class LoadingListService {
|
|||||||
AggregateRow before = requirePlan(planId);
|
AggregateRow before = requirePlan(planId);
|
||||||
requireWarehouse(before, warehouseId);
|
requireWarehouse(before, warehouseId);
|
||||||
String reason = trim(request.operationReason());
|
String reason = trim(request.operationReason());
|
||||||
if (!StringUtils.hasText(reason)) throw badRequest("纠错原因不能为空");
|
|
||||||
String operationId = operationId("TRANSPORT", planId, idempotencyKey);
|
String operationId = operationId("TRANSPORT", planId, idempotencyKey);
|
||||||
HistoryRow replay = repository.history(operationId);
|
HistoryRow replay = repository.history(operationId);
|
||||||
if (replay != null) {
|
if (replay != null) {
|
||||||
@@ -434,15 +433,21 @@ public class LoadingListService {
|
|||||||
return repository.preloadDetail(before);
|
return repository.preloadDetail(before);
|
||||||
}
|
}
|
||||||
|
|
||||||
requireTransportCorrectionMutable(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 correctingContainerNo = StringUtils.hasText(before.getContainerNo())
|
||||||
|
&& !java.util.Objects.equals(before.getContainerNo(), containerNo);
|
||||||
|
if (correctingContainerNo && !StringUtils.hasText(reason)) {
|
||||||
|
throw badRequest("纠正已有真实集装箱号时纠错原因不能为空");
|
||||||
|
}
|
||||||
String requestHash = transportHash(planId, request.expectedVersion(), containerNo, voyageNo, reason);
|
String requestHash = transportHash(planId, request.expectedVersion(), containerNo, voyageNo, reason);
|
||||||
if (java.util.Objects.equals(before.getContainerNo(), containerNo)
|
if (java.util.Objects.equals(before.getContainerNo(), containerNo)
|
||||||
&& java.util.Objects.equals(before.getVoyageNo(), voyageNo)) {
|
&& java.util.Objects.equals(before.getVoyageNo(), voyageNo)) {
|
||||||
return repository.preloadDetail(before);
|
return repository.preloadDetail(before);
|
||||||
}
|
}
|
||||||
if (containerRepository.containerNoExists(containerNo, before.getContainerBookingId())) {
|
if (containerNo != null
|
||||||
|
&& containerRepository.containerNoExists(containerNo, before.getContainerBookingId())) {
|
||||||
throw conflict("集装箱号已存在");
|
throw conflict("集装箱号已存在");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -464,8 +469,9 @@ public class LoadingListService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String targetContainerNo(AggregateRow before, String requested) {
|
private static String targetContainerNo(AggregateRow before, String requested) {
|
||||||
return requested == null ? Iso6346ContainerNumber.require(before.getContainerNo())
|
if (requested != null) return Iso6346ContainerNumber.require(requested);
|
||||||
: Iso6346ContainerNumber.require(requested);
|
return StringUtils.hasText(before.getContainerNo())
|
||||||
|
? Iso6346ContainerNumber.require(before.getContainerNo()) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String transportHash(long planId, int version, String containerNo,
|
private static String transportHash(long planId, int version, String containerNo,
|
||||||
@@ -577,14 +583,11 @@ public class LoadingListService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireTransportCorrectionMutable(AggregateRow row) {
|
private void requireTransportMutable(AggregateRow row) {
|
||||||
if (!"EDITING".equals(row.getPlanStatus())) {
|
if (!"EDITING".equals(row.getPlanStatus())) {
|
||||||
throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号");
|
throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号");
|
||||||
}
|
}
|
||||||
if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段");
|
if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段");
|
||||||
if (!StringUtils.hasText(row.getContainerNo())) {
|
|
||||||
throw conflict("真实集装箱号只能在关联已放舱订舱时首次写入");
|
|
||||||
}
|
|
||||||
if (repository.hasPdaWorkStarted(row.getPlanId())) {
|
if (repository.hasPdaWorkStarted(row.getPlanId())) {
|
||||||
throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryResource;
|
|||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InboundInventory;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.InboundInventory;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
||||||
|
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||||
@@ -113,11 +114,11 @@ public class LoadingPickService {
|
|||||||
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
|
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
|
||||||
private String internalToken;
|
private String internalToken;
|
||||||
|
|
||||||
/** Opens the field task only after planning, transport identity and ocean release have converged. */
|
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||||
@Transactional
|
@Transactional
|
||||||
public void beginIfReady(long containerBookingId, long operatorId, String operatorName) {
|
public void beginIfReady(long containerBookingId, long operatorId, String operatorName) {
|
||||||
var booking = smartLoadingRepository.booking(containerBookingId);
|
var booking = smartLoadingRepository.booking(containerBookingId);
|
||||||
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) return;
|
if (booking == null) return;
|
||||||
var plan = smartLoadingRepository.planByBooking(containerBookingId);
|
var plan = smartLoadingRepository.planByBooking(containerBookingId);
|
||||||
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||||
var links = loadingModelBridge.byLoadingPlan(plan.getId());
|
var links = loadingModelBridge.byLoadingPlan(plan.getId());
|
||||||
@@ -137,10 +138,6 @@ public class LoadingPickService {
|
|||||||
if (!Set.of("PENDING", "CONFIRMING").contains(link.preloadStatus())) {
|
if (!Set.of("PENDING", "CONFIRMING").contains(link.preloadStatus())) {
|
||||||
throw conflict("预装清单已确认或当前状态不允许确认");
|
throw conflict("预装清单已确认或当前状态不允许确认");
|
||||||
}
|
}
|
||||||
var booking = smartLoadingRepository.booking(link.containerBookingId());
|
|
||||||
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) {
|
|
||||||
throw conflict("确认预装前必须录入真实集装箱号");
|
|
||||||
}
|
|
||||||
var allocation = oceanBookingRepository.allocationByContainer(link.containerBookingId());
|
var allocation = oceanBookingRepository.allocationByContainer(link.containerBookingId());
|
||||||
if (allocation == null) throw conflict("确认预装前必须关联真实订舱柜");
|
if (allocation == null) throw conflict("确认预装前必须关联真实订舱柜");
|
||||||
var oceanBooking = oceanBookingRepository.find(allocation.getOceanBookingId());
|
var oceanBooking = oceanBookingRepository.find(allocation.getOceanBookingId());
|
||||||
@@ -462,6 +459,7 @@ public class LoadingPickService {
|
|||||||
requireSameCompletionCommand(replay, taskId, requestHash, operatorId);
|
requireSameCompletionCommand(replay, taskId, requestHash, operatorId);
|
||||||
if ("FAILED".equals(replay.getCommandStatus())) {
|
if ("FAILED".equals(replay.getCommandStatus())) {
|
||||||
LoadingPickTaskEntity failed = requiredTaskForUpdate(taskId);
|
LoadingPickTaskEntity failed = requiredTaskForUpdate(taskId);
|
||||||
|
requireCompletionContainerNumber(taskId);
|
||||||
if (!"COMPLETION_FAILED".equals(failed.getTaskStatus())
|
if (!"COMPLETION_FAILED".equals(failed.getTaskStatus())
|
||||||
|| !completionCommands.resume(replay.getId()) || repository.resumeCompletion(taskId) != 1) {
|
|| !completionCommands.resume(replay.getId()) || repository.resumeCompletion(taskId) != 1) {
|
||||||
throw conflict("装柜完成命令状态已变化,请刷新后重试");
|
throw conflict("装柜完成命令状态已变化,请刷新后重试");
|
||||||
@@ -485,6 +483,7 @@ public class LoadingPickService {
|
|||||||
if (!request.expectedTaskVersion().equals(task.getVersion())) {
|
if (!request.expectedTaskVersion().equals(task.getVersion())) {
|
||||||
throw conflict("PDA任务版本已变化,请刷新后复核");
|
throw conflict("PDA任务版本已变化,请刷新后复核");
|
||||||
}
|
}
|
||||||
|
requireCompletionContainerNumber(taskId);
|
||||||
if (!repository.allTaskItemsResolved(taskId)) {
|
if (!repository.allTaskItemsResolved(taskId)) {
|
||||||
throw conflict("存在尚未处理的PDA拣货明细,不能确认装柜完成");
|
throw conflict("存在尚未处理的PDA拣货明细,不能确认装柜完成");
|
||||||
}
|
}
|
||||||
@@ -514,6 +513,18 @@ public class LoadingPickService {
|
|||||||
return view(requiredTask(taskId), null);
|
return view(requiredTask(taskId), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requireCompletionContainerNumber(long taskId) {
|
||||||
|
ContainerBookingEntity booking = repository.activeBookingByTask(taskId);
|
||||||
|
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) {
|
||||||
|
throw conflict("PDA确认装柜完成前必须录入真实集装箱号");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Iso6346ContainerNumber.require(booking.getContainerNo());
|
||||||
|
} catch (BizException exception) {
|
||||||
|
throw conflict("PDA确认装柜完成前必须录入符合ISO 6346的真实集装箱号");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Called only by the completion worker, after its durable lease has committed. */
|
/** Called only by the completion worker, after its durable lease has committed. */
|
||||||
public void processCompletion(LoadingCompletionCommandEntity command, String worker) {
|
public void processCompletion(LoadingCompletionCommandEntity command, String worker) {
|
||||||
long taskId = command.getPickTaskId();
|
long taskId = command.getPickTaskId();
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ paths:
|
|||||||
patch:
|
patch:
|
||||||
summary: 修改预装运输信息
|
summary: 修改预装运输信息
|
||||||
description: >-
|
description: >-
|
||||||
唯一纠错入口。仅已推送且仍为 EDITING、真实柜号已由 ocean-booking-link 首次写入、PDA任务尚未领取且无任何物理作业事实时允许;
|
仅已推送且仍为 EDITING、PDA任务尚未领取且无任何物理作业事实时允许。可首次补录真实集装箱号;已有柜号的纠正必须提供原因和当前版本;
|
||||||
containerNo 为空表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位。禁止清空,LOADING/COMPLETED全部拒绝。
|
containerNo 为 null 表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位,空白字符串非法且禁止清空。
|
||||||
x-permission: platform:firstleg:preload:edit
|
x-permission: platform:firstleg:preload:edit
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/PlanId'}
|
- {$ref: '#/components/parameters/PlanId'}
|
||||||
@@ -66,7 +66,7 @@ paths:
|
|||||||
/api/platform/v1/first-leg/loading-plans/{planId}/confirm:
|
/api/platform/v1/first-leg/loading-plans/{planId}/confirm:
|
||||||
post:
|
post:
|
||||||
summary: 原子确认整份预装计划
|
summary: 原子确认整份预装计划
|
||||||
description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、真实集装箱号已录入、稳定 bridge 完整、真实订舱已关联并为 RELEASED;任一票不满足均不写入。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。
|
description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、稳定 bridge 完整、真实订舱已关联并为 RELEASED;真实集装箱号可在PDA确认装柜完成前补录。任一票不满足均不写入。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。
|
||||||
x-permission: platform:firstleg:preload:edit
|
x-permission: platform:firstleg:preload:edit
|
||||||
parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
@@ -313,11 +313,11 @@ components:
|
|||||||
pdaTaskStatus: {type: string, nullable: true, description: PDA task 当前状态}
|
pdaTaskStatus: {type: string, nullable: true, description: PDA task 当前状态}
|
||||||
confirmationStatus: {type: string, enum: [PENDING, PARTIALLY_CONFIRMED, CONFIRMED], description: 整份计划的预装确认状态}
|
confirmationStatus: {type: string, enum: [PENDING, PARTIALLY_CONFIRMED, CONFIRMED], description: 整份计划的预装确认状态}
|
||||||
canConfirm: {type: boolean, description: 当前是否可调用计划级确认接口}
|
canConfirm: {type: boolean, description: 当前是否可调用计划级确认接口}
|
||||||
readyForPda: {type: boolean, description: PDA 创建前置条件是否全部满足;task 已创建后仍为 true}
|
readyForPda: {type: boolean, description: PDA 创建前置条件是否全部满足;不依赖真实集装箱号,task 已创建后仍为 true}
|
||||||
blockingReasons:
|
blockingReasons:
|
||||||
type: array
|
type: array
|
||||||
description: 阻止计划确认的稳定原因码
|
description: 阻止计划确认的稳定原因码
|
||||||
items: {type: string, enum: [PRELOAD_NOT_PUSHED, LOADING_PLAN_NOT_EDITING, CONTAINER_NO_MISSING, PRELOAD_LINK_MISSING, OCEAN_BOOKING_NOT_LINKED, OCEAN_BOOKING_NOT_RELEASED, PDA_TASK_ALREADY_CREATED, PRELOAD_ALREADY_CONFIRMED]}
|
items: {type: string, enum: [PRELOAD_NOT_PUSHED, LOADING_PLAN_NOT_EDITING, PRELOAD_LINK_MISSING, OCEAN_BOOKING_NOT_LINKED, OCEAN_BOOKING_NOT_RELEASED, PDA_TASK_ALREADY_CREATED, PRELOAD_ALREADY_CONFIRMED]}
|
||||||
ConfirmPreloadRequest:
|
ConfirmPreloadRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [expectedVersion, operationReason]
|
required: [expectedVersion, operationReason]
|
||||||
@@ -428,4 +428,4 @@ components:
|
|||||||
containerNo: {type: string, maxLength: 64, nullable: true, description: 新真实集装箱号;null表示保持当前值,空白字符串非法,禁止清空}
|
containerNo: {type: string, maxLength: 64, nullable: true, description: 新真实集装箱号;null表示保持当前值,空白字符串非法,禁止清空}
|
||||||
voyageNo: {type: string, maxLength: 64, nullable: true, description: 航次}
|
voyageNo: {type: string, maxLength: 64, nullable: true, description: 航次}
|
||||||
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前乐观锁版本}
|
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前乐观锁版本}
|
||||||
operationReason: {type: string, minLength: 1, maxLength: 500, description: 必填纠错原因}
|
operationReason: {type: string, minLength: 1, maxLength: 500, nullable: true, description: 纠正已有真实集装箱号时必填;首次补录或保持当前柜号时可省略}
|
||||||
|
|||||||
+2
-2
@@ -100,10 +100,10 @@ class LoadingListControllerContractTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transportCorrectionRequiresReasonAndKeepsNullContainerAsUnchanged() {
|
void transportCorrectionReasonIsRuntimeConditionalAndNullContainerKeepsCurrentValue() {
|
||||||
assertThat(component(TransportUpdateRequest.class, "operationReason").getAccessor()
|
assertThat(component(TransportUpdateRequest.class, "operationReason").getAccessor()
|
||||||
.getAnnotation(NotBlank.class))
|
.getAnnotation(NotBlank.class))
|
||||||
.isNotNull();
|
.isNull();
|
||||||
assertThat(component(TransportUpdateRequest.class, "containerNo").getAccessor()
|
assertThat(component(TransportUpdateRequest.class, "containerNo").getAccessor()
|
||||||
.getAnnotation(NotBlank.class))
|
.getAnnotation(NotBlank.class))
|
||||||
.isNull();
|
.isNull();
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ class LoadingListQueryMapperContractTest {
|
|||||||
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
|
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
|
||||||
.contains("p.plan_status='EDITING'")
|
.contains("p.plan_status='EDITING'")
|
||||||
.contains("preload_pushed_at IS NOT NULL")
|
.contains("preload_pushed_at IS NOT NULL")
|
||||||
.contains("#{containerNo} IS NOT NULL")
|
.doesNotContain("#{containerNo} IS NOT NULL")
|
||||||
.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");
|
||||||
|
|||||||
@@ -72,4 +72,30 @@ class LoadingListRepositoryTest {
|
|||||||
assertThat(confirmed.planId()).isEqualTo(41L);
|
assertThat(confirmed.planId()).isEqualTo(41L);
|
||||||
assertThat(confirmed.containerBookingId()).isEqualTo(61L);
|
assertThat(confirmed.containerBookingId()).isEqualTo(61L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingContainerNumberDoesNotBlockPreloadConfirmationOrPdaReadiness() {
|
||||||
|
AggregateRow row = new AggregateRow();
|
||||||
|
row.setPlanId(41L);
|
||||||
|
row.setContainerBookingId(61L);
|
||||||
|
row.setPlanNo("LP-41");
|
||||||
|
row.setPlanStatus("EDITING");
|
||||||
|
row.setVersion(1);
|
||||||
|
row.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||||
|
row.setPreloadLinkCount(2L);
|
||||||
|
row.setConfirmedPreloadCount(0L);
|
||||||
|
row.setOceanBookingId(81L);
|
||||||
|
row.setOceanBookingStatus("RELEASED");
|
||||||
|
when(mapper.selectItems(41L)).thenReturn(List.of());
|
||||||
|
when(mapper.selectHistories(41L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
var pending = repository.preloadDetail(row).summary().readiness();
|
||||||
|
assertThat(pending.containerNoReady()).isFalse();
|
||||||
|
assertThat(pending.canConfirm()).isTrue();
|
||||||
|
assertThat(pending.blockingReasons()).doesNotContain("CONTAINER_NO_MISSING");
|
||||||
|
|
||||||
|
row.setConfirmedPreloadCount(2L);
|
||||||
|
var confirmed = repository.preloadDetail(row).summary().readiness();
|
||||||
|
assertThat(confirmed.readyForPda()).isTrue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,20 +208,43 @@ class LoadingListServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadingPlanAndMissingInitialOceanBookingNumberBothRejectCorrection() {
|
void loadingPlanRejectsTransportUpdateButPushedEditingPlanAllowsFirstContainerFill() {
|
||||||
AggregateRow loading = row("LOADING", 3);
|
AggregateRow loading = row("LOADING", 3);
|
||||||
loading.setPreloadPushedAt(java.time.LocalDateTime.now());
|
loading.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||||
loading.setContainerNo("MSCU6639870");
|
loading.setContainerNo("MSCU6639870");
|
||||||
AggregateRow missing = row("EDITING", 3);
|
AggregateRow missing = row("EDITING", 3);
|
||||||
missing.setPreloadPushedAt(java.time.LocalDateTime.now());
|
missing.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||||
when(repository.aggregate(10L)).thenReturn(loading, missing);
|
AggregateRow afterFill = row("EDITING", 4);
|
||||||
|
afterFill.setPreloadPushedAt(missing.getPreloadPushedAt());
|
||||||
|
afterFill.setContainerNo("CSQU3054383");
|
||||||
|
when(repository.aggregate(10L)).thenReturn(loading, missing, afterFill);
|
||||||
|
when(repository.updateTransport(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()));
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "loading-key",
|
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "loading-key",
|
||||||
new TransportUpdateRequest("CSQU3054383", null, 3, "作业后纠错")))
|
new TransportUpdateRequest("CSQU3054383", null, 3, "作业后纠错")))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("PDA尚未开始");
|
.isInstanceOf(BizException.class).hasMessageContaining("PDA尚未开始");
|
||||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
|
service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
|
||||||
new TransportUpdateRequest("CSQU3054383", null, 3, "首次补录")))
|
new TransportUpdateRequest("CSQU3054383", null, 3, null));
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("首次写入");
|
|
||||||
|
verify(repository).updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator");
|
||||||
|
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void correctingExistingContainerNumberStillRequiresReason() {
|
||||||
|
AggregateRow before = row("EDITING", 3);
|
||||||
|
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||||
|
before.setContainerNo("MSCU6639870");
|
||||||
|
when(repository.aggregate(10L)).thenReturn(before);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "reason-key",
|
||||||
|
new TransportUpdateRequest("CSQU3054383", null, 3, null)))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("纠错原因");
|
||||||
|
|
||||||
|
verify(repository, never()).updateTransport(anyLong(), anyInt(), any(), any(), anyLong(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class LoadingPickServiceTest {
|
|||||||
}).when(completionTransaction).executeWithoutResult(any());
|
}).when(completionTransaction).executeWithoutResult(any());
|
||||||
ContainerBookingEntity booking = new ContainerBookingEntity();
|
ContainerBookingEntity booking = new ContainerBookingEntity();
|
||||||
booking.setId(99L);
|
booking.setId(99L);
|
||||||
booking.setContainerNo("CSLU1234567");
|
booking.setContainerNo("MSCU6639870");
|
||||||
booking.setWarehouseId(101L);
|
booking.setWarehouseId(101L);
|
||||||
booking.setSourceType("SIMULATION");
|
booking.setSourceType("SIMULATION");
|
||||||
booking.setRecordStatus("ACTIVE");
|
booking.setRecordStatus("ACTIVE");
|
||||||
@@ -124,9 +124,9 @@ class LoadingPickServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
|
void opensTaskAfterReleasedBookingWithoutWaitingForRealContainerNumber() {
|
||||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
booking.setId(40L);
|
||||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||||
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
||||||
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||||
@@ -136,7 +136,7 @@ class LoadingPickServiceTest {
|
|||||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||||
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
|
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
|
||||||
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
|
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
|
||||||
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, "MSCU-001", "EDITING", 1, 1, null)));
|
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, null, "EDITING", 1, 1, null)));
|
||||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||||
when(repository.taskByPlan(10L)).thenReturn(null);
|
when(repository.taskByPlan(10L)).thenReturn(null);
|
||||||
@@ -191,11 +191,11 @@ class LoadingPickServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void planConfirmationGateRequiresRealContainerAndReleasedBooking() {
|
void planConfirmationGateRequiresReleasedBookingButNotRealContainerNumber() {
|
||||||
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
||||||
10L, 40L, "MSCU-001", "EDITING", 3, 2, null);
|
10L, 40L, "MSCU-001", "EDITING", 3, 2, null);
|
||||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
booking.setId(40L);
|
||||||
var allocation = new OceanBookingContainerAllocationEntity();
|
var allocation = new OceanBookingContainerAllocationEntity();
|
||||||
allocation.setOceanBookingId(90L);
|
allocation.setOceanBookingId(90L);
|
||||||
var ocean = new OceanBookingEntity();
|
var ocean = new OceanBookingEntity();
|
||||||
@@ -209,17 +209,21 @@ class LoadingPickServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void planConfirmationGateRejectsMissingRealContainerNumber() {
|
void planConfirmationGateAllowsMissingRealContainerNumber() {
|
||||||
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
||||||
10L, 40L, null, "EDITING", 3, 2, null);
|
10L, 40L, null, "EDITING", 3, 2, null);
|
||||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||||
booking.setId(40L);
|
booking.setId(40L);
|
||||||
|
var allocation = new OceanBookingContainerAllocationEntity();
|
||||||
|
allocation.setOceanBookingId(90L);
|
||||||
|
var ocean = new OceanBookingEntity();
|
||||||
|
ocean.setId(90L); ocean.setStatus("RELEASED");
|
||||||
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
|
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
|
||||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||||
|
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||||
|
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
|
assertThat(service.requirePreloadConfirmationReady(10L)).containsExactly(link);
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("真实集装箱号");
|
|
||||||
verify(oceanBookingRepository, never()).allocationByContainer(anyLong());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -775,6 +779,25 @@ class LoadingPickServiceTest {
|
|||||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completionRejectsMissingRealContainerNumberBeforeCreatingCommand() {
|
||||||
|
LoadingPickTaskEntity loading = task();
|
||||||
|
ContainerBookingEntity booking = new ContainerBookingEntity();
|
||||||
|
booking.setId(99L);
|
||||||
|
booking.setWarehouseId(101L);
|
||||||
|
booking.setRecordStatus("ACTIVE");
|
||||||
|
when(repository.taskForUpdate(20L)).thenReturn(loading);
|
||||||
|
when(repository.activeBookingByTask(20L)).thenReturn(booking);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.complete(20L,
|
||||||
|
new CompleteLoadingRequest(0, "complete-missing-container", "checked"), 9L, "operator"))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("确认装柜完成前");
|
||||||
|
|
||||||
|
verify(repository, never()).allTaskItemsResolved(20L);
|
||||||
|
verify(completionCommands, never()).insert(any());
|
||||||
|
verify(repository, never()).acceptCompletion(anyLong(), anyInt());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void differentOperationIdIsRejectedImmediatelyWithConflict() {
|
void differentOperationIdIsRejectedImmediatelyWithConflict() {
|
||||||
// different operationId is a canonical A0409 CONFLICT while the durable command is active.
|
// different operationId is a canonical A0409 CONFLICT while the durable command is active.
|
||||||
|
|||||||
Reference in New Issue
Block a user