// +---------------------------------------------------------------------- // | 文章CMS插件数据表自愈聚合入口 // +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\articles\library; use think\facade\Db; /** * 插件表结构自愈 * * 背景(重要): * - 框架 install.sql 导入有白名单,只执行 CREATE TABLE / INSERT, * ALTER / DROP / UPDATE 会被跳过并记日志 → 老库升级缺列只能靠代码补。 * - 早期实现复用 BaseModel::ensureTableFromInstall(),它读的是**核心** * public/install/install.sql(不含本插件表)→ 恒为空操作,还每请求 * 重复 file_get_contents + 正则解析大文件。 * * 现方案:本类直接解析**插件自身** addon/articles/install.sql: * - repair(false):只建表(轻量,首访兜底,同请求只跑一次) * - repair(true) :建表 + 补列 + 补索引(install / upgrade 时调用) */ class ArticleSchema { /** 同一次请求内是否已检查过(避免每请求重复解析 SQL) */ private static bool $checked = false; /** 需要补齐的性能索引:表名(不含前缀) => [索引名 => 列定义] */ private const INDEXES = [ 'articles_article' => [ // 前台列表:status + delete_at 过滤后按 create_at 排序 'idx_list' => '`status`,`delete_at`,`create_at`', // 后台列表:order is_top desc / is_recommend 'idx_top' => '`is_top`,`is_recommend`', ], ]; /** * 首访兜底:确保表存在(不补列,同请求只执行一次) */ public static function ensure(): void { if (self::$checked) { return; } self::$checked = true; self::repair(false); } /** * 完整自愈:建表(+ 补列 + 补索引) * * @param bool $withColumns 是否补列与补索引(升级场景传 true) */ public static function repair(bool $withColumns = true): void { $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'install.sql'; if (!is_file($file)) { return; } $content = (string) file_get_contents($file); $prefix = self::prefix(); // 解析 CREATE TABLE IF NOT EXISTS `__PREFIX__xxx` ( ... ) ENGINE=...; preg_match_all( '/CREATE TABLE IF NOT EXISTS\s+`__PREFIX__([a-z_]+)`\s*\((.*?)\)\s*ENGINE=[^;]*;/is', $content, $matches, PREG_SET_ORDER ); foreach ($matches as $m) { $table = $prefix . $m[1]; self::ensureTable($table, str_replace('__PREFIX__', $prefix, $m[0])); if ($withColumns) { self::ensureColumns($table, $m[2]); } } if ($withColumns) { foreach (self::INDEXES as $table => $indexes) { foreach ($indexes as $name => $cols) { self::ensureIndex($prefix . $table, $name, $cols); } } // 全文索引(可选):用于加速搜索,不支持时静默降级(搜索仍走 LIKE) self::ensureFulltext($prefix . 'articles_article', 'ft_title_summary', '`title`,`summary`'); } } /** * 当前数据库表前缀 */ private static function prefix(): string { $default = (string) config('database.default', 'mysql'); return (string) config("database.connections.{$default}.prefix", ''); } /** * 表不存在则建表 */ private static function ensureTable(string $table, string $ddl): void { try { $exists = Db::query("SHOW TABLES LIKE '{$table}'"); if (empty($exists)) { Db::execute($ddl); } } catch (\Throwable $e) { // 建表失败不阻断访问,交由 install/upgrade 流程暴露 } } /** * 按 install.sql 的列定义补列(SHOW COLUMNS 比对后 ALTER ADD COLUMN) */ private static function ensureColumns(string $table, string $body): void { $exists = []; try { foreach (Db::query("SHOW COLUMNS FROM `{$table}`") as $row) { $exists[strtolower((string) $row['Field'])] = true; } } catch (\Throwable $e) { return; // 表都还没有,交给 ensureTable } foreach (preg_split('/\r\n|\r|\n/', $body) as $line) { $line = trim($line); if ($line === '') { continue; } // 跳过键定义行 if (preg_match('/^(PRIMARY\s+KEY|UNIQUE\s+KEY|KEY|INDEX|CONSTRAINT|FULLTEXT)\b/i', $line)) { continue; } if (!preg_match('/^`(\w+)`\s+(.+?),?$/s', $line, $m)) { continue; } $col = $m[1]; if (isset($exists[strtolower($col)])) { continue; } try { Db::execute('ALTER TABLE `' . $table . '` ADD COLUMN `' . $col . '` ' . rtrim($m[2], ',')); } catch (\Throwable $e) { // 单列补失败不影响其它列 } } } /** * 全文索引不存在则添加(中文优先用 ngram 解析器,失败退回默认解析器,再失败则忽略) */ private static function ensureFulltext(string $table, string $name, string $cols): void { try { foreach (Db::query("SHOW INDEX FROM `{$table}`") as $row) { if (($row['Key_name'] ?? '') === $name) { return; } } try { Db::execute("ALTER TABLE `{$table}` ADD FULLTEXT INDEX `{$name}` ({$cols}) WITH PARSER ngram"); } catch (\Throwable $e) { // 不支持 ngram(非 MySQL / 版本过旧):退回默认解析器 Db::execute("ALTER TABLE `{$table}` ADD FULLTEXT INDEX `{$name}` ({$cols})"); } } catch (\Throwable $e) { // 不支持全文索引:忽略,搜索自动降级为 LIKE } } /** * 索引不存在则添加 */ private static function ensureIndex(string $table, string $name, string $cols): void { try { foreach (Db::query("SHOW INDEX FROM `{$table}`") as $row) { if (($row['Key_name'] ?? '') === $name) { return; } } Db::execute("ALTER TABLE `{$table}` ADD INDEX `{$name}` ({$cols})"); } catch (\Throwable $e) { // 索引已存在或不支持时忽略 } } }