This commit is contained in:
2026-08-28 11:28:30 +08:00
parent 1e5ce54858
commit f998bed09c
9 changed files with 140 additions and 31 deletions
@@ -706,9 +706,20 @@ public class LoadingPickService {
/** 进入PDA拣货任务即将预装柜计划推进为拣货中;状态推进和历史写入保持幂等。 */
private void startPickingIfNeeded(LoadingPickTaskEntity task, long operatorId, String operatorName) {
if (smartLoadingRepository.startPicking(task.getLoadingPlanId(), operatorId, operatorName) == 1) {
String operationId = "PDA-START-" + task.getId();
LoadingPlanHistoryEntity existing = smartLoadingRepository.operation(operationId);
if (existing != null) {
if (!Objects.equals(existing.getLoadingPlanId(), task.getLoadingPlanId())
|| !"START_LOADING".equals(existing.getActionType())
|| !"EDITING".equals(existing.getFromStatus())
|| !"LOADING".equals(existing.getToStatus())) {
throw conflict("PDA开始装柜历史与当前任务不一致");
}
return;
}
LoadingPlanHistoryEntity history = new LoadingPlanHistoryEntity();
history.setLoadingPlanId(task.getLoadingPlanId());
history.setOperationId("PDA-START-" + task.getId());
history.setOperationId(operationId);
history.setActionType("START_LOADING");
history.setFromStatus("EDITING");
history.setToStatus("LOADING");
@@ -175,3 +175,39 @@ CREATE TABLE IF NOT EXISTS fl_container_transport_history (
UNIQUE KEY uk_container_transport_history_operation (container_booking_id,operator_id,operation_id),
KEY idx_container_transport_history (container_booking_id,created_at,id)
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
DROP PROCEDURE IF EXISTS ensure_container_transport_history_schema;
DELIMITER $$
CREATE PROCEDURE ensure_container_transport_history_schema()
BEGIN
IF (NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='fl_container_transport_history' AND column_name='operation_id')
OR NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='fl_container_transport_history' AND column_name='request_hash'))
AND EXISTS (SELECT 1 FROM fl_container_transport_history LIMIT 1) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='运输历史升级失败:旧表已有数据且缺少幂等字段';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='fl_container_transport_history' AND column_name='operation_id') THEN
ALTER TABLE fl_container_transport_history
ADD COLUMN operation_id VARCHAR(64) NOT NULL COMMENT '运输字段操作幂等ID' AFTER after_voyage_no;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='fl_container_transport_history' AND column_name='request_hash') THEN
ALTER TABLE fl_container_transport_history
ADD COLUMN request_hash CHAR(64) NOT NULL COMMENT '运输字段请求哈希' AFTER operation_id;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='fl_container_transport_history' AND index_name='uk_container_transport_history_operation') THEN
ALTER TABLE fl_container_transport_history
ADD UNIQUE KEY uk_container_transport_history_operation (container_booking_id,operator_id,operation_id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
AND table_name='fl_container_transport_history' AND index_name='idx_container_transport_history') THEN
ALTER TABLE fl_container_transport_history
ADD KEY idx_container_transport_history (container_booking_id,created_at,id);
END IF;
END$$
DELIMITER ;
CALL ensure_container_transport_history_schema();
DROP PROCEDURE ensure_container_transport_history_schema;
@@ -902,20 +902,6 @@ 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
@@ -63,6 +63,10 @@ class Phase3ChannelContainerMigrationContractTest {
.contains("current_plan_status IN ('LOADING', 'COMPLETED')")
.contains("装柜作业开始后仅允许修改集装箱号与放舱运输数据")
.contains("已完成PDA拣货任务不可修改")
.contains("ensure_container_transport_history_schema")
.contains("ADD COLUMN operation_id VARCHAR(64) NOT NULL")
.contains("ADD COLUMN request_hash CHAR(64) NOT NULL")
.contains("旧表已有数据且缺少幂等字段")
.contains("NOT (OLD.channel_code <=> NEW.channel_code)")
.contains("NOT (OLD.channel_name <=> NEW.channel_name)")
.contains("NOT (OLD.ocean_booking_id <=> NEW.ocean_booking_id)")
@@ -204,7 +204,8 @@ class SmartLoadingSqlContractTest {
.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")
.doesNotContain("旧版本在推送预装时即写入 LOADING")
.doesNotContain("修复旧版本“推送预装即拣货中”的状态污染")
.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")
@@ -314,6 +315,7 @@ class SmartLoadingSqlContractTest {
.contains("cabinet_no VARCHAR(64) NOT NULL")
.contains("container_no VARCHAR(64) NULL")
.contains("task_status VARCHAR(24) NOT NULL DEFAULT 'LOADING'")
.contains("column_name = 'task_status' AND LOWER(column_type) = 'varchar(24)'")
.contains("UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id)")
.contains("UNIQUE KEY uk_loading_pick_task_booking (container_booking_id)")
.contains("KEY idx_loading_pick_container (container_no, task_status, started_at)")
@@ -17,6 +17,7 @@ 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.entity.LoadingPlanHistoryEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
@@ -902,6 +903,43 @@ class LoadingPickServiceTest {
verify(repository, times(1)).syncInboundProjections(20L, 9L, "operator");
}
@Test
void batchRecoversPlanResetToEditingWithoutDuplicatingExistingStartHistory() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
item.setPlannedPackageCount(100);
LoadingPlanHistoryEntity existingStart = new LoadingPlanHistoryEntity();
existingStart.setLoadingPlanId(10L);
existingStart.setOperationId("PDA-START-20");
existingStart.setActionType("START_LOADING");
existingStart.setFromStatus("EDITING");
existingStart.setToStatus("LOADING");
when(repository.history(any(String.class))).thenReturn(null);
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.task(20L)).thenReturn(task);
when(repository.assignment(20L)).thenReturn(assignment());
when(repository.items(20L)).thenReturn(List.of(item));
when(repository.items(20L, 101L)).thenReturn(List.of(item));
when(repository.pick(30L, 100, "AUTO", "[]", 0, 101L)).thenReturn(1);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.touchTask(20L, 0)).thenReturn(1);
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
when(smartLoadingRepository.operation("PDA-START-20")).thenReturn(existingStart);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 100))));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
new BatchItemRequest(30L, "PICK", 100, null, "AUTO", List.of(), 0, null)));
service.batch(20L, "batch-reset-plan", request, 9L, "operator");
verify(repository).pick(30L, 100, "AUTO", "[]", 0, 101L);
verify(smartLoadingRepository).startPicking(10L, 9L, "operator");
verify(smartLoadingRepository, never()).insert(any(LoadingPlanHistoryEntity.class));
verify(repository).touchTask(20L, 0);
}
@Test
void rejectsWholeBatchBeforeWritingWhenAnyLineIsInvalid() {
LoadingPickTaskEntity task = task();