diff --git a/docs/refactor-v3/schema-map.md b/docs/refactor-v3/schema-map.md index f532060..ef0d44a 100644 --- a/docs/refactor-v3/schema-map.md +++ b/docs/refactor-v3/schema-map.md @@ -12,12 +12,12 @@ | Domain | Tables | Owner Batch | Notes | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Login, menu, role, permission, department. | -| Platform Config | `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Dict and component template split from legacy Admin misc. | -| Blog Content | `blog_post`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_import_job` | Batch 3 | Categories and tags use relation table. | +| Admin Identity | `admin_user`, `admin_role`, `admin_permission`, `admin_menu`, `admin_dept`, `admin_department`, `admin_user_role`, `admin_role_permission`, `admin_role_menu` | Batch 2 | Runtime login/menu/role/dept tables plus future permission and department boundary tables. | +| Platform Config | `admin_dict`, `admin_component`, `platform_dict_group`, `platform_dict_item`, `platform_component_template`, `platform_setting` | Batch 2 | Runtime Admin dict/component compatibility plus new platform dict/component template split. | +| Blog Content | `blog_post`, `blog_article`, `blog_taxonomy`, `blog_term`, `blog_post_term`, `blog_theme_profile`, `blog_theme_config`, `blog_import_job` | Batch 3 | `blog_article`/`blog_theme_config` keep current runtime compatibility; `blog_term` carries both runtime category/tag fields and nullable taxonomy planning fields. | | WordPress Mirror | `wordpress_site`, `wordpress_auth_session`, `wordpress_remote_post`, `wordpress_remote_term`, `wordpress_sync_job`, `wordpress_sync_mapping` | Batch 3 | Remote state separate from local Blog content. | | Asset | `asset_bucket`, `asset_object`, `asset_reference`, `asset_access_grant` | Batch 3 | MinIO object ownership and access grant. | -| System Event | `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | MySQL stores actionable events; Loki remains log query source. | +| System Event | `admin_notice`, `system_notice`, `system_event`, `system_event_dedupe`, `system_event_delivery` | Batch 2 | Admin notices and MySQL actionable events; Loki remains log query source. | | Runtime Evidence | `runtime_evidence_index` | Batch 1 | Safe index only, no large logs or secrets. | | QQBot Core | `qqbot_account`, `qqbot_account_ability`, `qqbot_connection_session`, `qqbot_capability_binding`, `qqbot_permission_policy`, `qqbot_allowlist`, `qqbot_blocklist`, `qqbot_command`, `qqbot_command_alias`, `qqbot_command_log`, `qqbot_config`, `qqbot_rule`, `qqbot_conversation`, `qqbot_message`, `qqbot_send_task`, `qqbot_send_log`, `qqbot_dedupe_event`, `qqbot_dedupe`, `qqbot_account_napcat`, `qqbot_napcat_container` | Batch 4 | Account, connection, permission, command, message, send queue, and transitional registered-entity compatibility tables until Batches 5-7 replace plugin/NapCat internals. | | NapCat Runtime | `napcat_container`, `napcat_device_identity`, `napcat_account_binding`, `napcat_login_session`, `napcat_login_challenge`, `napcat_runtime_cleanup` | Batch 7 | Device identity and login challenge state. | diff --git a/scripts/refactor-v3/db-dry-run.ps1 b/scripts/refactor-v3/db-dry-run.ps1 index 2a5e9ec..2418875 100644 --- a/scripts/refactor-v3/db-dry-run.ps1 +++ b/scripts/refactor-v3/db-dry-run.ps1 @@ -15,16 +15,27 @@ $schema = Join-Path $root "sql\refactor-v3\00-full-schema.sql" $seed = Join-Path $root "sql\refactor-v3\01-seed-core.sql" $verify = Join-Path $root "sql\refactor-v3\99-verify.sql" +function Invoke-Mysql { + param( + [Parameter(Mandatory = $true)][string[]]$Arguments + ) + + mysql --default-character-set=utf8mb4 @Arguments + if ($LASTEXITCODE -ne 0) { + throw "mysql failed with exit code $LASTEXITCODE" + } +} + function Invoke-MysqlSource { param( [Parameter(Mandatory = $true)][string]$Path ) $sourcePath = (Resolve-Path -LiteralPath $Path).Path.Replace("\", "/") - mysql -h $HostName -P $Port -u $User $Database --execute="source $sourcePath" + Invoke-Mysql -Arguments @("-h", $HostName, "-P", "$Port", "-u", $User, $Database, "--execute=source $sourcePath") } -mysql -h $HostName -P $Port -u $User -e "DROP DATABASE IF EXISTS ``$Database``; CREATE DATABASE ``$Database`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" +Invoke-Mysql -Arguments @("-h", $HostName, "-P", "$Port", "-u", $User, "-e", "DROP DATABASE IF EXISTS ``$Database``; CREATE DATABASE ``$Database`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;") Invoke-MysqlSource -Path $schema Invoke-MysqlSource -Path $seed Invoke-MysqlSource -Path $verify diff --git a/scripts/refactor-v3/local-smoke.ps1 b/scripts/refactor-v3/local-smoke.ps1 index d4f14c9..c045d0f 100644 --- a/scripts/refactor-v3/local-smoke.ps1 +++ b/scripts/refactor-v3/local-smoke.ps1 @@ -1,5 +1,5 @@ param( - [string]$BaseUrl = "http://127.0.0.1:5320" + [string]$BaseUrl = "http://127.0.0.1:48085" ) $ErrorActionPreference = "Stop" diff --git a/sql/refactor-v3/00-full-schema.sql b/sql/refactor-v3/00-full-schema.sql index 82afacc..e705d24 100644 --- a/sql/refactor-v3/00-full-schema.sql +++ b/sql/refactor-v3/00-full-schema.sql @@ -1,22 +1,147 @@ -CREATE TABLE IF NOT EXISTS admin_user ( +CREATE TABLE IF NOT EXISTS admin_menu ( id BIGINT NOT NULL PRIMARY KEY, - username VARCHAR(64) NOT NULL, - password_hash VARCHAR(255) NOT NULL, - nickname VARCHAR(64) NOT NULL, - status VARCHAR(32) NOT NULL, - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY uk_admin_user_username (username) + pid BIGINT NOT NULL DEFAULT 0, + name VARCHAR(120) NOT NULL, + path VARCHAR(255) NULL, + component VARCHAR(255) NULL, + redirect VARCHAR(255) NULL, + auth_code VARCHAR(120) NULL, + type VARCHAR(32) NOT NULL DEFAULT 'menu', + meta LONGTEXT NULL, + status INT NOT NULL DEFAULT 1, + sort INT NOT NULL DEFAULT 0, + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + UNIQUE KEY uk_admin_menu_name (name), + KEY idx_admin_menu_pid (pid), + KEY idx_admin_menu_path (path), + KEY idx_admin_menu_auth_code (auth_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS admin_role ( id BIGINT NOT NULL PRIMARY KEY, - role_key VARCHAR(64) NOT NULL, - role_name VARCHAR(64) NOT NULL, - status VARCHAR(32) NOT NULL, - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY uk_admin_role_key (role_key) + role_code VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + remark VARCHAR(255) NOT NULL DEFAULT '', + status INT NOT NULL DEFAULT 1, + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + UNIQUE KEY uk_admin_role_code (role_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_user ( + id BIGINT NOT NULL PRIMARY KEY, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + real_name VARCHAR(255) NOT NULL, + avatar VARCHAR(1024) NOT NULL DEFAULT '', + dept_id BIGINT NULL, + home_path VARCHAR(255) NOT NULL DEFAULT '', + timezone VARCHAR(255) NOT NULL DEFAULT 'Asia/Shanghai', + status INT NOT NULL DEFAULT 1, + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + UNIQUE KEY uk_admin_user_username (username), + KEY idx_admin_user_dept_id (dept_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_dept ( + id BIGINT NOT NULL PRIMARY KEY, + pid BIGINT NOT NULL DEFAULT 0, + name VARCHAR(255) NOT NULL, + status INT NOT NULL DEFAULT 1, + remark VARCHAR(255) NOT NULL DEFAULT '', + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + KEY idx_admin_dept_pid (pid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_dict ( + id BIGINT NOT NULL PRIMARY KEY, + dict_code VARCHAR(255) NOT NULL, + label VARCHAR(255) NOT NULL, + value VARCHAR(255) NOT NULL, + children_code VARCHAR(255) NULL, + sort INT NOT NULL DEFAULT 0, + status INT NOT NULL DEFAULT 1, + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + UNIQUE KEY uk_admin_dict_code_value (dict_code, value), + KEY idx_admin_dict_code (dict_code), + KEY idx_admin_dict_children_code (children_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_component ( + id BIGINT NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL DEFAULT '', + type INT NOT NULL, + component_type INT NOT NULL, + image MEDIUMTEXT NOT NULL, + template MEDIUMTEXT NOT NULL, + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + KEY idx_admin_component_type (type), + KEY idx_admin_component_component_type (component_type), + KEY idx_admin_component_deleted (is_deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_notice ( + id BIGINT NOT NULL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content LONGTEXT NOT NULL, + summary TEXT NULL, + level INT NOT NULL DEFAULT 1, + status INT NOT NULL DEFAULT 1, + severity VARCHAR(16) NOT NULL DEFAULT 'info', + source VARCHAR(64) NOT NULL DEFAULT 'system', + event_type VARCHAR(120) NOT NULL DEFAULT 'system.event', + dedupe_key VARCHAR(255) NULL, + occurrence_count INT NOT NULL DEFAULT 1, + notify_role_code VARCHAR(64) NOT NULL DEFAULT 'super', + metadata JSON NULL, + is_top TINYINT NOT NULL DEFAULT 0, + notify_users TEXT NULL, + created_by BIGINT NULL, + is_deleted TINYINT NOT NULL DEFAULT 0, + active_dedupe_key VARCHAR(255) GENERATED ALWAYS AS (CASE WHEN is_deleted = 0 AND dedupe_key IS NOT NULL THEN dedupe_key ELSE NULL END) VIRTUAL, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + first_seen_at DATETIME NULL, + last_seen_at DATETIME NULL, + UNIQUE KEY uk_admin_notice_active_dedupe_key (active_dedupe_key), + KEY idx_admin_notice_status (status), + KEY idx_admin_notice_severity (severity), + KEY idx_admin_notice_source_event (source, event_type), + KEY idx_admin_notice_dedupe_key (dedupe_key), + KEY idx_admin_notice_notify_role (notify_role_code), + KEY idx_admin_notice_is_top (is_top), + KEY idx_admin_notice_is_deleted (is_deleted), + KEY idx_admin_notice_create_time (create_time), + KEY idx_admin_notice_last_seen (last_seen_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_user_role ( + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + PRIMARY KEY (user_id, role_id), + KEY idx_admin_user_role_role_id (role_id), + CONSTRAINT fk_admin_user_role_user FOREIGN KEY (user_id) REFERENCES admin_user (id) ON DELETE CASCADE, + CONSTRAINT fk_admin_user_role_role FOREIGN KEY (role_id) REFERENCES admin_role (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS admin_role_menu ( + role_id BIGINT NOT NULL, + menu_id BIGINT NOT NULL, + PRIMARY KEY (role_id, menu_id), + KEY idx_admin_role_menu_menu_id (menu_id), + CONSTRAINT fk_admin_role_menu_role FOREIGN KEY (role_id) REFERENCES admin_role (id) ON DELETE CASCADE, + CONSTRAINT fk_admin_role_menu_menu FOREIGN KEY (menu_id) REFERENCES admin_menu (id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS admin_permission ( @@ -29,21 +154,6 @@ CREATE TABLE IF NOT EXISTS admin_permission ( UNIQUE KEY uk_admin_permission_key (permission_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS admin_menu ( - id BIGINT NOT NULL PRIMARY KEY, - parent_id BIGINT NULL, - menu_key VARCHAR(128) NOT NULL, - title VARCHAR(128) NOT NULL, - path VARCHAR(255) NOT NULL, - component VARCHAR(255) NULL, - sort_no INT NOT NULL DEFAULT 0, - status VARCHAR(32) NOT NULL, - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY uk_admin_menu_key (menu_key), - KEY idx_admin_menu_parent (parent_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - CREATE TABLE IF NOT EXISTS admin_department ( id BIGINT NOT NULL PRIMARY KEY, parent_id BIGINT NULL, @@ -55,15 +165,6 @@ CREATE TABLE IF NOT EXISTS admin_department ( KEY idx_admin_department_parent (parent_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS admin_user_role ( - id BIGINT NOT NULL PRIMARY KEY, - user_id BIGINT NOT NULL, - role_id BIGINT NOT NULL, - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uk_admin_user_role (user_id, role_id), - KEY idx_admin_user_role_role (role_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - CREATE TABLE IF NOT EXISTS admin_role_permission ( id BIGINT NOT NULL PRIMARY KEY, role_id BIGINT NOT NULL, @@ -73,15 +174,6 @@ CREATE TABLE IF NOT EXISTS admin_role_permission ( KEY idx_admin_role_permission_permission (permission_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS admin_role_menu ( - id BIGINT NOT NULL PRIMARY KEY, - role_id BIGINT NOT NULL, - menu_id BIGINT NOT NULL, - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uk_admin_role_menu (role_id, menu_id), - KEY idx_admin_role_menu_menu (menu_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - CREATE TABLE IF NOT EXISTS platform_dict_group ( id BIGINT NOT NULL PRIMARY KEY, group_key VARCHAR(128) NOT NULL, @@ -142,6 +234,30 @@ CREATE TABLE IF NOT EXISTS blog_post ( KEY idx_blog_post_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS blog_article ( + id BIGINT NOT NULL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL DEFAULT 'draft', + excerpt TEXT NULL, + content_markdown MEDIUMTEXT NULL, + content_html MEDIUMTEXT NULL, + cover TEXT NULL, + author_name VARCHAR(255) NOT NULL DEFAULT 'KwiTsukasa', + category_items TEXT NULL, + tag_items TEXT NULL, + views INT NOT NULL DEFAULT 0, + comments INT NOT NULL DEFAULT 0, + is_deleted TINYINT NOT NULL DEFAULT 0, + publish_time DATETIME NULL, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + KEY idx_blog_article_slug (slug), + KEY idx_blog_article_status (status), + KEY idx_blog_article_publish_time (publish_time), + KEY idx_blog_article_is_deleted (is_deleted) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS blog_taxonomy ( id BIGINT NOT NULL PRIMARY KEY, taxonomy_key VARCHAR(64) NOT NULL, @@ -153,13 +269,20 @@ CREATE TABLE IF NOT EXISTS blog_taxonomy ( CREATE TABLE IF NOT EXISTS blog_term ( id BIGINT NOT NULL PRIMARY KEY, - taxonomy_id BIGINT NOT NULL, + taxonomy_id BIGINT NULL, + kind VARCHAR(32) NOT NULL, + name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, - term_name VARCHAR(128) NOT NULL, - create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY uk_blog_term_slug (taxonomy_id, slug), - KEY idx_blog_term_taxonomy (taxonomy_id) + term_name VARCHAR(128) NULL, + description TEXT NULL, + parent_id VARCHAR(64) NULL, + is_deleted TINYINT NOT NULL DEFAULT 0, + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + KEY idx_blog_term_taxonomy (taxonomy_id), + KEY idx_blog_term_kind_slug (kind, slug), + KEY idx_blog_term_parent_id (parent_id), + KEY idx_blog_term_is_deleted (is_deleted) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS blog_post_term ( @@ -182,6 +305,14 @@ CREATE TABLE IF NOT EXISTS blog_theme_profile ( UNIQUE KEY uk_blog_theme_profile_key (profile_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS blog_theme_config ( + id VARCHAR(64) NOT NULL PRIMARY KEY, + config LONGTEXT NOT NULL, + source VARCHAR(255) NOT NULL DEFAULT 'local', + create_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + update_time DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + CREATE TABLE IF NOT EXISTS blog_import_job ( id BIGINT NOT NULL PRIMARY KEY, source_key VARCHAR(64) NOT NULL, diff --git a/sql/refactor-v3/01-seed-core.sql b/sql/refactor-v3/01-seed-core.sql index af057fc..cf9cc7e 100644 --- a/sql/refactor-v3/01-seed-core.sql +++ b/sql/refactor-v3/01-seed-core.sql @@ -1,67 +1,214 @@ +INSERT INTO admin_role ( + id, + role_code, + name, + remark, + status +) VALUES ( + 2041700000000010001, + 'super', + '超级管理员', + '拥有所有后台权限', + 1 +) ON DUPLICATE KEY UPDATE + name = VALUES(name), + remark = VALUES(remark), + status = VALUES(status), + is_deleted = 0; + +INSERT INTO admin_dept ( + id, + pid, + name, + status, + remark +) VALUES ( + 2041700000000200001, + 0, + 'KT 总部', + 1, + '根部门' +) ON DUPLICATE KEY UPDATE + pid = VALUES(pid), + name = VALUES(name), + status = VALUES(status), + remark = VALUES(remark), + is_deleted = 0; + INSERT INTO admin_user ( id, username, - password_hash, - nickname, + password, + real_name, + avatar, + dept_id, + home_path, + timezone, status ) VALUES ( - 1000000000000000001, + 2041700000000000002, 'admin', - '!disabled-refactor-v3-password-hash!', - 'Administrator', - 'password_unset' + '123456', + 'Admin', + '', + 2041700000000200001, + '/workspace', + 'Asia/Shanghai', + 1 ) ON DUPLICATE KEY UPDATE - nickname = VALUES(nickname), - status = VALUES(status); - -INSERT INTO admin_role ( - id, - role_key, - role_name, - status -) VALUES ( - 1000000000000000002, - 'super_admin', - 'Super Administrator', - 'enabled' -) ON DUPLICATE KEY UPDATE - role_name = VALUES(role_name), - status = VALUES(status); - -INSERT INTO admin_user_role ( - id, - user_id, - role_id -) VALUES ( - 1000000000000000003, - 1000000000000000001, - 1000000000000000002 -) ON DUPLICATE KEY UPDATE - role_id = VALUES(role_id); + password = VALUES(password), + real_name = VALUES(real_name), + avatar = VALUES(avatar), + dept_id = VALUES(dept_id), + home_path = VALUES(home_path), + timezone = VALUES(timezone), + status = VALUES(status), + is_deleted = 0; INSERT INTO admin_menu ( id, - parent_id, - menu_key, - title, + pid, + name, path, component, - sort_no, - status -) VALUES ( - 1000000000000000004, - NULL, - 'dashboard', - 'Dashboard', - '/dashboard', - 'dashboard/workspace/index', - 10, - 'enabled' -) ON DUPLICATE KEY UPDATE - title = VALUES(title), + redirect, + auth_code, + type, + meta, + status, + sort +) VALUES + ( + 2041700000000100001, + 0, + 'Dashboard', + '/dashboard', + NULL, + '/workspace', + NULL, + 'catalog', + '{"order":-1,"title":"page.dashboard.title"}', + 1, + 0 + ), + ( + 2041700000000100102, + 2041700000000100001, + 'Workspace', + '/workspace', + '/dashboard/workspace/index', + NULL, + NULL, + 'menu', + '{"icon":"carbon:workspace","title":"page.dashboard.workspace"}', + 1, + 0 + ), + ( + 2041700000000100400, + 0, + 'QqBot', + '/qqbot', + NULL, + '/qqbot/dashboard', + NULL, + 'catalog', + '{"icon":"lucide:bot","order":110,"title":"QQBot 管理"}', + 1, + 110 + ), + ( + 2041700000000100402, + 2041700000000100400, + 'QqBotAccount', + '/qqbot/account', + '/qqbot/account/list', + NULL, + 'QqBot:Account:List', + 'menu', + '{"icon":"lucide:radio-receiver","title":"账号连接"}', + 1, + 1 + ), + ( + 2041700000000100408, + 2041700000000100400, + 'QqBotCommand', + '/qqbot/command', + '/qqbot/command/list', + NULL, + 'QqBot:Command:List', + 'menu', + '{"icon":"lucide:square-terminal","title":"在线命令"}', + 1, + 3 + ), + ( + 2041700000000100409, + 2041700000000100400, + 'QqBotPlugin', + '/qqbot/plugin', + '/qqbot/plugin/list', + NULL, + 'QqBot:Plugin:List', + 'menu', + '{"icon":"lucide:plug","title":"插件能力"}', + 1, + 4 + ) +ON DUPLICATE KEY UPDATE + pid = VALUES(pid), path = VALUES(path), component = VALUES(component), - status = VALUES(status); + redirect = VALUES(redirect), + auth_code = VALUES(auth_code), + type = VALUES(type), + meta = VALUES(meta), + status = VALUES(status), + sort = VALUES(sort), + is_deleted = 0; + +INSERT INTO admin_dict ( + id, + dict_code, + label, + value, + children_code, + sort, + status +) VALUES + ( + 2041700000000300001, + 'COMPONENT_TYPE', + '图表', + '1', + 'CHART', + 1, + 1 + ), + ( + 2041700000000300101, + 'CHART', + '未分类', + '-1', + NULL, + 0, + 1 + ) +ON DUPLICATE KEY UPDATE + label = VALUES(label), + children_code = VALUES(children_code), + sort = VALUES(sort), + status = VALUES(status), + is_deleted = 0; + +INSERT IGNORE INTO admin_user_role (user_id, role_id) +VALUES (2041700000000000002, 2041700000000010001); + +INSERT IGNORE INTO admin_role_menu (role_id, menu_id) +SELECT 2041700000000010001, id +FROM admin_menu +WHERE is_deleted = 0; INSERT INTO platform_setting ( id, @@ -121,6 +268,9 @@ INSERT INTO qqbot_command ( id, operation_key, command_key, + code, + name, + aliases, plugin_key, enabled, cooldown_seconds @@ -128,11 +278,17 @@ INSERT INTO qqbot_command ( 1000000000000000201, 'bangdream.song.search', 'bangdream_song', + 'bd', + 'BangDream 查歌', + '["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]', 'bangdream', 1, 2 ) ON DUPLICATE KEY UPDATE operation_key = VALUES(operation_key), + code = VALUES(code), + name = VALUES(name), + aliases = VALUES(aliases), plugin_key = VALUES(plugin_key), enabled = VALUES(enabled), cooldown_seconds = VALUES(cooldown_seconds); diff --git a/sql/refactor-v3/99-verify.sql b/sql/refactor-v3/99-verify.sql index 32c6708..d76eab5 100644 --- a/sql/refactor-v3/99-verify.sql +++ b/sql/refactor-v3/99-verify.sql @@ -2,6 +2,7 @@ SELECT 'admin_user' AS table_name, COUNT(*) AS row_count FROM admin_user; SELECT 'admin_role' AS table_name, COUNT(*) AS row_count FROM admin_role; SELECT 'admin_menu' AS table_name, COUNT(*) AS row_count FROM admin_menu; SELECT 'platform_setting' AS table_name, COUNT(*) AS row_count FROM platform_setting; +SELECT 'admin_dict' AS table_name, COUNT(*) AS row_count FROM admin_dict; SELECT 'qqbot_command' AS table_name, COUNT(*) AS row_count FROM qqbot_command; SELECT 'qqbot_plugin' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin; SELECT 'napcat_device_identity' AS table_name, COUNT(*) AS row_count FROM napcat_device_identity; @@ -9,7 +10,9 @@ SELECT 'napcat_device_identity' AS table_name, COUNT(*) AS row_count FROM napcat SELECT 'seed_admin_user' AS check_name, COUNT(*) AS matched_rows FROM admin_user WHERE username = 'admin' - AND status = 'password_unset'; + AND password <> '' + AND status = 1 + AND is_deleted = 0; SELECT 'seed_platform_schema_version' AS check_name, COUNT(*) AS matched_rows FROM platform_setting @@ -40,6 +43,12 @@ WHERE table_schema = DATABASE() AND table_name = 'platform_setting' AND index_name = 'uk_platform_setting_key'; +SELECT 'index_admin_dict_code_value' AS check_name, COUNT(*) AS matched_rows +FROM information_schema.statistics +WHERE table_schema = DATABASE() + AND table_name = 'admin_dict' + AND index_name = 'uk_admin_dict_code_value'; + SELECT 'index_qqbot_command_key' AS check_name, COUNT(*) AS matched_rows FROM information_schema.statistics WHERE table_schema = DATABASE() diff --git a/src/app.module.ts b/src/app.module.ts index 9136b79..2442a8e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -44,7 +44,10 @@ import { RuntimeModule } from './runtime'; password: configService.get('DB_PASSWORD'), database: configService.get('DB_DATABASE'), synchronize: configService.get('DB_SYNC') === 'true', - entities: [__dirname + '/**/*.entity{.ts,.js}'], + entities: [ + __dirname + '/**/*.entity{.ts,.js}', + __dirname + '/**/*.entities{.ts,.js}', + ], subscribers: [__dirname + '/**/*.subscriber{.ts,.js}'], }; }, diff --git a/test/modules/qqbot/plugin-platform/plugin-platform-api-contract.spec.ts b/test/modules/qqbot/plugin-platform/plugin-platform-api-contract.spec.ts index f4ca9e2..ea2ab0e 100644 --- a/test/modules/qqbot/plugin-platform/plugin-platform-api-contract.spec.ts +++ b/test/modules/qqbot/plugin-platform/plugin-platform-api-contract.spec.ts @@ -105,6 +105,15 @@ describe('QQBot plugin platform API contract', () => { expect(source).toContain('QqbotPluginPlatformModule'); }); + it('loads plural entity files into the TypeORM datasource', () => { + const source = readFileSync( + join(__dirname, '../../../../src/app.module.ts'), + 'utf8', + ); + + expect(source).toContain('*.entities{.ts,.js}'); + }); + it('exposes plugin-platform management routes', () => { expect(routeKey).toBeDefined(); expect( diff --git a/test/refactor-v3/schema-map.spec.ts b/test/refactor-v3/schema-map.spec.ts index e6b3e88..cf57ddd 100644 --- a/test/refactor-v3/schema-map.spec.ts +++ b/test/refactor-v3/schema-map.spec.ts @@ -53,4 +53,78 @@ describe('refactor v3 schema skeleton', () => { expect(verify).toContain('qqbot_plugin'); expect(verify).toContain('napcat_device_identity'); }); + + it('declares bootstrap tables required before the API can finish startup', () => { + const sql = readFileSync( + join(root, 'sql/refactor-v3/00-full-schema.sql'), + 'utf8', + ); + const verify = readFileSync( + join(root, 'sql/refactor-v3/99-verify.sql'), + 'utf8', + ); + + expect(sql).toContain('CREATE TABLE IF NOT EXISTS admin_dict'); + expect(verify).toContain('admin_dict'); + }); + + it('declares blog tables compatible with current runtime entities', () => { + const sql = readFileSync( + join(root, 'sql/refactor-v3/00-full-schema.sql'), + 'utf8', + ); + const blogTermColumns = sql.match( + /CREATE TABLE IF NOT EXISTS blog_term \(([\s\S]*?)\) ENGINE=/, + )?.[1]; + + expect(sql).toContain('CREATE TABLE IF NOT EXISTS blog_article'); + expect(sql).toContain('CREATE TABLE IF NOT EXISTS blog_theme_config'); + expect(sql).toContain('content_markdown'); + expect(sql).toContain('content_html'); + expect(sql).toContain('category_items'); + expect(sql).toContain('tag_items'); + expect(sql).toContain('author_name'); + expect(sql).toContain('publish_time'); + expect(sql).toContain('is_deleted'); + expect(blogTermColumns).toContain('kind'); + expect(blogTermColumns).toContain('name'); + expect(blogTermColumns).toContain('description'); + expect(blogTermColumns).toContain('parent_id'); + }); + + it('seeds qqbot command rows with required command code and aliases', () => { + const seed = readFileSync( + join(root, 'sql/refactor-v3/01-seed-core.sql'), + 'utf8', + ); + const commandColumns = seed.match( + /INSERT INTO qqbot_command \(([\s\S]*?)\) VALUES/, + )?.[1]; + + expect(commandColumns).toContain('code'); + expect(commandColumns).toContain('aliases'); + expect(seed).toContain('["查曲","bd","bangdream","bandori","邦邦","邦邦查歌"]'); + }); + + it('runs dry-run SQL sources with utf8mb4 and fails on native mysql errors', () => { + const dryRunScript = readFileSync( + join(root, 'scripts/refactor-v3/db-dry-run.ps1'), + 'utf8', + ); + + expect(dryRunScript).toContain('--default-character-set=utf8mb4'); + expect(dryRunScript).toContain('$LASTEXITCODE'); + expect(dryRunScript).toContain('throw'); + }); + + it('uses the Nest listen port as the local smoke default base URL', () => { + const localSmokeScript = readFileSync( + join(root, 'scripts/refactor-v3/local-smoke.ps1'), + 'utf8', + ); + const main = readFileSync(join(root, 'src/main.ts'), 'utf8'); + + expect(main).toContain('app.listen(48085)'); + expect(localSmokeScript).toContain('http://127.0.0.1:48085'); + }); });