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
@@ -5216,7 +5216,7 @@ BEGIN
OR (column_name = 'container_booking_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
OR (column_name = 'cabinet_no' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'NO')
OR (column_name = 'container_no' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES')
OR (column_name = 'task_status' AND LOWER(column_type) = 'varchar(16)' AND is_nullable = 'NO' AND column_default = 'LOADING')
OR (column_name = 'task_status' AND LOWER(column_type) = 'varchar(24)' AND is_nullable = 'NO' AND column_default = 'LOADING')
OR (column_name = 'started_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
OR (column_name = 'started_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES')
OR (column_name = 'started_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%')
@@ -15471,20 +15471,6 @@ WHERE tenant_id=0 AND app_code='PLATFORM'
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
);
/* Phase 3 channel-owned containers and stable PDA identity. Source: firstleg phase3-channel-container-migration.sql */
DROP PROCEDURE IF EXISTS migrate_phase3_channel_containers;
DELIMITER $$
@@ -15648,6 +15634,42 @@ CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_container_transport_history (
KEY idx_container_transport_history(container_booking_id,created_at,id)
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
DROP PROCEDURE IF EXISTS baiyue_firstleg.ensure_container_transport_history_schema;
DELIMITER $$
CREATE PROCEDURE baiyue_firstleg.ensure_container_transport_history_schema()
BEGIN
IF (NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_firstleg'
AND table_name='fl_container_transport_history' AND column_name='operation_id')
OR NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_firstleg'
AND table_name='fl_container_transport_history' AND column_name='request_hash'))
AND EXISTS (SELECT 1 FROM baiyue_firstleg.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='baiyue_firstleg'
AND table_name='fl_container_transport_history' AND column_name='operation_id') THEN
ALTER TABLE baiyue_firstleg.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='baiyue_firstleg'
AND table_name='fl_container_transport_history' AND column_name='request_hash') THEN
ALTER TABLE baiyue_firstleg.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='baiyue_firstleg'
AND table_name='fl_container_transport_history' AND index_name='uk_container_transport_history_operation') THEN
ALTER TABLE baiyue_firstleg.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='baiyue_firstleg'
AND table_name='fl_container_transport_history' AND index_name='idx_container_transport_history') THEN
ALTER TABLE baiyue_firstleg.fl_container_transport_history
ADD KEY idx_container_transport_history (container_booking_id,created_at,id);
END IF;
END$$
DELIMITER ;
CALL baiyue_firstleg.ensure_container_transport_history_schema();
DROP PROCEDURE baiyue_firstleg.ensure_container_transport_history_schema;
ALTER TABLE baiyue_firstleg.fl_container_transport_history
MODIFY COLUMN id BIGINT UNSIGNED NOT NULL COMMENT '运输字段变更历史ID',
MODIFY COLUMN container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
@@ -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();
@@ -163,6 +163,10 @@ public class PlatformUserManagementService {
call(() -> authClient.revokeSessions(internalToken,
new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(user.getAuthUserId()))));
}
if (rolesChanged || warehouseChanged || scopeChanged) {
securityEvents.permissionChanged(user.getAuthUserId(), newTenantId,
appCode(newTenantId), "PLATFORM_USER_AUTHORIZATION_CHANGED");
}
} catch (RuntimeException localFailure) {
compensateUpdate(user.getAuthUserId(), membershipId, oldTenantId, oldAccount,
profileChanged, scopeChanged, localFailure);
@@ -159,6 +159,8 @@ class PlatformUserManagementServiceTest {
assertEquals(900L, membership.getOverseasWarehouseId());
assertEquals(900L, response.overseasWarehouseId());
verify(auth).revokeSessions(eq("internal-token"), any());
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
"PLATFORM_USER_AUTHORIZATION_CHANGED");
}
@Test
@@ -195,6 +197,8 @@ class PlatformUserManagementServiceTest {
assertEquals(901L, response.overseasWarehouseId());
verify(auth).revokeSessions(eq("internal-token"),
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(10L))));
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
"PLATFORM_USER_AUTHORIZATION_CHANGED");
}
@Test
@@ -209,6 +213,8 @@ class PlatformUserManagementServiceTest {
assertNull(response.overseasWarehouseId());
verify(auth).revokeSessions(eq("internal-token"),
eq(new AuthAccountClient.RevokeAccountSessionsRequest(Set.of(10L))));
verify(securityEvents).permissionChanged(10L, 0L, "PLATFORM",
"PLATFORM_USER_AUTHORIZATION_CHANGED");
}
@Test