# 模型自愈机制(Schema Self-Heal) YwxApp 的数据库结构自愈引擎收敛在 `ywxapp/model/BaseModel.php` 的静态方法中。 它的定位是 **「缺表 / 缺列兜底」**,不是迁移工具——旧表改名、跨前缀重整等问题仍需手动执行迁移 SQL(见 `docs/migrations/`)。 ## 一、自愈引擎能力(BaseModel 静态方法) | 方法 | 作用 | 说明 | | --- | --- | --- | | `ensureTable($table, $sql)` | 表不存在则 CREATE | `$table` 为已带前缀的完整表名;内部先 `SHOW TABLES LIKE` 探测 | | `ensureTableFromInstall($prefix, $table)` | 从 `public/install/install.sql` 正则提取建表 DDL 执行 | **install.sql 是唯一事实源**;`$table` 为不含前缀的表名 | | `ensureColumn($table, $col, $def)` | 列不存在则 ALTER ADD | MySQL 不支持 `ADD COLUMN IF NOT EXISTS`,故先探测 | | `ensureAutoIncrementPk($table, $col='id')` | 修复 id 非自增主键 | 老库 `id` 定义成 NOT NULL 但无 AUTO_INCREMENT 时,create() 会报 1364 | | `tableExists($table)` | 探测表是否存在 | `SHOW TABLES LIKE`,异常时返回 false | | `currentPrefix()` | 取运行时表前缀 | 优先 `config/database.php`,回退 `Db::getConfig('prefix')` | 所有方法内部都包了 `try/catch`:自愈失败(如数据库账号无 `CREATE/ALTER` 权限、`NO_BACKSLASH_ESCAPES` 导致 SQL 出错)会**被静默吞掉并记录日志**,不会阻断业务——但意味着「自愈没起作用」时不会立刻报错,需要查日志才能发现。 > **模块自带 SQL 的情况**:`ensureTableFromInstall()` 只认核心的 `public/install/install.sql`。 > 中心站专用模块 `appmall` 有自己的一份事实源 `app/appmall/install.sql`,由 > `app/appmall/library/AppmallSchema` 用同样的「解析 SQL → `ensureTable()`」方式执行, > 模块内不再内联重复 DDL。 ## 二、模型壳约定 每个需要自愈的模型都自带一个静态方法: ```php public static function ensureSchema(): void { try { $prefix = \think\facade\Db::getConfig('prefix') ?: ''; $table = $prefix . 'notice'; // 缺表建表(优先 ensureTableFromInstall,或从 install.sql 同款 DDL 兜底) self::ensureTable($table, "CREATE TABLE IF NOT EXISTS `{$prefix}notice` (...)"); // 老库扩展列兜底(幂等) self::ensureColumn($table, 'type', "tinyint(1) NOT NULL DEFAULT 1"); } catch (\Throwable $e) { // 忽略自愈失败,由业务暴露 } } ``` **幂等性**:所有自愈都先探测(SHOW TABLES / SHOW COLUMNS)再操作,已存在则跳过,重复调用安全。 **自动触发(appmall 采用)**:`app/appmall/model/` 下的 **13 个模型与本模块 13 张表一一对应** (清单登记在 `AppmallSchema::MODELS`,加表必查),每个都在 `initialize()` 里调用 `self::ensureSchema()` —— 模型一被使用(含 `Model::where()` 这类静态查询)就先兜底建表 + 补老库扩展列, 业务入口无需记得手动调用,不会再撞 1146(缺表)/ 1054(缺列)。 `AppmallSchema::ensureTable()` 内部按「表」和「表+列」做了进程内 memo,每请求每表最多一次 `SHOW TABLES`, 开销可忽略。(核心库同样有先例:`MemberProp::initialize()` / `CommonCard::initialize()`。) **应用级中间件兜底(appmall 采用)**:`app/appmall/middleware.php` 注册 `AppmallSchemaMiddleware`,任何进入 `/appmall/*` 的请求(前台页 / 后台页 / 对外 API / 未注册路由) 在业务之前先 `AppmallSchema::ensure()` 一次(见第三节第 3 小节)。 > 为什么要「模型 + 中间件」双层:模型兜底保证**直接使用模型**的代码路径不缺表; > 中间件兜底保证**任何入口**(含控制器里大量 `Db::name('appmall_xxx')` 直查、 > 以及将来新增的页面 / 接口)不会因为某处忘了调用而缺表。两者都幂等,重复执行无副作用。 ## 三、触发时机(谁在什么时候调用 ensureSchema) 自愈**不会定时 / 不会自动**,全部是「业务请求时按需触发」。 ### 1. 核心库(每次相关请求都跑一次,开销极小) | 入口 | 调用 | 触发场景 | | --- | --- | --- | | `ywxapp/library/AdminAuth.php` (`login`/`register`) | `BackendAdmin::ensureSchema()` | 后台登录 / 注册 | | `ywxapp/controller/MemberBase.php` (`_initialize`) | `MemberUser::ensureSchema()` | 访问任意会员中心控制器 | ### 2. 业务方法内触发 | 模型 / 类 | 触发方法 | 触发场景 | | --- | --- | --- | | `ywxapp/model/Notice.php` | `getActiveTopNotices()` / `getList()` | 前台公告条渲染、公告列表页 | | `ywxapp/model/Ad.php` | `getByPosition()` / `getSlots()` | 广告位渲染 | ### 3. 中间件 | 入口 | 调用 | 触发场景 | | --- | --- | --- | | `ywxapp/middleware/SpiderStat`(全局) | `ensureTables()` | 每次请求(蜘蛛日志 / 统计表) | | `app/appmall/middleware/AppmallSchemaMiddleware`(应用级) | `AppmallSchema::ensure()` | **任意** `/appmall/*` 请求(前台 / 后台 / 对外 API) | ### 4. 插件安装 / 升级 | 插件 | 触发入口 | 触发场景 | | --- | --- | --- | | blog | `BlogSchema::ensure*` | 插件业务方法 | | forum | `ForumSchema::ensure*` | 插件业务方法 | | appmall(中心站) | 应用中间件 `AppmallSchema::ensure()` + 各模型 `ensureSchema()`(`initialize()` 自动触发) | 任意 `/appmall/*` 请求,或直接使用任一 appmall 模型 | | haonav / seo | `Addon.php::upgrade()` / `ensureTablesFromInstallSql()` | 插件升级 / 安装 | | wxchat / iot / mqpay 等 | 各自 Schema 类 | 插件初始化 | ## 四、线上老库迁移不在自愈范围 自愈只负责「缺表 / 缺列兜底」,**不会做 RENAME、不会跨前缀重整**。旧库改名必须手动执行迁移 SQL: - `docs/migrations/migrate_admin_user_to_backend_member_wxapp.sql` — admin/user → backend/member - `docs/migrations/rename_table_prefix.sql` — 批量改前缀 - `docs/migrations/fix_missing_wxapp_notice.sql` — 自愈因权限被吞时手动补表 ## 五、排错:遇到 1146 Table doesn't exist 1. 确认表是否在 `install.sql` 中有建表语句(事实源)。 2. 若是核心 / 插件表,确认对应 `ensureSchema()` 是否被调用(见第三节入口)。 3. ⚠️ **高频根因:运行时没取到表前缀**。`ensureSchema()` 若用 `Db::getConfig('prefix')` 取前缀,在部分线上环境(多应用路由 / CLI / Db 配置未就绪)会返回空字符串,导致自愈建出**无前缀的表**(如 `notice`),而模型 `name` 拼前缀后查的是 `wxapp_notice` → 1146。 - **修复**:所有模型壳 / 控制器一律改用 `BaseModel::currentPrefix()` 取前缀(优先 `config/database.php`,回退 `Db::getConfig`),并已统一修复 `Notice` / `Ad`;`appmall` 的控制器 / 服务原先各自内联 DDL 且用 `Db::getConfig('prefix')`,现已全部收敛到 `AppmallSchema`(事实源 `app/appmall/install.sql`,统一走 `currentPrefix()`),并由应用中间件统一触发。 - 自查:`grep "Db::getConfig('prefix')"` 应只剩 0 处(全部换成 `currentPrefix`)。 4. 若前缀正确、`ensureSchema()` 已调用仍 1146 → **多半是线上 DB 账号无 CREATE/ALTER 权限**,自愈被静默吞掉。 - 根治:给 DB 用户授予建表 / 改表权限,自愈会自动补齐; - 兜底:手动执行 `docs/migrations/` 下对应修复 SQL(如 `fix_missing_wxapp_notice.sql`)。 5. 若是改名类(admin_* / user_*)→ 必须手动跑迁移 SQL,自愈无法处理。