diff --git a/backend/sql/migrations/upgrade_imeeting_qy_to_latest.sql b/backend/sql/migrations/upgrade_imeeting_qy_to_latest.sql new file mode 100644 index 0000000..cd485da --- /dev/null +++ b/backend/sql/migrations/upgrade_imeeting_qy_to_latest.sql @@ -0,0 +1,1240 @@ +-- iMeeting legacy DB upgrade +-- Goal: upgrade an old schema to the latest application-facing structure +-- Strategy: +-- 1) standardize system tables to sys_* base tables +-- 2) align shared table columns/indexes +-- 3) create missing configuration tables +-- 4) migrate legacy configuration/hot-word data into new tables +-- 5) rebuild menu tree and role permissions needed by the latest backend +-- 6) recreate compatibility views + +-- ---------------------------------------------------------------------- +-- 1. Rename legacy system tables to sys_* base tables when needed +-- ---------------------------------------------------------------------- +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'users' AND table_type = 'BASE TABLE' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'sys_users' + ), + 'RENAME TABLE users TO sys_users', + 'SELECT 1' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'roles' AND table_type = 'BASE TABLE' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'sys_roles' + ), + 'RENAME TABLE roles TO sys_roles', + 'SELECT 1' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'menus' AND table_type = 'BASE TABLE' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' + ), + 'RENAME TABLE menus TO sys_menus', + 'SELECT 1' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'role_menu_permissions' AND table_type = 'BASE TABLE' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' + ), + 'RENAME TABLE role_menu_permissions TO sys_role_menu_permissions', + 'SELECT 1' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'dict_data' AND table_type = 'BASE TABLE' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'sys_dict_data' + ), + 'RENAME TABLE dict_data TO sys_dict_data', + 'SELECT 1' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'system_parameters' AND table_type = 'BASE TABLE' + ) AND NOT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'sys_system_parameters' + ), + 'RENAME TABLE system_parameters TO sys_system_parameters', + 'SELECT 1' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ---------------------------------------------------------------------- +-- 2. Align existing sys_* tables +-- ---------------------------------------------------------------------- +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_users' AND index_name = 'idx_role_id' + ), + 'SELECT 1', + 'ALTER TABLE sys_users ADD KEY idx_role_id (role_id)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_roles' AND index_name = 'uk_role_name' + ), + 'SELECT 1', + 'ALTER TABLE sys_roles ADD UNIQUE KEY uk_role_name (role_name)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND column_name = 'menu_level' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD COLUMN menu_level TINYINT(3) NOT NULL DEFAULT 1 COMMENT ''菜单层级(根节点为1)'' AFTER parent_id' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND column_name = 'tree_path' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD COLUMN tree_path VARCHAR(255) DEFAULT NULL COMMENT ''树路径(如 /3/6)'' AFTER menu_level' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND column_name = 'is_visible' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD COLUMN is_visible TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否在侧边菜单显示'' AFTER is_active' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND index_name = 'idx_menu_level' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD KEY idx_menu_level (menu_level)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND index_name = 'idx_tree_path' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD KEY idx_tree_path (tree_path)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND index_name = 'idx_is_visible' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD KEY idx_is_visible (is_visible)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_menus' AND index_name = 'idx_menus_visible_tree' + ), + 'SELECT 1', + 'ALTER TABLE sys_menus ADD KEY idx_menus_visible_tree (is_active, is_visible, parent_id, sort_order, menu_id)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' AND column_name = 'granted_by' + ), + 'SELECT 1', + 'ALTER TABLE sys_role_menu_permissions ADD COLUMN granted_by INT(11) DEFAULT NULL COMMENT ''授权操作人ID'' AFTER menu_id' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' AND column_name = 'granted_at' + ), + 'SELECT 1', + 'ALTER TABLE sys_role_menu_permissions ADD COLUMN granted_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''授权时间'' AFTER granted_by' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' AND index_name = 'idx_granted_by' + ), + 'SELECT 1', + 'ALTER TABLE sys_role_menu_permissions ADD KEY idx_granted_by (granted_by)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' AND index_name = 'idx_granted_at' + ), + 'SELECT 1', + 'ALTER TABLE sys_role_menu_permissions ADD KEY idx_granted_at (granted_at)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' AND index_name = 'idx_rmp_role' + ), + 'SELECT 1', + 'ALTER TABLE sys_role_menu_permissions ADD KEY idx_rmp_role (role_id)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'sys_role_menu_permissions' AND index_name = 'idx_rmp_menu' + ), + 'SELECT 1', + 'ALTER TABLE sys_role_menu_permissions ADD KEY idx_rmp_menu (menu_id)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE `terminals` +MODIFY COLUMN `current_user_id` INT(11) DEFAULT NULL COMMENT '终端绑定账号'; + +-- prompts: add is_system and composite index +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'prompts' AND column_name = 'is_system' + ), + 'SELECT 1', + 'ALTER TABLE prompts ADD COLUMN is_system TINYINT(1) NOT NULL DEFAULT 0 AFTER creator_id' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF( + EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'prompts' AND index_name = 'idx_prompts_task_scope_active' + ), + 'SELECT 1', + 'CREATE INDEX idx_prompts_task_scope_active ON prompts (task_type, is_system, creator_id, is_active, is_default)' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +UPDATE prompts +SET is_system = 1 +WHERE creator_id = 1; + +-- ---------------------------------------------------------------------- +-- 3. Create missing tables with latest structure +-- ---------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `sys_system_parameters` ( + `param_id` bigint(20) NOT NULL AUTO_INCREMENT, + `param_key` varchar(128) NOT NULL, + `param_name` varchar(255) NOT NULL, + `param_value` text, + `value_type` varchar(32) NOT NULL DEFAULT 'string', + `category` varchar(64) NOT NULL DEFAULT 'system', + `description` varchar(500) DEFAULT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`param_id`), + UNIQUE KEY `uk_param_key` (`param_key`), + KEY `idx_param_category` (`category`), + KEY `idx_param_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `ai_model_configs` ( + `model_id` bigint(20) NOT NULL AUTO_INCREMENT, + `model_code` varchar(128) NOT NULL, + `model_name` varchar(255) NOT NULL, + `model_type` varchar(32) NOT NULL, + `provider` varchar(64) DEFAULT NULL, + `endpoint_url` varchar(512) DEFAULT NULL, + `api_key` varchar(512) DEFAULT NULL, + `llm_model_name` varchar(128) DEFAULT NULL, + `llm_timeout` int(11) DEFAULT NULL, + `llm_temperature` decimal(5,2) DEFAULT NULL, + `llm_top_p` decimal(5,2) DEFAULT NULL, + `llm_max_tokens` int(11) DEFAULT NULL, + `llm_system_prompt` text, + `asr_model_name` varchar(128) DEFAULT NULL, + `asr_vocabulary_id` varchar(255) DEFAULT NULL, + `asr_speaker_count` int(11) DEFAULT NULL, + `asr_language_hints` varchar(255) DEFAULT NULL, + `asr_disfluency_removal_enabled` tinyint(1) DEFAULT NULL, + `asr_diarization_enabled` tinyint(1) DEFAULT NULL, + `config_json` json DEFAULT NULL, + `description` varchar(500) DEFAULT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`model_id`), + UNIQUE KEY `uk_model_code` (`model_code`), + KEY `idx_model_type` (`model_type`), + KEY `idx_model_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `llm_model_config` ( + `config_id` bigint(20) NOT NULL AUTO_INCREMENT, + `model_code` varchar(128) NOT NULL, + `model_name` varchar(255) NOT NULL, + `provider` varchar(64) DEFAULT NULL, + `endpoint_url` varchar(512) DEFAULT NULL, + `api_key` varchar(512) DEFAULT NULL, + `llm_model_name` varchar(128) NOT NULL, + `llm_timeout` int(11) NOT NULL DEFAULT '120', + `llm_temperature` decimal(5,2) NOT NULL DEFAULT '0.70', + `llm_top_p` decimal(5,2) NOT NULL DEFAULT '0.90', + `llm_max_tokens` int(11) NOT NULL DEFAULT '2048', + `llm_system_prompt` text, + `description` varchar(500) DEFAULT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`config_id`), + UNIQUE KEY `uk_llm_model_code` (`model_code`), + KEY `idx_llm_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `audio_model_config` ( + `config_id` bigint(20) NOT NULL AUTO_INCREMENT, + `model_code` varchar(128) NOT NULL, + `model_name` varchar(255) NOT NULL, + `audio_scene` varchar(32) NOT NULL, + `provider` varchar(64) DEFAULT NULL, + `endpoint_url` varchar(512) DEFAULT NULL, + `api_key` varchar(512) DEFAULT NULL, + `asr_model_name` varchar(128) DEFAULT NULL, + `asr_vocabulary_id` varchar(255) DEFAULT NULL, + `hot_word_group_id` int(11) DEFAULT NULL COMMENT '关联热词组ID', + `extra_config` json DEFAULT NULL COMMENT '音频模型差异化配置(JSON)', + `asr_speaker_count` int(11) DEFAULT NULL, + `asr_language_hints` varchar(255) DEFAULT NULL, + `asr_disfluency_removal_enabled` tinyint(1) DEFAULT NULL, + `asr_diarization_enabled` tinyint(1) DEFAULT NULL, + `vp_template_text` text, + `vp_duration_seconds` int(11) DEFAULT NULL, + `vp_sample_rate` int(11) DEFAULT NULL, + `vp_channels` int(11) DEFAULT NULL, + `vp_max_size_bytes` bigint(20) DEFAULT NULL, + `description` varchar(500) DEFAULT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`config_id`), + UNIQUE KEY `uk_audio_model_code` (`model_code`), + KEY `idx_audio_scene` (`audio_scene`), + KEY `idx_audio_active` (`is_active`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `hot_word_group` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '热词组名称', + `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述', + `vocabulary_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '阿里云 DashScope 词表ID', + `last_sync_time` datetime DEFAULT NULL COMMENT '最后同步时间', + `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1:启用 0:停用', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='热词组主表'; + +CREATE TABLE IF NOT EXISTS `hot_word_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `group_id` int(11) NOT NULL COMMENT '热词组ID', + `text` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '热词内容', + `weight` int(11) NOT NULL DEFAULT '4' COMMENT '权重 1-10', + `lang` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zh' COMMENT 'zh/en', + `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1:启用 0:停用', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `idx_group_text` (`group_id`,`text`), + KEY `idx_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='热词条目从表'; + +CREATE TABLE IF NOT EXISTS `prompt_config` ( + `config_id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `task_type` enum('MEETING_TASK','KNOWLEDGE_TASK') NOT NULL, + `prompt_id` int(11) NOT NULL, + `is_enabled` tinyint(1) NOT NULL DEFAULT '1', + `sort_order` int(11) NOT NULL DEFAULT '0', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`config_id`), + UNIQUE KEY `uk_user_task_prompt` (`user_id`,`task_type`,`prompt_id`), + KEY `idx_user_task_order` (`user_id`,`task_type`,`sort_order`), + KEY `idx_prompt_id` (`prompt_id`), + CONSTRAINT `fk_upc_prompt` FOREIGN KEY (`prompt_id`) REFERENCES `prompts` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_upc_user` FOREIGN KEY (`user_id`) REFERENCES `sys_users` (`user_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `sys_user_mcp` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `bot_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `bot_secret` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL, + `status` tinyint(1) NOT NULL DEFAULT '1', + `last_used_at` datetime DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_sys_user_mcp_user_id` (`user_id`), + UNIQUE KEY `uk_sys_user_mcp_bot_id` (`bot_id`), + KEY `idx_sys_user_mcp_status` (`status`), + CONSTRAINT `fk_sys_user_mcp_user` FOREIGN KEY (`user_id`) REFERENCES `sys_users` (`user_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户MCP接入凭证'; + +-- ---------------------------------------------------------------------- +-- 4. Migrate legacy system config data into new parameter/model tables +-- ---------------------------------------------------------------------- +INSERT INTO `sys_system_parameters` +(`param_key`, `param_name`, `param_value`, `value_type`, `category`, `description`, `is_active`) +SELECT + d.`dict_code`, + d.`label_cn`, + JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.value')), + 'string', + 'system', + CONCAT('migrated from dict_data.system_config(', d.`dict_code`, ')'), + CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END +FROM `sys_dict_data` d +WHERE d.`dict_type` = 'system_config' + AND d.`dict_code` NOT IN ('llm_model', 'voiceprint') + AND JSON_EXTRACT(d.`extension_attr`, '$.value') IS NOT NULL +ON DUPLICATE KEY UPDATE + `param_name` = VALUES(`param_name`), + `param_value` = VALUES(`param_value`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`); + +INSERT INTO `ai_model_configs` +(`model_code`, `model_name`, `model_type`, `provider`, `endpoint_url`, `api_key`, + `llm_model_name`, `llm_timeout`, `llm_temperature`, `llm_top_p`, `llm_max_tokens`, `llm_system_prompt`, + `config_json`, `description`, `is_active`, `is_default`) +SELECT + 'llm_model', + '默认文本模型', + 'llm', + 'dashscope', + 'https://dashscope.aliyuncs.com/compatible-mode/v1', + NULL, + COALESCE(JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.model_name')), 'qwen-plus'), + COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.time_out')) AS UNSIGNED), 120), + COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.temperature')) AS DECIMAL(5,2)), 0.70), + COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.top_p')) AS DECIMAL(5,2)), 0.90), + COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.max_tokens')) AS UNSIGNED), 2048), + JSON_UNQUOTE(JSON_EXTRACT(d.`extension_attr`, '$.system_prompt')), + d.`extension_attr`, + 'migrated from dict_data.system_config.llm_model', + CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END, + 1 +FROM `sys_dict_data` d +WHERE d.`dict_type` = 'system_config' + AND d.`dict_code` = 'llm_model' +LIMIT 1 +ON DUPLICATE KEY UPDATE + `model_name` = VALUES(`model_name`), + `provider` = VALUES(`provider`), + `endpoint_url` = VALUES(`endpoint_url`), + `llm_model_name` = VALUES(`llm_model_name`), + `llm_timeout` = VALUES(`llm_timeout`), + `llm_temperature` = VALUES(`llm_temperature`), + `llm_top_p` = VALUES(`llm_top_p`), + `llm_max_tokens` = VALUES(`llm_max_tokens`), + `llm_system_prompt` = VALUES(`llm_system_prompt`), + `config_json` = VALUES(`config_json`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`), + `is_default` = VALUES(`is_default`); + +INSERT INTO `ai_model_configs` +(`model_code`, `model_name`, `model_type`, `provider`, `endpoint_url`, `api_key`, + `asr_model_name`, `asr_vocabulary_id`, `asr_speaker_count`, `asr_language_hints`, + `asr_disfluency_removal_enabled`, `asr_diarization_enabled`, + `config_json`, `description`, `is_active`, `is_default`) +SELECT + 'audio_model', + '默认音频识别模型', + 'audio', + 'dashscope', + 'https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription', + NULL, + 'paraformer-v2', + ( + SELECT JSON_UNQUOTE(JSON_EXTRACT(d2.`extension_attr`, '$.value')) + FROM `sys_dict_data` d2 + WHERE d2.`dict_type` = 'system_config' AND d2.`dict_code` = 'asr_vocabulary_id' + LIMIT 1 + ), + 10, + 'zh, en', + 1, + 1, + JSON_OBJECT( + 'model', 'paraformer-v2', + 'vocabulary_id', ( + SELECT JSON_UNQUOTE(JSON_EXTRACT(d3.`extension_attr`, '$.value')) + FROM `sys_dict_data` d3 + WHERE d3.`dict_type` = 'system_config' AND d3.`dict_code` = 'asr_vocabulary_id' + LIMIT 1 + ), + 'speaker_count', 10, + 'language_hints', 'zh, en', + 'disfluency_removal_enabled', TRUE, + 'diarization_enabled', TRUE + ), + 'migrated default ASR config', + 1, + 1 +FROM DUAL +ON DUPLICATE KEY UPDATE + `model_name` = VALUES(`model_name`), + `provider` = VALUES(`provider`), + `endpoint_url` = VALUES(`endpoint_url`), + `asr_model_name` = VALUES(`asr_model_name`), + `asr_vocabulary_id` = VALUES(`asr_vocabulary_id`), + `asr_speaker_count` = VALUES(`asr_speaker_count`), + `asr_language_hints` = VALUES(`asr_language_hints`), + `asr_disfluency_removal_enabled` = VALUES(`asr_disfluency_removal_enabled`), + `asr_diarization_enabled` = VALUES(`asr_diarization_enabled`), + `config_json` = VALUES(`config_json`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`), + `is_default` = VALUES(`is_default`); + +INSERT INTO `ai_model_configs` +(`model_code`, `model_name`, `model_type`, `provider`, `endpoint_url`, `api_key`, + `config_json`, `description`, `is_active`, `is_default`) +SELECT + 'voiceprint_model', + '默认声纹模型', + 'audio', + 'funasr', + 'http://127.0.0.1:10095', + NULL, + d.`extension_attr`, + 'migrated from dict_data.system_config.voiceprint', + CASE WHEN d.`status` = 1 THEN 1 ELSE 0 END, + 0 +FROM `sys_dict_data` d +WHERE d.`dict_type` = 'system_config' + AND d.`dict_code` = 'voiceprint' +LIMIT 1 +ON DUPLICATE KEY UPDATE + `model_name` = VALUES(`model_name`), + `provider` = VALUES(`provider`), + `endpoint_url` = VALUES(`endpoint_url`), + `config_json` = VALUES(`config_json`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`), + `is_default` = VALUES(`is_default`); + +INSERT INTO `llm_model_config` +(`model_code`, `model_name`, `provider`, `endpoint_url`, `api_key`, `llm_model_name`, `llm_timeout`, + `llm_temperature`, `llm_top_p`, `llm_max_tokens`, `llm_system_prompt`, `description`, `is_active`, `is_default`) +SELECT + `model_code`, + `model_name`, + `provider`, + `endpoint_url`, + `api_key`, + COALESCE(`llm_model_name`, JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.model_name')), 'qwen-plus'), + COALESCE(`llm_timeout`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.time_out')) AS UNSIGNED), 120), + COALESCE(`llm_temperature`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.temperature')) AS DECIMAL(5,2)), 0.70), + COALESCE(`llm_top_p`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.top_p')) AS DECIMAL(5,2)), 0.90), + COALESCE(`llm_max_tokens`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.max_tokens')) AS UNSIGNED), 2048), + COALESCE(`llm_system_prompt`, JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.system_prompt'))), + `description`, + `is_active`, + `is_default` +FROM `ai_model_configs` +WHERE `model_type` = 'llm' +ON DUPLICATE KEY UPDATE + `model_name` = VALUES(`model_name`), + `provider` = VALUES(`provider`), + `endpoint_url` = VALUES(`endpoint_url`), + `api_key` = VALUES(`api_key`), + `llm_model_name` = VALUES(`llm_model_name`), + `llm_timeout` = VALUES(`llm_timeout`), + `llm_temperature` = VALUES(`llm_temperature`), + `llm_top_p` = VALUES(`llm_top_p`), + `llm_max_tokens` = VALUES(`llm_max_tokens`), + `llm_system_prompt` = VALUES(`llm_system_prompt`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`), + `is_default` = VALUES(`is_default`); + +INSERT INTO `audio_model_config` +(`model_code`, `model_name`, `audio_scene`, `provider`, `endpoint_url`, `api_key`, `asr_model_name`, `asr_vocabulary_id`, + `asr_speaker_count`, `asr_language_hints`, `asr_disfluency_removal_enabled`, `asr_diarization_enabled`, + `description`, `is_active`, `is_default`) +SELECT + `model_code`, + `model_name`, + 'asr', + `provider`, + `endpoint_url`, + `api_key`, + COALESCE(`asr_model_name`, JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.model')), 'paraformer-v2'), + COALESCE(`asr_vocabulary_id`, JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.vocabulary_id'))), + COALESCE(`asr_speaker_count`, CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.speaker_count')) AS UNSIGNED), 10), + COALESCE(`asr_language_hints`, 'zh, en'), + COALESCE(`asr_disfluency_removal_enabled`, 1), + COALESCE(`asr_diarization_enabled`, 1), + `description`, + `is_active`, + `is_default` +FROM `ai_model_configs` +WHERE `model_code` = 'audio_model' +ON DUPLICATE KEY UPDATE + `model_name` = VALUES(`model_name`), + `provider` = VALUES(`provider`), + `endpoint_url` = VALUES(`endpoint_url`), + `api_key` = VALUES(`api_key`), + `asr_model_name` = VALUES(`asr_model_name`), + `asr_vocabulary_id` = VALUES(`asr_vocabulary_id`), + `asr_speaker_count` = VALUES(`asr_speaker_count`), + `asr_language_hints` = VALUES(`asr_language_hints`), + `asr_disfluency_removal_enabled` = VALUES(`asr_disfluency_removal_enabled`), + `asr_diarization_enabled` = VALUES(`asr_diarization_enabled`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`), + `is_default` = VALUES(`is_default`); + +INSERT INTO `audio_model_config` +(`model_code`, `model_name`, `audio_scene`, `provider`, `endpoint_url`, `api_key`, + `vp_template_text`, `vp_duration_seconds`, `vp_sample_rate`, `vp_channels`, `vp_max_size_bytes`, + `description`, `is_active`, `is_default`) +SELECT + `model_code`, + `model_name`, + 'voiceprint', + `provider`, + `endpoint_url`, + `api_key`, + JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.template_text')), + CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.duration_seconds')) AS UNSIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.sample_rate')) AS UNSIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.channels')) AS UNSIGNED), + CAST(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.voiceprint_max_size')) AS UNSIGNED), + `description`, + `is_active`, + `is_default` +FROM `ai_model_configs` +WHERE `model_code` = 'voiceprint_model' +ON DUPLICATE KEY UPDATE + `model_name` = VALUES(`model_name`), + `provider` = VALUES(`provider`), + `endpoint_url` = VALUES(`endpoint_url`), + `api_key` = VALUES(`api_key`), + `vp_template_text` = VALUES(`vp_template_text`), + `vp_duration_seconds` = VALUES(`vp_duration_seconds`), + `vp_sample_rate` = VALUES(`vp_sample_rate`), + `vp_channels` = VALUES(`vp_channels`), + `vp_max_size_bytes` = VALUES(`vp_max_size_bytes`), + `description` = VALUES(`description`), + `is_active` = VALUES(`is_active`), + `is_default` = VALUES(`is_default`); + +-- Backfill extra_config JSON for audio model configs +UPDATE `audio_model_config` +SET `extra_config` = CASE + WHEN `audio_scene` = 'asr' THEN JSON_OBJECT( + 'model', `asr_model_name`, + 'vocabulary_id', `asr_vocabulary_id`, + 'speaker_count', `asr_speaker_count`, + 'language_hints', `asr_language_hints`, + 'disfluency_removal_enabled', `asr_disfluency_removal_enabled`, + 'diarization_enabled', `asr_diarization_enabled` + ) + WHEN `audio_scene` = 'voiceprint' THEN JSON_OBJECT( + 'model', `model_name`, + 'template_text', `vp_template_text`, + 'duration_seconds', `vp_duration_seconds`, + 'sample_rate', `vp_sample_rate`, + 'channels', `vp_channels`, + 'max_size_bytes', `vp_max_size_bytes` + ) + ELSE JSON_OBJECT() +END +WHERE `extra_config` IS NULL; + +-- ---------------------------------------------------------------------- +-- 5. Hot-word migration: old hot_words -> group/item +-- ---------------------------------------------------------------------- +INSERT INTO `hot_word_group` (`name`, `description`, `status`) +SELECT '默认热词组', '从旧 hot_words 表迁移的热词', 1 +FROM DUAL +WHERE EXISTS (SELECT 1 FROM `hot_words` LIMIT 1) + AND NOT EXISTS (SELECT 1 FROM `hot_word_group` WHERE `name` = '默认热词组'); + +UPDATE `hot_word_group` g +JOIN ( + SELECT JSON_UNQUOTE(JSON_EXTRACT(`extension_attr`, '$.value')) AS vocab_id + FROM `sys_dict_data` + WHERE `dict_type` = 'system_config' AND `dict_code` = 'asr_vocabulary_id' + LIMIT 1 +) p ON 1 = 1 +SET g.`vocabulary_id` = p.`vocab_id`, + g.`last_sync_time` = COALESCE(g.`last_sync_time`, NOW()) +WHERE g.`name` = '默认热词组' + AND (g.`vocabulary_id` IS NULL OR g.`vocabulary_id` = ''); + +INSERT INTO `hot_word_item` +(`group_id`, `text`, `weight`, `lang`, `status`, `create_time`, `update_time`) +SELECT + g.`id`, + hw.`text`, + hw.`weight`, + hw.`lang`, + hw.`status`, + hw.`create_time`, + hw.`update_time` +FROM `hot_words` hw +JOIN `hot_word_group` g ON g.`name` = '默认热词组' +LEFT JOIN `hot_word_item` i + ON i.`group_id` = g.`id` + AND i.`text` = hw.`text` +WHERE i.`id` IS NULL; + +UPDATE `audio_model_config` a +JOIN `hot_word_group` g ON g.`name` = '默认热词组' +SET a.`hot_word_group_id` = g.`id` +WHERE a.`audio_scene` = 'asr' + AND a.`hot_word_group_id` IS NULL + AND a.`asr_vocabulary_id` IS NOT NULL + AND a.`asr_vocabulary_id` <> ''; + +-- ---------------------------------------------------------------------- +-- 6. Rebuild menu tree for the latest backend +-- ---------------------------------------------------------------------- +-- Reuse legacy root rows when possible +SET @has_dashboard := (SELECT COUNT(*) FROM `sys_menus` WHERE `menu_code` = 'dashboard'); +UPDATE `sys_menus` +SET + `menu_code` = 'dashboard', + `menu_name` = 'Dashboard', + `menu_icon` = 'DashboardOutlined', + `menu_url` = '/dashboard', + `menu_type` = 'link', + `parent_id` = NULL, + `sort_order` = 1, + `is_active` = 1, + `is_visible` = 1, + `description` = '管理员桌面' +WHERE `menu_code` = 'account_settings' + AND @has_dashboard = 0; + +SET @has_desktop := (SELECT COUNT(*) FROM `sys_menus` WHERE `menu_code` = 'desktop'); +UPDATE `sys_menus` +SET + `menu_code` = 'desktop', + `menu_name` = 'Desktop', + `menu_icon` = 'DesktopOutlined', + `menu_url` = '/dashboard', + `menu_type` = 'link', + `parent_id` = NULL, + `sort_order` = 2, + `is_active` = 1, + `is_visible` = 1, + `description` = '普通用户桌面' +WHERE `menu_code` = 'logout' + AND @has_desktop = 0; + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +VALUES +('dashboard', 'Dashboard', 'DashboardOutlined', '/dashboard', 'link', NULL, 1, NULL, 1, 1, 1, '管理员桌面') +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +VALUES +('desktop', 'Desktop', 'DesktopOutlined', '/dashboard', 'link', NULL, 1, NULL, 2, 1, 1, '普通用户桌面') +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +VALUES +('platform_admin', '平台管理', 'Shield', '/admin/management/hot-word-management', 'link', NULL, 1, NULL, 3, 1, 1, '平台管理员后台'), +('system_management', '系统管理', 'Setting', '/admin/management/user-management', 'link', NULL, 1, NULL, 4, 1, 1, '系统基础配置管理(用户、权限、字典、参数)'), +('meeting_manage', '会议管理', 'CalendarOutlined', '/meetings/center', 'link', NULL, 1, NULL, 2, 1, 1, '普通用户会议菜单') +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'prompt_management', '提示词库', 'BookText', '/prompt-management', 'link', + p.`menu_id`, 2, NULL, 3, 1, 1, '管理AI提示词模版' +FROM `sys_menus` p +WHERE p.`menu_code` = 'platform_admin' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'hot_word_management', '热词管理', 'Text', '/admin/management/hot-word-management', 'link', + p.`menu_id`, 2, NULL, 1, 1, 1, 'ASR 热词与同步' +FROM `sys_menus` p +WHERE p.`menu_code` = 'platform_admin' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'model_management', '模型管理', 'Appstore', '/admin/management/model-management', 'link', + p.`menu_id`, 2, NULL, 2, 1, 1, '音频/LLM模型配置管理' +FROM `sys_menus` p +WHERE p.`menu_code` = 'platform_admin' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'client_management', '客户端管理', 'Smartphone', '/admin/management/client-management', 'link', + p.`menu_id`, 2, NULL, 5, 1, 1, '版本、下载地址、发布状态' +FROM `sys_menus` p +WHERE p.`menu_code` = 'platform_admin' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'external_app_management', '外部应用管理', 'AppWindow', '/admin/management/external-app-management', 'link', + p.`menu_id`, 2, NULL, 6, 1, 1, '外部系统入口与图标配置' +FROM `sys_menus` p +WHERE p.`menu_code` = 'platform_admin' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'terminal_management', '终端管理', 'Monitor', '/admin/management/terminal-management', 'link', + p.`menu_id`, 2, NULL, 7, 1, 1, '专用设备、激活和绑定状态' +FROM `sys_menus` p +WHERE p.`menu_code` = 'platform_admin' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'user_management', '用户管理', 'Users', '/admin/management/user-management', 'link', + p.`menu_id`, 2, NULL, 1, 1, 1, '账号、角色、密码重置' +FROM `sys_menus` p +WHERE p.`menu_code` = 'system_management' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'permission_management', '权限管理', 'KeyRound', '/admin/management/permission-management', 'link', + p.`menu_id`, 2, NULL, 2, 1, 1, '菜单与角色授权矩阵' +FROM `sys_menus` p +WHERE p.`menu_code` = 'system_management' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'dict_management', '字典管理', 'BookMarked', '/admin/management/dict-management', 'link', + p.`menu_id`, 2, NULL, 3, 1, 1, '码表、平台类型、扩展属性' +FROM `sys_menus` p +WHERE p.`menu_code` = 'system_management' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'parameter_management', '参数管理', 'Setting', '/admin/management/parameter-management', 'link', + p.`menu_id`, 2, NULL, 8, 1, 1, '系统参数管理' +FROM `sys_menus` p +WHERE p.`menu_code` = 'system_management' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'permission_menu_tree', '菜单树维护', 'AppstoreAdd', '/admin/management/permission-management', 'link', + p.`menu_id`, 3, NULL, 20, 1, 0, '权限管理中的菜单树维护入口(隐藏于侧栏)' +FROM `sys_menus` p +WHERE p.`menu_code` = 'permission_management' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'meeting_center', '会议中心', 'CalendarOutlined', '/meetings/center', 'link', + p.`menu_id`, 2, NULL, 1, 1, 1, '普通用户会议中心' +FROM `sys_menus` p +WHERE p.`menu_code` = 'meeting_manage' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'prompt_config', '提示词配置', 'Book', '/prompt-config', 'link', + p.`menu_id`, 2, NULL, 2, 1, 1, '用户可配置启用提示词与排序' +FROM `sys_menus` p +WHERE p.`menu_code` = 'meeting_manage' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +INSERT INTO `sys_menus` +(`menu_code`, `menu_name`, `menu_icon`, `menu_url`, `menu_type`, `parent_id`, `menu_level`, `tree_path`, `sort_order`, `is_active`, `is_visible`, `description`) +SELECT + 'personal_prompt_library', '个人提示词仓库', 'ReadOutlined', '/personal-prompts', 'link', + p.`menu_id`, 2, NULL, 3, 0, 1, '普通用户个人提示词仓库' +FROM `sys_menus` p +WHERE p.`menu_code` = 'meeting_manage' +ON DUPLICATE KEY UPDATE + `menu_name` = VALUES(`menu_name`), + `menu_icon` = VALUES(`menu_icon`), + `menu_url` = VALUES(`menu_url`), + `menu_type` = VALUES(`menu_type`), + `parent_id` = VALUES(`parent_id`), + `sort_order` = VALUES(`sort_order`), + `is_active` = VALUES(`is_active`), + `is_visible` = VALUES(`is_visible`), + `description` = VALUES(`description`); + +-- Menu tree metadata (supports current depth 3) +UPDATE `sys_menus` +SET `menu_level` = 1, + `tree_path` = CONCAT('/', `menu_id`) +WHERE `parent_id` IS NULL; + +UPDATE `sys_menus` c +JOIN `sys_menus` p ON c.`parent_id` = p.`menu_id` +SET c.`menu_level` = p.`menu_level` + 1, + c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`); + +UPDATE `sys_menus` c +JOIN `sys_menus` p ON c.`parent_id` = p.`menu_id` +SET c.`menu_level` = p.`menu_level` + 1, + c.`tree_path` = CONCAT(p.`tree_path`, '/', c.`menu_id`); + +-- ---------------------------------------------------------------------- +-- 7. Align role-menu permissions for latest menu model +-- ---------------------------------------------------------------------- +-- Admin gets all active menus +INSERT IGNORE INTO `sys_role_menu_permissions` (`role_id`, `menu_id`, `granted_by`, `granted_at`) +SELECT 1, m.`menu_id`, 1, NOW() +FROM `sys_menus` m +WHERE m.`is_active` = 1; + +-- Normal user only gets desktop + meeting menus + prompt config +DELETE p +FROM `sys_role_menu_permissions` p +JOIN `sys_menus` m ON m.`menu_id` = p.`menu_id` +WHERE p.`role_id` = 2 + AND m.`menu_code` NOT IN ('desktop', 'meeting_manage', 'meeting_center', 'prompt_config'); + +INSERT IGNORE INTO `sys_role_menu_permissions` (`role_id`, `menu_id`, `granted_by`, `granted_at`) +SELECT 2, m.`menu_id`, 1, NOW() +FROM `sys_menus` m +WHERE m.`menu_code` IN ('desktop', 'meeting_manage', 'meeting_center', 'prompt_config') + AND m.`is_active` = 1; + +-- ---------------------------------------------------------------------- +-- 8. Recreate compatibility views +-- ---------------------------------------------------------------------- +DROP VIEW IF EXISTS `users`; +DROP VIEW IF EXISTS `roles`; +DROP VIEW IF EXISTS `menus`; +DROP VIEW IF EXISTS `role_menu_permissions`; +DROP VIEW IF EXISTS `dict_data`; +DROP VIEW IF EXISTS `system_parameters`; + +CREATE VIEW `users` AS SELECT * FROM `sys_users`; +CREATE VIEW `roles` AS SELECT * FROM `sys_roles`; +CREATE VIEW `menus` AS SELECT * FROM `sys_menus`; +CREATE VIEW `role_menu_permissions` AS SELECT * FROM `sys_role_menu_permissions`; +CREATE VIEW `dict_data` AS SELECT * FROM `sys_dict_data`; +CREATE VIEW `system_parameters` AS SELECT * FROM `sys_system_parameters`; diff --git a/docs/imeeting_qy_upgrade_report_20260403.md b/docs/imeeting_qy_upgrade_report_20260403.md new file mode 100644 index 0000000..fa11083 --- /dev/null +++ b/docs/imeeting_qy_upgrade_report_20260403.md @@ -0,0 +1,129 @@ +# iMeeting `imeeting_qy` 数据库升级报告 + +## 1. 升级目标 + +- 源旧库:`imeeting_qy` +- 对标最新结构库:`imeeting` +- 升级日期:`2026-04-03` +- 执行方式:先在临时测试库 `imeeting_qy_upgrade_test` 演练,再正式执行到 `imeeting_qy` + +## 2. 备份信息 + +- 正式升级前已创建完整备份库:`imeeting_qy_backup_20260403_004354` +- 备份方式:同服务器整库复制全部 `BASE TABLE` + +## 3. 本次执行内容 + +本次升级使用迁移脚本: + +- `backend/sql/migrations/upgrade_imeeting_qy_to_latest.sql` + +核心动作如下: + +1. 将旧系统表标准化为 `sys_*` 体系: + - `users -> sys_users` + - `roles -> sys_roles` + - `menus -> sys_menus` + - `role_menu_permissions -> sys_role_menu_permissions` + - `dict_data -> sys_dict_data` + +2. 重建兼容视图: + - `users` + - `roles` + - `menus` + - `role_menu_permissions` + - `dict_data` + - `system_parameters` + +3. 对齐旧系统表字段与索引: + - `sys_users` 补齐 `idx_role_id` + - `sys_roles` 补齐 `uk_role_name` + - `sys_menus` 补齐 `menu_level/tree_path/is_visible` 及相关索引 + - `sys_role_menu_permissions` 补齐 `granted_by/granted_at` 及相关索引 + - `prompts` 补齐 `is_system` 字段及组合索引 + - `terminals.current_user_id` 字段注释对齐到最新结构 + +4. 新增并初始化最新配置表: + - `sys_system_parameters` + - `ai_model_configs` + - `llm_model_config` + - `audio_model_config` + - `hot_word_group` + - `hot_word_item` + - `prompt_config` + - `sys_user_mcp` + +5. 迁移旧配置数据: + - 从 `sys_dict_data(dict_type='system_config')` 迁移系统参数到 `sys_system_parameters` + - 迁移 LLM / ASR / 声纹配置到 `ai_model_configs` + - 拆分生成 `llm_model_config`、`audio_model_config` + - 从旧 `hot_words` 迁移到 `hot_word_group` / `hot_word_item` + +6. 重建最新菜单树与角色授权模型: + - 新增 `dashboard`、`desktop`、`meeting_manage`、`system_management` 等最新菜单结构 + - 规范平台管理、系统管理、会议管理三套菜单层级 + - 管理员角色授予全部启用菜单 + - 普通用户保留 `desktop/meeting_manage/meeting_center/prompt_config` + +## 4. 升级结果 + +升级后关键表数据如下: + +| 表名 | 行数 | +|---|---:| +| `sys_users` | 44 | +| `sys_roles` | 2 | +| `sys_menus` | 19 | +| `sys_role_menu_permissions` | 22 | +| `sys_system_parameters` | 4 | +| `ai_model_configs` | 3 | +| `llm_model_config` | 1 | +| `audio_model_config` | 2 | +| `hot_word_group` | 1 | +| `hot_word_item` | 20 | +| `prompt_config` | 0 | +| `sys_user_mcp` | 0 | + +迁移后的系统参数: + +| 参数键 | 参数值 | +|---|---| +| `asr_vocabulary_id` | `vocab-imeeting-734e93f5bd8a4f3bb665dd526d584516` | +| `default_reset_password` | `123456` | +| `max_audio_size` | `500` | +| `timeline_pagesize` | `20` | + +迁移后的模型配置: + +- `llm_model_config`:1 条默认模型,`model_code=llm_model` +- `audio_model_config`:2 条配置 + - `audio_model` / `asr` + - `voiceprint_model` / `voiceprint` + +迁移后的热词配置: + +- `hot_word_group`:1 个默认热词组 +- `hot_word_item`:20 条热词条目 + +## 5. 角色菜单结果 + +- 平台管理员: + - `dashboard, hot_word_management, user_management, meeting_center, desktop, meeting_manage, model_management, permission_management, prompt_config, prompt_management, platform_admin, dict_management, system_management, client_management, external_app_management, terminal_management, parameter_management, permission_menu_tree` + +- 普通用户: + - `meeting_center, desktop, meeting_manage, prompt_config` + +## 6. 结构校验结论 + +对 `imeeting_qy` 与 `imeeting` 进行了 `information_schema.tables` + `information_schema.columns` 级别的最终校验,结果如下: + +- 缺失表:`0` +- 多余表:`0` +- 表类型差异:`0` +- 字段差异:`0` + +结论: + +- `imeeting_qy` 已完成升级 +- 当前库结构已与 `imeeting` 对齐 +- 本次升级为“结构对齐 + 必要配置数据迁移”,未删除旧业务数据