From f998bed09c7921795938a5a30e2ceba39d5b20e2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 28 Aug 2026 11:28:30 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 52 +++++++++++++------ .../firstleg/service/LoadingPickService.java | 13 ++++- .../db/phase3-channel-container-migration.sql | 36 +++++++++++++ .../db/phase3-smart-loading-schema.sql | 14 ----- ...ChannelContainerMigrationContractTest.java | 4 ++ .../SmartLoadingSqlContractTest.java | 4 +- .../service/LoadingPickServiceTest.java | 38 ++++++++++++++ .../PlatformUserManagementService.java | 4 ++ .../PlatformUserManagementServiceTest.java | 6 +++ 9 files changed, 140 insertions(+), 31 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 067c1cf16..fd08047b6 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -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', diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java index 4fca039ff..20da87435 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java @@ -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"); diff --git a/firstleg/src/main/resources/db/phase3-channel-container-migration.sql b/firstleg/src/main/resources/db/phase3-channel-container-migration.sql index d238600ca..253376b8c 100644 --- a/firstleg/src/main/resources/db/phase3-channel-container-migration.sql +++ b/firstleg/src/main/resources/db/phase3-channel-container-migration.sql @@ -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; diff --git a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql index 96979710b..cf68da615 100644 --- a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql +++ b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql @@ -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 diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3ChannelContainerMigrationContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3ChannelContainerMigrationContractTest.java index 58a17c51b..eb74c0acc 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3ChannelContainerMigrationContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3ChannelContainerMigrationContractTest.java @@ -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)") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java index ce8e6c27e..03c858e58 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java @@ -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)") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index f5ad4b8e7..d1b0c4258 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -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(); diff --git a/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java b/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java index 812c131a9..7d9b01f52 100644 --- a/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java +++ b/iam/src/main/java/com/baiyue/iam/service/PlatformUserManagementService.java @@ -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); diff --git a/iam/src/test/java/com/baiyue/iam/service/PlatformUserManagementServiceTest.java b/iam/src/test/java/com/baiyue/iam/service/PlatformUserManagementServiceTest.java index 292703777..606955222 100644 --- a/iam/src/test/java/com/baiyue/iam/service/PlatformUserManagementServiceTest.java +++ b/iam/src/test/java/com/baiyue/iam/service/PlatformUserManagementServiceTest.java @@ -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