Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -5115,6 +5115,14 @@ BEGIN
|
||||
(OLD.plan_status = 'EDITING' AND NEW.plan_status IN ('EDITING', 'LOADING'))
|
||||
OR (OLD.plan_status = 'LOADING'
|
||||
AND NEW.plan_status IN ('LOADING', 'COMPLETED'))
|
||||
OR (OLD.plan_status = 'LOADING' AND NEW.plan_status = 'EDITING'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
WHERE pick_task.loading_plan_id = OLD.id
|
||||
AND pick_item.picked_package_count > 0
|
||||
))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '装柜状态只允许单向推进';
|
||||
END IF;
|
||||
@@ -13770,6 +13778,20 @@ DROP PROCEDURE IF EXISTS assert_unified_final_schema;
|
||||
CALL assert_loading_pick_task_shapes(1);
|
||||
DROP PROCEDURE IF EXISTS assert_loading_pick_task_shapes;
|
||||
|
||||
-- 修复旧版本“推送预装即拣货中”的状态污染;从未实际拣货的计划回收到预装柜中。
|
||||
UPDATE fl_loading_plan plan
|
||||
SET plan_status = 'EDITING',
|
||||
updated_at = CURRENT_TIMESTAMP(3),
|
||||
version = version + 1
|
||||
WHERE plan.plan_status = 'LOADING'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
WHERE pick_task.loading_plan_id = plan.id
|
||||
AND pick_item.picked_package_count > 0
|
||||
);
|
||||
|
||||
-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。
|
||||
SELECT 'PASS' AS migration_status,
|
||||
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源及可靠审计结构校验通过' AS message;
|
||||
|
||||
+5
-4
@@ -134,9 +134,10 @@
|
||||
|
||||
| 字段 | 状态值 | 中文含义 |
|
||||
| --- | --- | --- |
|
||||
| 装柜计划 | `EDITING` | 编辑中,可维护货柜信息,并可增删未拣货明细。 |
|
||||
| 装柜计划 | `LOADING` | 装柜中,已生成 PDA 拣货任务;未拣货明细仍可追加、修改或删除。 |
|
||||
| 装柜计划 | `COMPLETED` | PDA 拣货完成后自动完成,计划、明细和货柜信息只读。 |
|
||||
| 货柜聚合 | `WAIT_LOADING` | 待装柜。货柜已创建但尚未创建装柜计划的派生状态,只在查询结果中返回,不写入装柜计划表。 |
|
||||
| 装柜计划 | `EDITING` | 预装柜中,可维护货柜信息,并可增删未拣货明细。 |
|
||||
| 装柜计划 | `LOADING` | 拣货中,已生成拣货任务;未拣货明细仍可追加、修改或删除。 |
|
||||
| 装柜计划 | `COMPLETED` | 装柜完成。拣货完成后自动完成,计划、明细和货柜信息只读。 |
|
||||
| PDA 拣货任务 | `LOADING` | 拣货作业进行中。 |
|
||||
| PDA 拣货任务 | `COMPLETED` | 拣货已完成。 |
|
||||
| PDA 拣货明细 | `PENDING` | 尚未开始拣货。 |
|
||||
@@ -153,7 +154,7 @@
|
||||
| 旧版拆单记录 | `LOCKED` | 已锁定待完成。 |
|
||||
| 旧版拆单记录 | `COMPLETED` | 已完成并关联子入库单。 |
|
||||
|
||||
装柜计划只能单向推进:`EDITING` -> `LOADING` -> `COMPLETED`,没有撤回状态。`COMPLETED` 后不允许编辑或删除装柜明细。
|
||||
货柜列表状态为:`WAIT_LOADING`(待装柜)-> `EDITING`(预装柜中)-> `LOADING`(拣货中)-> `COMPLETED`(装柜完成)。其中 `WAIT_LOADING` 是货柜没有装柜计划时的派生状态;装柜计划持久化状态只能单向推进:`EDITING` -> `LOADING` -> `COMPLETED`,没有撤回状态。`COMPLETED` 后不允许编辑或删除装柜明细。
|
||||
|
||||
## 文件、导出和审计
|
||||
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ public class PlatformContainerController {
|
||||
}
|
||||
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_UPDATE", resourceType="CONTAINER", resourceId="#id", description="完整编辑货柜基础资料")
|
||||
@Operation(summary="完整编辑货柜", description="仅无装柜计划或计划为EDITING时允许")
|
||||
@Operation(summary="完整编辑货柜", description="仅待装柜或预装柜中时允许")
|
||||
public ApiResponse<ContainerBookingEntity> updateDraft(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@Valid @RequestBody FullUpdateRequest request) {
|
||||
@@ -53,7 +53,7 @@ public class PlatformContainerController {
|
||||
@PatchMapping("/{id}/transport") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_TRANSPORT_UPDATE", resourceType="CONTAINER", resourceId="#id",
|
||||
description="修改货柜集装箱号和航次")
|
||||
@Operation(summary="修改集装箱号和航次", description="LOADING仅开放此白名单;COMPLETED全部只读")
|
||||
@Operation(summary="修改集装箱号和航次", description="PDA拣货中仅开放此白名单;装柜完成后全部只读")
|
||||
public ApiResponse<ContainerBookingEntity> updateTransport(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@Valid @RequestBody TransportUpdateRequest request) {
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.baiyue.firstleg.domain;
|
||||
|
||||
/** 智能装柜状态,只允许沿此方向推进。 */
|
||||
public enum SmartLoadingStatus {
|
||||
/** 编辑中:可以维护货柜信息并增删未拣货明细。 */
|
||||
/** 待装柜:货柜已创建但尚未创建装柜计划,仅作为货柜聚合查询的派生状态,不写入装柜计划表。 */
|
||||
WAIT_LOADING,
|
||||
/** 预装柜中:可以维护货柜信息并增删未拣货明细。 */
|
||||
EDITING,
|
||||
/** 装柜中:已生成 PDA 拣货任务,仍可追加或调整未拣货明细。 */
|
||||
/** 拣货中:已生成拣货任务,仍可追加或调整未拣货明细。 */
|
||||
LOADING,
|
||||
/** 装柜完成:PDA 拣货完成后自动完成,计划、明细和货柜信息只读。 */
|
||||
COMPLETED;
|
||||
@@ -12,7 +14,7 @@ public enum SmartLoadingStatus {
|
||||
/** 装柜完成前可维护集装箱号和航次。 */
|
||||
public boolean allowsTransportUpdate() { return this != COMPLETED; }
|
||||
|
||||
/** 编辑中和装柜中均允许维护尚未拣货的明细。 */
|
||||
/** 待装柜、预装柜中和拣货中均允许维护尚未拣货的明细。 */
|
||||
public boolean mutable() { return this != COMPLETED; }
|
||||
|
||||
public boolean canAdvanceTo(SmartLoadingStatus next) {
|
||||
|
||||
@@ -73,14 +73,14 @@ public final class ContainerDtos {
|
||||
LocalDateTime actualWarehouseArrivalAt
|
||||
) {}
|
||||
|
||||
@Schema(name = "ContainerFullUpdateRequest", description = "智能装柜 EDITING 阶段完整编辑")
|
||||
@Schema(name = "ContainerFullUpdateRequest", description = "待装柜或预装柜中阶段完整编辑")
|
||||
public record FullUpdateRequest(
|
||||
@NotNull @PositiveOrZero Integer version,
|
||||
@Valid @NotNull SaveRequest data
|
||||
) {}
|
||||
|
||||
@Schema(name = "ContainerTransportUpdateRequest",
|
||||
description = "LOADING 阶段只允许修改集装箱号和航次")
|
||||
description = "拣货中阶段只允许修改集装箱号和航次")
|
||||
public record TransportUpdateRequest(
|
||||
@Size(max = 64) String containerNo,
|
||||
@Size(max = 64) String voyageNo,
|
||||
|
||||
@@ -13,7 +13,13 @@ import org.apache.ibatis.annotations.Update;
|
||||
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT b.*, p.id AS plan_id, COALESCE(p.plan_status, 'EDITING') AS plan_status
|
||||
SELECT b.*, p.id AS plan_id,
|
||||
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
|
||||
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
|
||||
) THEN 'WAIT_LOADING'
|
||||
ELSE p.plan_status END AS plan_status
|
||||
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
WHERE (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
|
||||
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
|
||||
@@ -24,7 +30,12 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
AND (#{query.departurePortCode} IS NULL OR b.departure_port_code = #{query.departurePortCode})
|
||||
AND (#{query.destinationPortCode} IS NULL OR b.destination_port_code = #{query.destinationPortCode})
|
||||
AND (#{query.destinationWarehouseCode} IS NULL OR b.destination_warehouse_code = #{query.destinationWarehouseCode})
|
||||
AND (#{query.status} IS NULL OR COALESCE(p.plan_status, 'EDITING') = #{query.status})
|
||||
AND (#{query.status} IS NULL OR CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
|
||||
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
|
||||
) THEN 'WAIT_LOADING'
|
||||
ELSE p.plan_status END = #{query.status})
|
||||
AND (#{query.estimatedSailingFrom} IS NULL OR b.estimated_sailing_at >= #{query.estimatedSailingFrom})
|
||||
AND (#{query.estimatedSailingTo} IS NULL OR b.estimated_sailing_at <= #{query.estimatedSailingTo})
|
||||
AND (#{query.keyword} IS NULL OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
@@ -38,7 +49,13 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page, @Param("query") Query query);
|
||||
|
||||
@Select("""
|
||||
SELECT b.*, p.id AS plan_id, COALESCE(p.plan_status, 'EDITING') AS plan_status
|
||||
SELECT b.*, p.id AS plan_id,
|
||||
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
|
||||
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0
|
||||
) THEN 'WAIT_LOADING'
|
||||
ELSE p.plan_status END AS plan_status
|
||||
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
WHERE b.id = #{id}
|
||||
""")
|
||||
|
||||
@@ -45,7 +45,10 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
WHERE p.plan_status = 'LOADING'
|
||||
WHERE p.preload_pushed_at IS NOT NULL
|
||||
AND p.plan_status IN ('EDITING', 'LOADING')
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0)
|
||||
<if test="query.status != null and query.status != ''">AND p.plan_status = #{query.status}</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
|
||||
@@ -8,10 +8,28 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl
|
||||
""")
|
||||
int touchEditing(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan SET plan_status='LOADING',push_idempotency_key=#{key},
|
||||
UPDATE fl_loading_plan SET push_idempotency_key=#{key},
|
||||
preload_pushed_by=#{operatorId},preload_pushed_by_name=#{operatorName},preload_pushed_at=CURRENT_TIMESTAMP(3),
|
||||
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{planId} AND plan_status='EDITING' AND version=#{expectedVersion}
|
||||
WHERE id=#{planId} AND plan_status='EDITING' AND push_idempotency_key IS NULL AND version=#{expectedVersion}
|
||||
""")
|
||||
int pushPreload(@Param("planId") long planId,@Param("key") String key,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan SET plan_status='LOADING',updated_by=#{operatorId},updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{planId} AND plan_status='EDITING' AND preload_pushed_at IS NOT NULL
|
||||
""")
|
||||
int startPicking(@Param("planId") long planId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan plan SET plan_status='EDITING',updated_by=#{operatorId},updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE plan.id=#{planId} AND plan.plan_status='LOADING' AND plan.version=#{expectedVersion}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id=pick_item.pick_task_id
|
||||
WHERE pick_task.loading_plan_id=plan.id AND pick_item.picked_package_count>0
|
||||
)
|
||||
""")
|
||||
int recycleUnpicked(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
}
|
||||
|
||||
@@ -72,4 +72,10 @@ public class SmartLoadingRepository {
|
||||
public int push(long planId, String key, int version, long operatorId, String operatorName) {
|
||||
return plans.pushPreload(planId, key, version, operatorId, operatorName);
|
||||
}
|
||||
public int startPicking(long planId, long operatorId, String operatorName) {
|
||||
return plans.startPicking(planId, operatorId, operatorName);
|
||||
}
|
||||
public int recycleUnpicked(long planId, int expectedVersion, long operatorId, String operatorName) {
|
||||
return plans.recycleUnpicked(planId, expectedVersion, operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ContainerService {
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
|
||||
try { repository.insert(value); } catch (DuplicateKeyException ex) { throw duplicate(); }
|
||||
value.setPlanStatus(SmartLoadingStatus.EDITING.name());
|
||||
value.setPlanStatus(SmartLoadingStatus.WAIT_LOADING.name());
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class ContainerService {
|
||||
FullUpdateRequest request) {
|
||||
ContainerBookingEntity current = required(id);
|
||||
requireVersion(current, request.version());
|
||||
if (status(current) != SmartLoadingStatus.EDITING) {
|
||||
if (status(current) != SmartLoadingStatus.WAIT_LOADING && status(current) != SmartLoadingStatus.EDITING) {
|
||||
throw conflict("货柜已进入预装流程,只能修改集装箱号和航次");
|
||||
}
|
||||
validateSchedule(request.data());
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class LoadingListService {
|
||||
private static final Set<String> LOADING_STATUSES = Set.of("LOADING");
|
||||
private static final Set<String> PRELOAD_STATUSES = Set.of("EDITING", "LOADING");
|
||||
@Resource
|
||||
private LoadingListRepository repository;
|
||||
@Resource
|
||||
@@ -43,8 +43,8 @@ public class LoadingListService {
|
||||
public PageResult<PreloadSummary> preloadPage(PreloadQuery query) {
|
||||
validateRange(query.createdFrom(), query.createdTo());
|
||||
String status = trim(query.status());
|
||||
if (StringUtils.hasText(status) && !LOADING_STATUSES.contains(status)) {
|
||||
throw badRequest("装柜清单状态只能是 LOADING");
|
||||
if (StringUtils.hasText(status) && !PRELOAD_STATUSES.contains(status)) {
|
||||
throw badRequest("预装清单状态只能是 EDITING 或 LOADING");
|
||||
}
|
||||
return repository.preloadPage(new PreloadQuery(trim(query.keyword()), status,
|
||||
query.warehouseId(), query.createdFrom(), query.createdTo(), query.page(), query.size()));
|
||||
@@ -59,8 +59,8 @@ public class LoadingListService {
|
||||
public PreloadDetail preloadDetail(long planId, long warehouseId) {
|
||||
AggregateRow row = requirePlan(planId);
|
||||
requireWarehouse(row, warehouseId);
|
||||
if (!LOADING_STATUSES.contains(row.getPlanStatus())) {
|
||||
throw notFound("装柜中清单不存在");
|
||||
if (!PRELOAD_STATUSES.contains(row.getPlanStatus())) {
|
||||
throw notFound("预装清单不存在");
|
||||
}
|
||||
return repository.preloadDetail(row);
|
||||
}
|
||||
@@ -160,8 +160,8 @@ public class LoadingListService {
|
||||
}
|
||||
|
||||
private static void requireLoadingMutable(AggregateRow row) {
|
||||
if (!LOADING_STATUSES.contains(row.getPlanStatus())) {
|
||||
throw conflict("仅装柜中可修改集装箱号与航次");
|
||||
if (!PRELOAD_STATUSES.contains(row.getPlanStatus())) {
|
||||
throw conflict("仅预装柜中或拣货中可修改集装箱号与航次");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -48,6 +50,8 @@ public class LoadingPickService {
|
||||
@Resource
|
||||
private LoadingPickRepository repository;
|
||||
@Resource
|
||||
private SmartLoadingRepository smartLoadingRepository;
|
||||
@Resource
|
||||
private LoadingListService loadingListService;
|
||||
@Resource
|
||||
private PdaPickSettlementService settlementService;
|
||||
@@ -123,6 +127,7 @@ public class LoadingPickService {
|
||||
throw conflict("PDA任务分配版本已变化");
|
||||
}
|
||||
}
|
||||
startPickingIfNeeded(task, operatorId, operatorName);
|
||||
insertOperation(taskId, operationId, requestHash, "CLAIM", operatorId, operatorName);
|
||||
return view(task, null);
|
||||
}
|
||||
@@ -182,6 +187,7 @@ public class LoadingPickService {
|
||||
history.setOperatorId(operatorId);
|
||||
history.setOperatorName(operatorName);
|
||||
repository.insert(history);
|
||||
startPickingIfNeeded(task, operatorId, operatorName);
|
||||
touchSettleFinishAndSync(task, operatorId, operatorName);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
@@ -207,6 +213,7 @@ public class LoadingPickService {
|
||||
}
|
||||
insertHistory(taskId, itemId, operationId, requestHash, "PARTIAL",
|
||||
request.finalPickedPackageCount(), null, operatorId, operatorName);
|
||||
startPickingIfNeeded(task, operatorId, operatorName);
|
||||
touchSettleFinishAndSync(task, operatorId, operatorName);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
@@ -271,6 +278,9 @@ public class LoadingPickService {
|
||||
for (BatchLine line : lines) {
|
||||
applyBatchLine(taskId, line, operationId, operatorId, operatorName);
|
||||
}
|
||||
if (lines.stream().anyMatch(line -> "PICK".equals(line.action()) || "PARTIAL".equals(line.action()))) {
|
||||
startPickingIfNeeded(task, operatorId, operatorName);
|
||||
}
|
||||
if (!repository.allTaskItemsResolved(taskId)) {
|
||||
throw conflict("整批提交必须处理本任务全部待拣明细");
|
||||
}
|
||||
@@ -375,6 +385,25 @@ public class LoadingPickService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入PDA拣货任务即将预装柜计划推进为拣货中;状态推进和历史写入保持幂等。 */
|
||||
private void startPickingIfNeeded(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
||||
if (smartLoadingRepository.startPicking(task.getLoadingPlanId(), operatorId, operatorName) != 1) {
|
||||
return;
|
||||
}
|
||||
LoadingPlanHistoryEntity history = new LoadingPlanHistoryEntity();
|
||||
history.setLoadingPlanId(task.getLoadingPlanId());
|
||||
history.setOperationId("PDA-START-" + task.getId());
|
||||
history.setActionType("START_LOADING");
|
||||
history.setFromStatus("EDITING");
|
||||
history.setToStatus("LOADING");
|
||||
history.setBeforeSnapshot("{\"source\":\"PDA\"}");
|
||||
history.setAfterSnapshot("{\"source\":\"PDA\",\"status\":\"LOADING\"}");
|
||||
history.setOperatorId(operatorId);
|
||||
history.setOperatorName(operatorName);
|
||||
history.setOperationReason("进入PDA拣货任务");
|
||||
smartLoadingRepository.insert(history);
|
||||
}
|
||||
|
||||
private LoadingPickTaskEntity requiredTask(long taskId) {
|
||||
LoadingPickTaskEntity task = repository.task(taskId);
|
||||
if (task == null) {
|
||||
|
||||
@@ -89,7 +89,7 @@ public class SmartLoadingService {
|
||||
+ '|' + request.expectedVersion());
|
||||
Plan replay = replay(operation, planId, "ADD_ITEM", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion());
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
Candidate candidate = repository.candidate(booking.getWarehouseId(), request.inboundId(), request.inboundDetailId());
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
@@ -131,7 +131,7 @@ public class SmartLoadingService {
|
||||
String requestHash = sha256("RECOMMEND|" + planId + '|' + request.expectedVersion());
|
||||
Plan replay = replay(operation, planId, "SMART_RECOMMEND", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion());
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
for (Candidate candidate : candidates(booking.getWarehouseId())) {
|
||||
if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue;
|
||||
@@ -163,7 +163,7 @@ public class SmartLoadingService {
|
||||
+ '|' + request.expectedItemVersion());
|
||||
Plan replay = replay(operation, planId, "ADJUST_ITEM", itemId, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion());
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
|
||||
@@ -197,7 +197,7 @@ public class SmartLoadingService {
|
||||
+ request.expectedPlanVersion() + '|' + request.expectedItemVersion());
|
||||
Plan replay = replay(operation, planId, "REMOVE_ITEM", itemId, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion());
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
loadingPickService.remove(itemId);
|
||||
@@ -216,7 +216,8 @@ public class SmartLoadingService {
|
||||
String key = trim(idempotencyKey);
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
|
||||
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
if ("LOADING".equals(plan.getPlanStatus()) && key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
if (key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
|
||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||
@@ -228,7 +229,7 @@ public class SmartLoadingService {
|
||||
try {
|
||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||
history(planId, null, "START_LOADING", "EDITING", "LOADING", key, operatorId, operatorName);
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, operatorId, operatorName);
|
||||
LoadingPlanEntity pushed = repository.plan(planId);
|
||||
return view(pushed, requireBooking(pushed.getContainerBookingId()));
|
||||
}
|
||||
@@ -329,7 +330,19 @@ public class SmartLoadingService {
|
||||
}
|
||||
return view(plan, booking);
|
||||
}
|
||||
private LoadingPlanEntity editing(long id,int version){LoadingPlanEntity p=repository.plan(id);if(p==null)throw new BizException(ResultCode.NOT_FOUND,"装柜计划不存在");if(!Set.of("EDITING", "LOADING").contains(p.getPlanStatus())||!Objects.equals(p.getVersion(),version))throw conflict("计划已完成或版本冲突");return p;}
|
||||
private LoadingPlanEntity editing(long id, int version, long operatorId, String operatorName) {
|
||||
LoadingPlanEntity p = repository.plan(id);
|
||||
if (p == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
if (!Set.of("EDITING", "LOADING").contains(p.getPlanStatus()) || !Objects.equals(p.getVersion(), version)) {
|
||||
throw conflict("计划已完成或版本冲突");
|
||||
}
|
||||
if ("LOADING".equals(p.getPlanStatus())
|
||||
&& repository.recycleUnpicked(id, version, operatorId, operatorName) == 1) {
|
||||
p.setPlanStatus("EDITING");
|
||||
p.setVersion(version + 1);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
private ContainerBookingEntity requireBooking(long id){ContainerBookingEntity b=repository.booking(id);if(b==null)throw new BizException(ResultCode.NOT_FOUND,"货柜预订不存在");return b;}
|
||||
private void touch(LoadingPlanEntity p,long op,String name){if(repository.touch(p.getId(),p.getVersion(),op,name)!=1)throw conflict("计划已被并发修改");}
|
||||
private Plan replay(String operation, long planId, String action, Long itemId, String requestHash) {
|
||||
|
||||
@@ -79,11 +79,18 @@ public class SplitPreloadService {
|
||||
SplitActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, START_CONFIRMING, key);
|
||||
if (replay != null) return replay;
|
||||
InboundSnapshot inbound = requireInbound(tenantId, inboundId);
|
||||
requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认");
|
||||
boolean loading = "LOADING".equals(inbound.processStatus());
|
||||
if (loading) {
|
||||
// Starting a real loading task advances the inbound before its preload
|
||||
// confirmation is submitted; retain the linked-container hard gate.
|
||||
loadingModelBridge.requireStarted(tenantId, inboundId);
|
||||
} else {
|
||||
requireStatus(inbound, "INBOUNDED", "仅待分柜订单可以提交预装清单确认");
|
||||
}
|
||||
PreloadPlanEntity plan = requirePlan(tenantId, inboundId);
|
||||
if (PreloadStatus.CONFIRMING.equals(plan.getStatus())) {
|
||||
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
|
||||
new SplitActionResponse("INBOUNDED", response(plan)));
|
||||
new SplitActionResponse(loading ? "LOADING" : "INBOUNDED", response(plan)));
|
||||
}
|
||||
if (!PreloadStatus.PENDING.equals(plan.getStatus())
|
||||
|| repository.changePlanStatus(tenantId, inboundId, PreloadStatus.PENDING,
|
||||
@@ -91,7 +98,8 @@ public class SplitPreloadService {
|
||||
throw concurrent();
|
||||
}
|
||||
return complete(tenantId, inboundId, operatorId, START_CONFIRMING, key,
|
||||
new SplitActionResponse("INBOUNDED", response(repository.findPlan(tenantId, inboundId))));
|
||||
new SplitActionResponse(loading ? "LOADING" : "INBOUNDED",
|
||||
response(repository.findPlan(tenantId, inboundId))));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -602,6 +602,14 @@ BEGIN
|
||||
(OLD.plan_status = 'EDITING' AND NEW.plan_status IN ('EDITING', 'LOADING'))
|
||||
OR (OLD.plan_status = 'LOADING'
|
||||
AND NEW.plan_status IN ('LOADING', 'COMPLETED'))
|
||||
OR (OLD.plan_status = 'LOADING' AND NEW.plan_status = 'EDITING'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
WHERE pick_task.loading_plan_id = OLD.id
|
||||
AND pick_item.picked_package_count > 0
|
||||
))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '装柜状态只允许单向推进';
|
||||
END IF;
|
||||
@@ -676,6 +684,20 @@ BEGIN
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
-- 旧版本在推送预装时即写入 LOADING。仅回收从未发生实际拣货的计划;有实际拣货记录的数据不回退。
|
||||
UPDATE fl_loading_plan plan
|
||||
SET plan_status = 'EDITING',
|
||||
updated_at = CURRENT_TIMESTAMP(3),
|
||||
version = version + 1
|
||||
WHERE plan.plan_status = 'LOADING'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
WHERE pick_task.loading_plan_id = plan.id
|
||||
AND pick_item.picked_package_count > 0
|
||||
);
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_loading_item_before_insert;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER trg_loading_item_before_insert
|
||||
|
||||
@@ -136,7 +136,7 @@ paths:
|
||||
- {name: containerNo, in: query, schema: {type: string, maxLength: 64}}
|
||||
- {name: cabinetNo, in: query, schema: {type: string, maxLength: 64}}
|
||||
- {name: warehouseId, in: query, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: status, in: query, schema: {type: string, enum: [EDITING, LOADING, COMPLETED]}}
|
||||
- {name: status, in: query, schema: {type: string, enum: [WAIT_LOADING, EDITING, LOADING, COMPLETED]}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功或业务失败,按响应体 code 判断
|
||||
@@ -146,7 +146,7 @@ paths:
|
||||
'403': {description: 无货柜查询权限}
|
||||
post:
|
||||
summary: 新增平台货柜
|
||||
description: 需要 platform:firstleg:container:create 权限;新增后聚合状态为 EDITING
|
||||
description: 需要 platform:firstleg:container:create 权限;新增后聚合状态为 WAIT_LOADING(待装柜)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -166,7 +166,7 @@ paths:
|
||||
'403': {description: 无货柜查询权限}
|
||||
put:
|
||||
summary: 草稿阶段完整编辑货柜
|
||||
description: 需要 platform:firstleg:container:edit 权限;仅无装柜计划或计划为 EDITING 时允许
|
||||
description: 需要 platform:firstleg:container:edit 权限;仅待装柜或计划为 EDITING(预装柜中)时允许
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/ContainerBookingId'}
|
||||
requestBody:
|
||||
@@ -701,7 +701,7 @@ components:
|
||||
actualPortArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
estimatedWarehouseArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
actualWarehouseArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
planStatus: {type: string, enum: [EDITING, LOADING, COMPLETED]}
|
||||
planStatus: {type: string, enum: [WAIT_LOADING, EDITING, LOADING, COMPLETED]}
|
||||
version: {type: integer, minimum: 0}
|
||||
ContainerSaveRequest:
|
||||
type: object
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ class ContainerControllerContractTest {
|
||||
.contains("/api/platform/v1/containers/{id}/transport:")
|
||||
.contains("platform:firstleg:container:list")
|
||||
.contains("EDITING")
|
||||
.contains("WAIT_LOADING")
|
||||
.contains("LOADING")
|
||||
.contains("COMPLETED")
|
||||
.doesNotContain("tenantId")
|
||||
|
||||
+3
@@ -24,7 +24,10 @@ class ContainerBookingMapperContractTest {
|
||||
assertThat(select)
|
||||
.contains("fl_loading_plan")
|
||||
.contains("p.id AS plan_id")
|
||||
.contains("CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'")
|
||||
.contains("plan_status")
|
||||
.contains("p.plan_status != 'COMPLETED'")
|
||||
.doesNotContain("<>")
|
||||
.doesNotContain("tenant_id")
|
||||
.doesNotContain("account_id");
|
||||
}
|
||||
|
||||
+3
-1
@@ -34,7 +34,9 @@ class LoadingListQueryMapperContractTest {
|
||||
.contains("FROM fl_loading_plan p")
|
||||
.contains("JOIN fl_container_booking b")
|
||||
.contains("LEFT JOIN fl_loading_plan_item i")
|
||||
.contains("p.plan_status = 'LOADING'")
|
||||
.contains("p.preload_pushed_at IS NOT NULL")
|
||||
.contains("p.plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("active_item.deleted = 0")
|
||||
.contains("b.warehouse_id = #{query.warehouseId}")
|
||||
.doesNotContain("query.accountId")
|
||||
.doesNotContain("PRELOAD_PUSHED");
|
||||
|
||||
@@ -110,6 +110,8 @@ class SmartLoadingSqlContractTest {
|
||||
.contains("CHECK (plan_status IN ('EDITING', 'LOADING', 'COMPLETED'))")
|
||||
.contains("OLD.plan_status = 'EDITING' AND NEW.plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("NEW.plan_status IN ('LOADING', 'COMPLETED')")
|
||||
.contains("OLD.plan_status = 'LOADING' AND NEW.plan_status = 'EDITING'")
|
||||
.contains("旧版本在推送预装时即写入 LOADING")
|
||||
.contains("SET MESSAGE_TEXT = '装柜状态只允许单向推进'")
|
||||
.contains("IF OLD.plan_status = 'EDITING' AND NEW.plan_status = 'LOADING' THEN")
|
||||
.contains("ELSEIF OLD.plan_status = 'LOADING' AND NEW.plan_status = 'COMPLETED' THEN")
|
||||
|
||||
@@ -56,7 +56,7 @@ class ContainerServiceTest {
|
||||
assertThat(saved.getWarehouseName()).isEqualTo("上海中转仓");
|
||||
assertThat(saved.getDeparturePortCode()).isEqualTo("CNSHA");
|
||||
assertThat(saved.getCapacityCbm()).isEqualByComparingTo("68.0000");
|
||||
assertThat(saved.getPlanStatus()).isEqualTo("EDITING");
|
||||
assertThat(saved.getPlanStatus()).isEqualTo("WAIT_LOADING");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -73,6 +73,20 @@ class ContainerServiceTest {
|
||||
verify(repository).updateDraft(current, 2, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待装柜阶段允许完整编辑")
|
||||
void updatesAllFieldsWhileWaitingForLoading() {
|
||||
ContainerBookingEntity current = booking("WAIT_LOADING", 2);
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(repository.updateDraft(any(), org.mockito.ArgumentMatchers.eq(2),
|
||||
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq("admin"))).thenReturn(1);
|
||||
|
||||
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request("柜 NEW")));
|
||||
|
||||
assertThat(current.getCabinetNo()).isEqualTo("柜 NEW");
|
||||
verify(repository).updateDraft(current, 2, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("进入预装柜中后拒绝完整编辑")
|
||||
void rejectsFullUpdateAfterPreloadPush() {
|
||||
|
||||
@@ -50,13 +50,12 @@ class LoadingListServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectEditingStatusFromPreloadList() {
|
||||
void shouldAcceptEditingStatusFromPreloadList() {
|
||||
PreloadQuery query = new PreloadQuery(null, "EDITING", 100L, null, null, 1, 20);
|
||||
|
||||
assertThatThrownBy(() -> service.preloadPage(query))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.BAD_REQUEST);
|
||||
service.preloadPage(query);
|
||||
|
||||
verify(repository).preloadPage(query);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -7,12 +7,14 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest;
|
||||
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.entity.LoadingPickAssignmentEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
@@ -24,6 +26,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -36,6 +39,8 @@ class LoadingPickServiceTest {
|
||||
@MockBean
|
||||
private LoadingPickRepository repository;
|
||||
@MockBean
|
||||
private SmartLoadingRepository smartLoadingRepository;
|
||||
@MockBean
|
||||
private LoadingListService loadingListService;
|
||||
@MockBean
|
||||
private PdaPickSettlementService settlementService;
|
||||
@@ -75,6 +80,52 @@ class LoadingPickServiceTest {
|
||||
verify(repository).insert(any(LoadingPickHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsPickAsStartLoadingFallbackForOlderClients() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity item = item();
|
||||
preparePick(task, item, "pick-start-loading");
|
||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
|
||||
|
||||
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-start-loading"),
|
||||
9L, "operator");
|
||||
|
||||
verify(smartLoadingRepository).startPicking(10L, 9L, "operator");
|
||||
verify(smartLoadingRepository).insert(any(com.baiyue.firstleg.entity.LoadingPlanHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimStartsLoadingAndSameOperatorReentryDoesNotDuplicateStartHistory() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickAssignmentEntity assignment = assignment();
|
||||
AtomicReference<LoadingPickOperationEntity> operation = new AtomicReference<>();
|
||||
when(repository.operation("claim-1")).thenAnswer(invocation -> operation.get());
|
||||
when(repository.operation("claim-2")).thenReturn(null);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(null, assignment);
|
||||
when(repository.createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED")).thenReturn(1);
|
||||
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1, 0);
|
||||
doAnswer(invocation -> {
|
||||
LoadingPickOperationEntity value = invocation.getArgument(0);
|
||||
if ("claim-1".equals(value.getOperationId())) operation.set(value);
|
||||
return null;
|
||||
}).when(repository).insert(any(LoadingPickOperationEntity.class));
|
||||
|
||||
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(0, "claim-1"), 9L, "operator");
|
||||
service.claim(20L, new ClaimRequest(0, "claim-2"), 9L, "operator");
|
||||
|
||||
verify(smartLoadingRepository, times(2)).startPicking(10L, 9L, "operator");
|
||||
verify(smartLoadingRepository, times(1)).insert(argThat(
|
||||
(com.baiyue.firstleg.entity.LoadingPlanHistoryEntity history) ->
|
||||
"START_LOADING".equals(history.getActionType())
|
||||
&& "EDITING".equals(history.getFromStatus())
|
||||
&& "LOADING".equals(history.getToStatus())
|
||||
&& "PDA-START-20".equals(history.getOperationId())));
|
||||
verify(repository, times(1)).createAssignment(20L, 9L, "operator", 9L, "operator", "CLAIMED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void touchesTaskBeforeCompletingLastPartialItem() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
|
||||
@@ -202,6 +202,28 @@ class SmartLoadingServiceTest {
|
||||
assertThat(captor.getValue().getProductQuantity()).isEqualTo(67);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recyclesLegacyLoadingPlanBeforeAddingFirstItem() {
|
||||
LoadingPlanEntity stale = plan("LOADING", 5);
|
||||
LoadingPlanEntity after = plan("EDITING", 7);
|
||||
when(repository.plan(1)).thenReturn(stale, after);
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
when(repository.recycleUnpicked(1, 5, 7, "u")).thenReturn(1);
|
||||
when(repository.touch(1, 6, 7, "u")).thenReturn(1);
|
||||
when(repository.items(1)).thenReturn(List.of());
|
||||
when(repository.candidate(100, 10, 20)).thenReturn(candidate(10));
|
||||
when(repository.activeItem(1L, 9L, 20L)).thenReturn(null);
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn(ApiResponse.success(
|
||||
new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 1,
|
||||
BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, null)));
|
||||
|
||||
Plan result = service.assign(1, new AssignRequest(10L, 20L, 1, 5, "assign-recycle"), 7, "u");
|
||||
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).recycleUnpicked(1, 5, 7, "u");
|
||||
verify(repository).touch(1, 6, 7, "u");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaysBeforeVersionCheck() {
|
||||
AssignRequest request = new AssignRequest(10L, 20L, 2, 0, "assign-replay");
|
||||
@@ -335,7 +357,7 @@ class SmartLoadingServiceTest {
|
||||
@Test
|
||||
void pushesIdempotently() {
|
||||
LoadingPlanEntity editing = plan("EDITING", 3);
|
||||
LoadingPlanEntity pushed = plan("LOADING", 4);
|
||||
LoadingPlanEntity pushed = plan("EDITING", 4);
|
||||
pushed.setPushIdempotencyKey("key");
|
||||
when(repository.plan(1)).thenReturn(editing, pushed);
|
||||
when(repository.items(1)).thenReturn(List.of(item(8, 9, 20, 1)));
|
||||
@@ -344,7 +366,7 @@ class SmartLoadingServiceTest {
|
||||
|
||||
Plan result = service.pushPreload(1, "key", new PushRequest(3), 7, "u");
|
||||
|
||||
assertThat(result.status()).isEqualTo("LOADING");
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).push(1, "key", 3, 7, "u");
|
||||
}
|
||||
|
||||
@@ -373,7 +395,7 @@ class SmartLoadingServiceTest {
|
||||
@Test
|
||||
void pushesWithoutContainerNumberWhenCapacityIsWithinLimit() {
|
||||
LoadingPlanEntity editing = plan("EDITING", 1);
|
||||
LoadingPlanEntity pushed = plan("LOADING", 2);
|
||||
LoadingPlanEntity pushed = plan("EDITING", 2);
|
||||
ContainerBookingEntity booking = booking();
|
||||
booking.setContainerNo(null);
|
||||
when(repository.plan(1)).thenReturn(editing, pushed);
|
||||
@@ -383,7 +405,7 @@ class SmartLoadingServiceTest {
|
||||
|
||||
Plan result = service.pushPreload(1, "no-container-key", new PushRequest(1), 7, "u");
|
||||
|
||||
assertThat(result.status()).isEqualTo("LOADING");
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).push(1, "no-container-key", 1, 7, "u");
|
||||
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), anyString(), anyList());
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class SmartLoadingStatusTest {
|
||||
@Test
|
||||
void statusCanOnlyAdvanceOneWay() {
|
||||
assertThat(SmartLoadingStatus.WAIT_LOADING.canAdvanceTo(SmartLoadingStatus.EDITING)).isTrue();
|
||||
assertThat(SmartLoadingStatus.EDITING.canAdvanceTo(SmartLoadingStatus.LOADING)).isTrue();
|
||||
assertThat(SmartLoadingStatus.LOADING.canAdvanceTo(SmartLoadingStatus.COMPLETED)).isTrue();
|
||||
assertThat(SmartLoadingStatus.COMPLETED.canAdvanceTo(SmartLoadingStatus.LOADING)).isFalse();
|
||||
assertThat(SmartLoadingStatus.WAIT_LOADING.mutable()).isTrue();
|
||||
assertThat(SmartLoadingStatus.EDITING.mutable()).isTrue();
|
||||
assertThat(SmartLoadingStatus.LOADING.mutable()).isTrue();
|
||||
assertThat(SmartLoadingStatus.LOADING.allowsTransportUpdate()).isTrue();
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
@@ -72,4 +73,36 @@ class SplitPreloadServiceTest {
|
||||
verify(loadingModelBridge).requireStarted(10, 20);
|
||||
verify(repository).beginLoading(10, 20, 7, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsPreloadConfirmationAfterLoadingBeginsWithLinkedContainerGate() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "LOADING", 5));
|
||||
PreloadPlanEntity plan = new PreloadPlanEntity();
|
||||
plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("PENDING"); plan.setVersion(3);
|
||||
plan.setItemsJson("[]");
|
||||
when(repository.findPlan(10, 20)).thenReturn(plan);
|
||||
org.mockito.Mockito.doNothing().when(loadingModelBridge).requireStarted(10, 20);
|
||||
when(repository.changePlanStatus(10, 20, "PENDING", "CONFIRMING", 7, "admin", 3)).thenReturn(1);
|
||||
|
||||
SplitActionResponse result = service.startConfirming(10, 20, 7, "admin", "k4",
|
||||
new ConfirmPreloadRequest(3));
|
||||
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADING");
|
||||
verify(loadingModelBridge).requireStarted(10, 20);
|
||||
verify(repository).changePlanStatus(10, 20, "PENDING", "CONFIRMING", 7, "admin", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsLoadingPreloadConfirmationWhenLinkedContainersHaveNotStarted() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "LOADING", 5));
|
||||
org.mockito.Mockito.doThrow(new IllegalStateException("not started"))
|
||||
.when(loadingModelBridge).requireStarted(10, 20);
|
||||
|
||||
assertThatThrownBy(() -> service.startConfirming(10, 20, 7, "admin", "k5",
|
||||
new ConfirmPreloadRequest(3)))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("not started");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user