-- 消息加密密钥管理。 -- 可重复执行。根 KEK 只在首次执行时生成,重复执行不会覆盖已有值。 SET NAMES utf8mb4; SET @now := NOW(); SET @im_sql = IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config' AND COLUMN_NAME = 'message_encryption_enabled') = 0, 'ALTER TABLE `im_tenant_config` ADD COLUMN `message_encryption_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否启用消息加密'' AFTER `local_moderation_enabled`', 'SELECT 1' ); PREPARE im_stmt FROM @im_sql; EXECUTE im_stmt; DEALLOCATE PREPARE im_stmt; CREATE TABLE IF NOT EXISTS `im_message_encryption_keys` ( `id` bigint NOT NULL AUTO_INCREMENT, `tenant_id` bigint NOT NULL COMMENT '商户ID', `key_version` int unsigned NOT NULL COMMENT '密钥版本', `algorithm` varchar(32) NOT NULL DEFAULT 'AES-256-GCM' COMMENT '数据加密算法', `encrypted_dek` blob NOT NULL COMMENT '由根KEK包裹后的商户DEK', `dek_nonce` varbinary(12) NOT NULL COMMENT '包裹DEK使用的随机nonce', `status` varchar(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/active/retired/disabled', `activated_at` datetime DEFAULT NULL, `retired_at` datetime DEFAULT NULL, `remark` varchar(255) NOT NULL DEFAULT '', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_by` bigint NOT NULL DEFAULT 0, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated_by` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_im_message_key_tenant_version` (`tenant_id`, `key_version`), KEY `idx_im_message_key_tenant_status` (`tenant_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商户消息加密密钥版本'; INSERT INTO `hg_sys_dict_type` (`pid`, `name`, `type`, `sort`, `remark`, `status`, `created_at`, `updated_at`) SELECT 0, 'IM内部安全材料', 'im_internal_secret', 9999, '仅供服务端读取,通用字典接口禁止查询和维护', 1, @now, @now WHERE NOT EXISTS ( SELECT 1 FROM `hg_sys_dict_type` WHERE `type` = 'im_internal_secret' ); INSERT INTO `hg_sys_dict_data` (`label`, `value`, `value_type`, `type`, `list_class`, `is_default`, `sort`, `remark`, `status`, `created_at`, `updated_at`) SELECT 'core_material_01', TO_BASE64(RANDOM_BYTES(32)), 'string', 'im_internal_secret', '', 2, 1, '消息DEK包裹根密钥KEK,请勿通过后台展示或修改', 1, @now, @now WHERE NOT EXISTS ( SELECT 1 FROM `hg_sys_dict_data` WHERE `type` = 'im_internal_secret' AND `label` = 'core_material_01' ); SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1); SET @settingsId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imSystemSettings' LIMIT 1); INSERT INTO `hg_admin_menu` (`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) SELECT @settingsId, '消息密钥管理', 'imMessageEncryptionIndex', 'message-encryption/index', 'KeyOutlined', 2, '', '/imMessageEncryption/list', '', '/imMessageEncryption/index', 1, 'imManager', 0, 0, '', 0, 0, 0, 3, CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), 80, '商户消息加密开关及密钥版本管理', 1, @now, @now WHERE @settingsId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex'); SET @keyMenuId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionIndex' LIMIT 1); UPDATE `hg_admin_menu` SET `pid` = @settingsId, `title` = '消息密钥管理', `path` = 'message-encryption/index', `icon` = 'KeyOutlined', `permissions` = '/imMessageEncryption/list', `component` = '/imMessageEncryption/index', `active_menu` = 'imManager', `level` = 3, `tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' '), `sort` = 80, `hidden` = 0, `status` = 1, `updated_at` = @now WHERE `id` = @keyMenuId; INSERT INTO `hg_admin_menu` (`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) SELECT @keyMenuId, '初始化密钥', 'imMessageEncryptionInitialize', '', '', 3, '', '/imMessageEncryption/initialize', '', '', 1, '', 0, 0, '', 0, 1, 0, 4, CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 10, '', 1, @now, @now WHERE @keyMenuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionInitialize'); INSERT INTO `hg_admin_menu` (`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) SELECT @keyMenuId, '轮换密钥', 'imMessageEncryptionRotate', '', '', 3, '', '/imMessageEncryption/rotate', '', '', 1, '', 0, 0, '', 0, 1, 0, 4, CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 20, '', 1, @now, @now WHERE @keyMenuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionRotate'); INSERT INTO `hg_admin_menu` (`pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`) SELECT @keyMenuId, '切换消息加密', 'imMessageEncryptionSwitch', '', '', 3, '', '/imMessageEncryption/switch', '', '', 1, '', 0, 0, '', 0, 1, 0, 4, CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), 30, '', 1, @now, @now WHERE @keyMenuId IS NOT NULL AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMessageEncryptionSwitch'); UPDATE `hg_admin_menu` SET `level` = 4, `tree` = CONCAT('tr_', @imRootId, ' tr_', @settingsId, ' tr_', @keyMenuId, ' '), `updated_at` = @now WHERE `pid` = @keyMenuId AND `type` = 3; INSERT IGNORE INTO `hg_admin_role_menu` (`role_id`, `menu_id`) SELECT 1, `id` FROM `hg_admin_menu` WHERE `name` IN ( 'imManager', 'imSystemSettings', 'imMessageEncryptionIndex', 'imMessageEncryptionInitialize', 'imMessageEncryptionRotate', 'imMessageEncryptionSwitch' );