diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index b07517258..13f91ab9a 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -4491,6 +4491,96 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_history ( -- fl_loading_pick_task 曾被另一套入库汇总模型复用。先按字段签名识别并迁走, -- 再创建智能装柜任务表;未知或混合结构必须人工处理,禁止静默套用错误表型。 +DROP PROCEDURE IF EXISTS assert_loading_pick_task_shapes; +DELIMITER $$ +CREATE PROCEDURE assert_loading_pick_task_shapes(IN require_both TINYINT) +BEGIN + DECLARE source_exists INT DEFAULT 0; + DECLARE target_exists INT DEFAULT 0; + DECLARE source_total INT DEFAULT 0; + DECLARE source_smart INT DEFAULT 0; + DECLARE source_inbound INT DEFAULT 0; + DECLARE target_total INT DEFAULT 0; + DECLARE target_inbound INT DEFAULT 0; + SELECT COUNT(*) INTO source_exists FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'; + SELECT COUNT(*) INTO target_exists FROM information_schema.tables + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'; + IF source_exists = 1 THEN + SELECT COUNT(*) INTO source_total FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'; + SELECT COUNT(*) INTO source_smart FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND ( + (column_name = 'id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'loading_plan_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'container_no' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'NO') + OR (column_name = 'task_status' AND LOWER(column_type) = 'varchar(16)' 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%') + OR (column_name = 'completed_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'YES') + OR (column_name = 'completed_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES') + OR (column_name = 'completed_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'YES') + OR (column_name = 'version' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'NO' AND CAST(column_default AS CHAR) = '0')); + SELECT COUNT(*) INTO source_inbound FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND ( + (column_name = 'id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'tenant_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'inbound_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'status' AND LOWER(column_type) = 'varchar(24)' AND is_nullable = 'NO' AND column_default = 'PENDING') + OR (column_name = 'version' AND LOWER(column_type) = 'int' AND is_nullable = 'NO' AND CAST(column_default AS CHAR) = '0') + OR (column_name = 'expected_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES') + OR (column_name = 'picked_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES') + OR (column_name = 'remark' AND LOWER(column_type) = 'varchar(1000)' AND is_nullable = 'YES') + OR (column_name = 'updated_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'updated_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES') + OR (column_name = 'created_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%') + OR (column_name = 'updated_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%' AND LOWER(extra) LIKE '%on update current_timestamp%')); + IF NOT ( + (source_total = 11 AND source_smart = 11 + AND (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND index_name = 'uk_loading_pick_task_plan' AND non_unique = 0 AND seq_in_index = 1 AND column_name = 'loading_plan_id') = 1 + AND (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND index_name = 'idx_loading_pick_container' AND ((seq_in_index = 1 AND column_name = 'container_no') OR (seq_in_index = 2 AND column_name = 'task_status') OR (seq_in_index = 3 AND column_name = 'started_at'))) = 3 + AND (SELECT COUNT(*) FROM information_schema.key_column_usage WHERE constraint_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND column_name = 'loading_plan_id' AND referenced_table_name = 'fl_loading_plan' AND referenced_column_name = 'id') = 1) + OR + (source_total = 12 AND source_inbound = 12 + AND (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND index_name = 'uk_loading_pick_tenant_inbound' AND non_unique = 0 AND ((seq_in_index = 1 AND column_name = 'tenant_id') OR (seq_in_index = 2 AND column_name = 'inbound_id'))) = 2 + AND (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND index_name = 'idx_loading_pick_status' AND ((seq_in_index = 1 AND column_name = 'tenant_id') OR (seq_in_index = 2 AND column_name = 'status') OR (seq_in_index = 3 AND column_name = 'updated_at'))) = 3 + AND (SELECT COUNT(*) FROM information_schema.key_column_usage WHERE constraint_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND column_name = 'inbound_id' AND referenced_table_name = 'fl_inbound_order' AND referenced_column_name = 'id') = 1) + ) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拣货任务迁移失败:fl_loading_pick_task 不是完整 canonical 表型'; + END IF; + END IF; + IF target_exists = 1 THEN + SELECT COUNT(*) INTO target_total FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'; + SELECT COUNT(*) INTO target_inbound FROM information_schema.columns + WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task' AND ( + (column_name = 'id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'tenant_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'inbound_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'status' AND LOWER(column_type) = 'varchar(24)' AND is_nullable = 'NO' AND column_default = 'PENDING') + OR (column_name = 'version' AND LOWER(column_type) = 'int' AND is_nullable = 'NO' AND CAST(column_default AS CHAR) = '0') + OR (column_name = 'expected_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES') + OR (column_name = 'picked_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES') + OR (column_name = 'remark' AND LOWER(column_type) = 'varchar(1000)' AND is_nullable = 'YES') + OR (column_name = 'updated_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO') + OR (column_name = 'updated_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES') + OR (column_name = 'created_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%') + OR (column_name = 'updated_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%' AND LOWER(extra) LIKE '%on update current_timestamp%')); + IF target_total <> 12 OR target_inbound <> 12 + OR (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task' AND index_name = 'uk_loading_pick_tenant_inbound' AND non_unique = 0 AND ((seq_in_index = 1 AND column_name = 'tenant_id') OR (seq_in_index = 2 AND column_name = 'inbound_id'))) <> 2 + OR (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task' AND index_name = 'idx_loading_pick_status' AND ((seq_in_index = 1 AND column_name = 'tenant_id') OR (seq_in_index = 2 AND column_name = 'status') OR (seq_in_index = 3 AND column_name = 'updated_at'))) <> 3 + OR (SELECT COUNT(*) FROM information_schema.key_column_usage WHERE constraint_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task' AND column_name = 'inbound_id' AND referenced_table_name = 'fl_inbound_order' AND referenced_column_name = 'id') <> 1 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拣货任务迁移失败:fl_inbound_loading_pick_task 不是完整 canonical 表型'; + END IF; + END IF; + IF require_both = 1 AND (source_exists <> 1 OR target_exists <> 1 OR source_smart <> 11) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '拣货任务终验失败:两张 canonical 表未同时就绪'; + END IF; +END$$ +DELIMITER ; +CALL assert_loading_pick_task_shapes(0); + DROP PROCEDURE IF EXISTS migrate_legacy_loading_pick_task; DELIMITER $$ CREATE PROCEDURE migrate_legacy_loading_pick_task() @@ -5037,6 +5127,32 @@ DELIMITER ; -- 三期智能装柜、预装清单和装柜清单平台权限;与一期平台根菜单幂等合并。 USE baiyue_iam; +DROP PROCEDURE IF EXISTS assert_smart_loading_permission_ids_available; +DELIMITER $$ +CREATE PROCEDURE assert_smart_loading_permission_ids_available() +BEGIN + IF EXISTS ( + SELECT 1 FROM iam_permission + WHERE id IN (5102091, 5102092, 5102093, 5102094, 5102096, 5102097, 5102098, 5102099) + AND permission_code NOT IN ( + 'platform:firstleg:container:view', 'platform:firstleg:container:create', + 'platform:firstleg:container:edit', 'platform:firstleg:smart-loading:list', + 'platform:firstleg:smart-loading:create', 'platform:firstleg:smart-loading:assign', + 'platform:firstleg:smart-loading:delete', 'platform:firstleg:smart-loading:push', + 'firstleg:inbound:split', 'firstleg:inbound:preload', 'firstleg:inbound:preload-view', + 'platform:firstleg:inbound:loading:edit', 'platform:firstleg:inbound:loading:view', + 'platform:firstleg:inbound:loading:complete', 'platform:firstleg:inbound:delivery:complete', + 'platform:firstleg:inbound:picking:edit', 'platform:firstleg:inbound:picking:view' + ) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '三期权限迁移失败:5102091-5102099 存在未知权限占用'; + END IF; +END$$ +DELIMITER ; +CALL assert_smart_loading_permission_ids_available(); +DROP PROCEDURE assert_smart_loading_permission_ids_available; + INSERT INTO iam_permission (id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, built_in, deleted) @@ -5207,8 +5323,6 @@ BEGIN END IF; END$$ DELIMITER ; -CALL assert_phase3_smart_loading_schema(); -DROP PROCEDURE IF EXISTS assert_phase3_smart_loading_schema; -- ============================================================ @@ -6242,6 +6356,11 @@ SELECT role.id, permission.id, role.tenant_id ) WHERE role.tenant_id = 0 AND role.role_code IN ('PLATFORM_ADMIN', 'ORDER_OPERATOR'); +-- 污染存量库必须先完成旧入库权限搬迁和两类授权恢复,再校验智能装柜权限。 +USE baiyue_firstleg; +CALL assert_phase3_smart_loading_schema(); +DROP PROCEDURE IF EXISTS assert_phase3_smart_loading_schema; + -- 兼容已经执行过旧单行配置模型的开发/测试库。仅精确识别旧结构时丢弃旧值并重建; -- 已是四箱规结构时不做 DDL,避免破坏 Admin 已编辑的数据。 DROP PROCEDURE IF EXISTS unified_upgrade_legacy_customer_dimension_config; @@ -7109,11 +7228,27 @@ BEGIN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '统一迁移失败:三期入库装柜权限 ID 未完成迁移'; END IF; + IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission + WHERE tenant_id = 0 AND app_code = 'PLATFORM' + AND ((id = 5102091 AND permission_code = 'platform:firstleg:container:view' AND parent_id = 5102090) + OR (id = 5102092 AND permission_code = 'platform:firstleg:container:create' AND parent_id = 5102090) + OR (id = 5102093 AND permission_code = 'platform:firstleg:container:edit' AND parent_id = 5102090) + OR (id = 5102094 AND permission_code = 'platform:firstleg:smart-loading:list' AND parent_id = 5102000) + OR (id = 5102096 AND permission_code = 'platform:firstleg:smart-loading:create' AND parent_id = 5102094) + OR (id = 5102097 AND permission_code = 'platform:firstleg:smart-loading:assign' AND parent_id = 5102094) + OR (id = 5102098 AND permission_code = 'platform:firstleg:smart-loading:delete' AND parent_id = 5102094) + OR (id = 5102099 AND permission_code = 'platform:firstleg:smart-loading:push' AND parent_id = 5102094))) <> 8 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '统一迁移失败:智能装柜权限 ID 或父子关系不正确'; + END IF; END$$ DELIMITER ; CALL phase3_assert_inbound_parent_child_schema(); DROP PROCEDURE IF EXISTS phase3_assert_inbound_parent_child_schema; +CALL assert_loading_pick_task_shapes(1); +DROP PROCEDURE IF EXISTS assert_loading_pick_task_shapes; + -- WAIT_SPLIT 是二期当前有效终点;统一迁移不得改写主表或历史状态。 SELECT 'PASS' AS migration_status, '统一增量全部执行完成;箱号映射、单箱测量、三期出仓、费用、公共资源及可靠审计结构校验通过' AS message; 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 5728672e9..553f42b4b 100644 --- a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql +++ b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql @@ -868,6 +868,23 @@ DELIMITER $$ CREATE PROCEDURE relocate_legacy_inbound_loading_permissions() BEGIN DECLARE legacy_count INT DEFAULT 0; + IF EXISTS ( + SELECT 1 FROM iam_permission + WHERE id IN (5102091, 5102092, 5102093, 5102094, 5102096, 5102097, 5102098, 5102099) + AND permission_code NOT IN ( + 'platform:firstleg:container:view', 'platform:firstleg:container:create', + 'platform:firstleg:container:edit', 'platform:firstleg:smart-loading:list', + 'platform:firstleg:smart-loading:create', 'platform:firstleg:smart-loading:assign', + 'platform:firstleg:smart-loading:delete', 'platform:firstleg:smart-loading:push', + 'firstleg:inbound:split', 'firstleg:inbound:preload', 'firstleg:inbound:preload-view', + 'platform:firstleg:inbound:loading:edit', 'platform:firstleg:inbound:loading:view', + 'platform:firstleg:inbound:loading:complete', 'platform:firstleg:inbound:delivery:complete', + 'platform:firstleg:inbound:picking:edit', 'platform:firstleg:inbound:picking:view' + ) + ) THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = '三期权限迁移失败:5102091-5102099 存在未知权限占用'; + END IF; SELECT COUNT(*) INTO legacy_count FROM iam_permission WHERE tenant_id = 0 AND app_code = 'PLATFORM' 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 c2d202371..3c80d704b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/SmartLoadingSqlContractTest.java @@ -158,6 +158,15 @@ class SmartLoadingSqlContractTest { .isEqualTo(1); assertThat(count(unified, "SELECT 'PASS' AS migration_status")) .isEqualTo(1); + int relocation = unified.indexOf("CALL unified_relocate_inbound_loading_permissions();"); + int assertion = unified.indexOf("CALL assert_phase3_smart_loading_schema();"); + assertThat(relocation).isGreaterThan(0); + assertThat(assertion).isGreaterThan(relocation); + assertThat(unified.substring(relocation, assertion)) + .contains("tmp_phase3_inbound_role_grant") + .contains("DROP TEMPORARY TABLE IF EXISTS tmp_phase3_inbound_role_grant") + .contains("5102121, 0, 'PLATFORM'") + .contains("5102129, 0, 'PLATFORM'"); } @Test @@ -180,6 +189,13 @@ class SmartLoadingSqlContractTest { .contains("KEY idx_loading_pick_status (tenant_id, status, updated_at)"); } + @Test + @DisplayName("迁移拒绝缺列错类型和混合拣货表型") + void shouldRejectMalformedLegacyPickTaskShapes() throws Exception { + assertMalformedShapeGuards(migration()); + assertMalformedShapeGuards(unifiedMigration()); + } + private static String migration() throws Exception { return new ClassPathResource("db/phase3-smart-loading-schema.sql") .getContentAsString(StandardCharsets.UTF_8); @@ -213,6 +229,27 @@ class SmartLoadingSqlContractTest { return matcher.results().map(result -> result.group(1)).collect(Collectors.toSet()); } + private static void assertMalformedShapeGuards(String sql) { + assertThat(sql) + .containsAnyOf("source_total_columns = 11", "source_total = 11") + .containsAnyOf("source_smart_columns = 11", "source_smart = 11") + .containsAnyOf("source_total_columns = 12", "source_total = 12") + .containsAnyOf("source_inbound_columns = 12", "source_inbound = 12") + .contains("column_name = 'version'") + .contains("column_name = 'remark'") + .contains("column_name = 'updated_by'") + .contains("column_name = 'updated_by_name'") + .contains("column_name = 'created_at'") + .contains("column_name = 'updated_at'") + .contains("LOWER(column_type) = 'int'") + .contains("LOWER(column_type) = 'int unsigned'") + .contains("LOWER(extra) LIKE '%on update current_timestamp%'") + .contains("uk_loading_pick_tenant_inbound") + .contains("idx_loading_pick_status") + .contains("referenced_table_name = 'fl_inbound_order'") + .contains("字段签名未知或混合"); + } + private static int count(String value, String token) { return (value.length() - value.replace(token, "").length()) / token.length(); }