*/ public const MODELS = [ \app\appmall\model\AddonList::class, // appmall_addon_list \app\appmall\model\AddonSubmission::class, // appmall_addon_submissions \app\appmall\model\AddonReviewLog::class, // appmall_addon_review_log \app\appmall\model\Developer::class, // appmall_developers \app\appmall\model\Revenue::class, // appmall_addon_revenues \app\appmall\model\Withdrawal::class, // appmall_addon_withdrawals \app\appmall\model\AddonOrder::class, // appmall_addon_orders \app\appmall\model\AddonLicense::class, // appmall_addon_licenses \app\appmall\model\AddonDownloadLog::class, // appmall_addon_download_logs \app\appmall\model\Site::class, // appmall_sites \app\appmall\model\FrameworkList::class, // appmall_framework_list \app\appmall\model\FrameworkDownloadLog::class, // appmall_framework_download_log \app\appmall\model\Throttle::class, // appmall_throttle \app\appmall\model\AddonReview::class, // appmall_addon_reviews(用户评价) ]; /** 进程内已确保建表的表(不含前缀),避免每请求重复 SHOW TABLES */ private static $tableDone = []; /** 进程内已确保过的扩展列 "表|列" */ private static $colDone = []; /** install.sql 解析缓存:[不含前缀表名 => CREATE 语句] */ private static $ddlMap = null; /** * 确保单张 appmall 表就绪:缺表建表 + 补齐扩展列(幂等,进程内只探测一次)。 * 模型 ensureSchema() 直接委托这里,控制器无需再手写 DDL。 * * @param string $table 不含前缀的表名,如 appmall_addon_list * @param array $columns 该表的老库扩展列 [列名 => 列定义] */ public static function ensureTable(string $table, array $columns = []): void { $table = self::bare($table); $prefix = BaseModel::currentPrefix(); if (!isset(self::$tableDone[$table])) { self::$tableDone[$table] = true; $map = self::ddlMap(); if (isset($map[$table])) { BaseModel::ensureTable($prefix . $table, str_replace('__PREFIX__', $prefix, $map[$table])); } } foreach ($columns as $col => $def) { $key = $table . '|' . $col; if (isset(self::$colDone[$key])) { continue; } self::$colDone[$key] = true; BaseModel::ensureColumn($prefix . $table, $col, $def); } } /** * 确保全部 appmall 表就绪(幂等,进程内每表只探测一次)。 * * 触发点: * - app/appmall/middleware.php(应用级中间件)→ 任何 /appmall/* 请求先建全表; * - 任意模型被使用 → 该模型 initialize() 自触发对应单表兜底。 */ public static function ensure(): void { // 1) 模型清单:缺表建表 + 老库扩展列,随模型一起维护,不会漏 foreach (self::MODELS as $model) { try { if (class_exists($model) && method_exists($model, 'ensureSchema')) { $model::ensureSchema(); } } catch (\Throwable $e) { // 单表自愈失败不阻断其余表(由业务暴露) } } // 2) install.sql 里存在、但暂未建模型的表也兜底(防止「只加 SQL 忘了加模型」而漏表) foreach (array_keys(self::ddlMap()) as $table) { self::ensureTable($table); } } /** * 去掉可能已带上的表前缀,统一成 install.sql 里的裸表名。 */ private static function bare(string $table): string { $prefix = BaseModel::currentPrefix(); if ($prefix !== '' && strpos($table, $prefix) === 0) { return substr($table, strlen($prefix)); } return $table; } /** * 解析 install.sql,得到 [裸表名 => CREATE 语句](语句内保留 __PREFIX__ 占位符)。 * 结果进程内缓存一次;文件缺失时记日志并返回空表(自愈降级为不动作,不阻断业务)。 */ private static function ddlMap(): array { if (self::$ddlMap !== null) { return self::$ddlMap; } self::$ddlMap = []; $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'install.sql'; if (!is_file($file)) { try { Log::error('[appmall] 建表事实源 install.sql 缺失,表结构自愈已跳过:' . $file); } catch (\Throwable $e) { // 日志不可用则忽略 } return self::$ddlMap; } $content = (string) file_get_contents($file); if (preg_match_all('/CREATE TABLE IF NOT EXISTS `__PREFIX__([a-z0-9_]+)`\s*\(.*?\)\s*ENGINE=[^;]*;/is', $content, $rows, PREG_SET_ORDER)) { foreach ($rows as $row) { self::$ddlMap[$row[1]] = $row[0]; } } return self::$ddlMap; } }