升级面板 * /admin/upgrade/apply/ -> 升级单个 * /admin/upgrade/applyall -> 一键全部升级 * /admin/upgrade/init -> 基础数据/表结构初始化(POST,仅超管) */ class UpgradeController extends Controller { protected $layout = 'layouts/admin'; private function dir(): string { return BASE_PATH . '/install/upgrades'; } public function index() { admin_required(); if (Db::driver() !== 'mysql') { return $this->view('admin/upgrade', ['driver' => 'file']); } $this->requireSuper(); Installer::ensureUpgradeLog(); $files = $this->scan(); $applied = $this->appliedMap(); $list = []; foreach ($files as $f) { $name = basename($f); $hash = md5_file($f); $state = !isset($applied[$name]) ? 'pending' : ($applied[$name] !== $hash ? 'changed' : 'done'); $list[] = [ 'name' => $name, 'size' => filesize($f), 'mtime' => filemtime($f), 'state' => $state, ]; } // 排序:待升级 / 已变更 在前,已应用在后 $rank = ['pending' => 0, 'changed' => 1, 'done' => 2]; usort($list, fn($a, $b) => ($rank[$a['state']] ?? 9) - ($rank[$b['state']] ?? 9)); $pending = count(array_filter($list, fn($x) => $x['state'] !== 'done')); $history = Db::query("SELECT * FROM db_upgrades ORDER BY applied_at DESC, id DESC LIMIT 50")->fetchAll(); return $this->view('admin/upgrade', [ 'driver' => 'mysql', 'list' => $list, 'pending' => $pending, 'history' => $history, ]); } /** 升级单个升级包 */ public function apply($file = null) { admin_required(); $this->requireSuper(); $file = basename((string)$file); $path = $this->dir() . '/' . $file; if (!is_file($path) || !preg_match('/\.sql$/i', $file)) { $this->flash('升级包不存在', 'err'); $this->redirect('admin/upgrade'); return ''; } try { Installer::applySqlFile($path); Installer::ensureUpgradeLog(); Db::query( "INSERT INTO db_upgrades (file, hash, applied_at, applied_by, note) VALUES (?, ?, ?, ?, ?)", [$file, md5_file($path), date('Y-m-d H:i:s'), ($_SESSION['admin']['username'] ?? 'admin'), ''] ); $this->flash("已升级:{$file}", 'ok'); } catch (\Throwable $e) { $this->flash('升级失败:' . $e->getMessage(), 'err'); } $this->redirect('admin/upgrade'); return ''; } /** 一键升级全部待处理 */ public function applyAll() { admin_required(); $this->requireSuper(); $files = $this->scan(); $applied = $this->appliedMap(); $done = 0; foreach ($files as $f) { $name = basename($f); $hash = md5_file($f); if (isset($applied[$name]) && $applied[$name] === $hash) continue; try { Installer::applySqlFile($f); Installer::ensureUpgradeLog(); Db::query( "INSERT INTO db_upgrades (file, hash, applied_at, applied_by, note) VALUES (?, ?, ?, ?, ?)", [$name, $hash, date('Y-m-d H:i:s'), ($_SESSION['admin']['username'] ?? 'admin'), ''] ); $done++; } catch (\Throwable $e) { $this->flash('升级失败:' . e($e->getMessage()), 'err'); $this->redirect('admin/upgrade'); return ''; } } $this->flash($done > 0 ? "已批量升级 {$done} 个升级包" : '没有需要升级的包', $done > 0 ? 'ok' : 'err'); $this->redirect('admin/upgrade'); return ''; } /** 基础数据/表结构初始化(保留旧版能力:补齐新模块表与种子) */ public function init() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->redirect('admin/upgrade'); return ''; } admin_required(); $this->requireSuper(); if (!csrf_check()) { $this->flash('表单已过期,请刷新后重试', 'err'); $this->redirect('admin/upgrade'); return ''; } $msgs = Installer::upgrade(); $this->flash('基础数据升级完成 ✓ ' . implode(';', $msgs), 'ok'); $this->redirect('admin/upgrade'); return ''; } /* ---------------- 工具 ---------------- */ private function requireSuper(): void { if (!is_admin() || admin_role() !== 'super_admin') { App::forbidden('仅超级管理员可执行数据库升级'); } } private function scan(): array { $d = $this->dir(); return is_dir($d) ? (glob($d . '/*.sql') ?: []) : []; } private function appliedMap(): array { try { return Db::query("SELECT file, hash FROM db_upgrades")->fetchAll(\PDO::FETCH_KEY_PAIR); } catch (\Throwable $e) { return []; } } }