__DIR__ . '/sql/upgrades/v1.0.sql', '1.1' => __DIR__ . '/sql/upgrades/v1.1.sql', '1.2' => __DIR__ . '/sql/upgrades/v1.2.sql', '1.3' => __DIR__ . '/sql/upgrades/v1.3.sql', '1.4' => __DIR__ . '/sql/upgrades/v1.4.sql', '1.5' => __DIR__ . '/sql/upgrades/v1.5.sql', '1.6' => __DIR__ . '/sql/upgrades/v1.6.sql', '1.7' => __DIR__ . '/sql/upgrades/v1.7.sql', '1.8' => __DIR__ . '/sql/upgrades/v1.8.sql', '2.0' => __DIR__ . '/sql/upgrades/v2.0.sql', '2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql', '2.1' => __DIR__ . '/sql/upgrades/v2.1.sql', '2.2' => __DIR__ . '/sql/upgrades/v2.2.sql', '2.3' => __DIR__ . '/sql/upgrades/v2.3.sql', '2.4' => __DIR__ . '/sql/upgrades/v2.4.sql', '2.5' => __DIR__ . '/sql/upgrades/v2.5.sql', '2.5.1' => __DIR__ . '/sql/upgrades/v2.5.1.sql', '2.6' => __DIR__ . '/sql/upgrades/v2.6.sql', '2.7' => __DIR__ . '/sql/upgrades/v2.7.sql', '2.8' => __DIR__ . '/sql/upgrades/v2.8.sql', ]; /** * 读取 backend/.env 文件并解析数据库配置 */ function readEnvConfig($envPath) { if (!file_exists($envPath)) { throw new RuntimeException('配置文件不存在: ' . $envPath); } $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $config = []; foreach ($lines as $line) { $line = trim($line); if ($line === '' || strpos($line, '#') === 0) { continue; } if (strpos($line, '=') !== false) { list($key, $value) = explode('=', $line, 2); $config[trim($key)] = trim($value); } } $required = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']; foreach ($required as $key) { if (!isset($config[$key]) || $config[$key] === '') { throw new RuntimeException("缺少必要的数据库配置: {$key}"); } } return $config; } /** * 检测数据库当前版本 */ function detectCurrentVersion($pdo) { try { $stmt = $pdo->query("SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'"); $row = $stmt->fetch(PDO::FETCH_ASSOC); return $row ? $row['setting_value'] : '0.0.0'; } catch (PDOException $e) { return '0.0.0'; } } /** * 获取需要执行的升级步骤 */ function getUpgradeSteps($currentVersion, $targetVersion) { global $UPGRADE_VERSIONS; $steps = []; foreach ($UPGRADE_VERSIONS as $version => $sqlFile) { if (version_compare($version, $currentVersion, '>') && version_compare($version, $targetVersion, '<=')) { $steps[$version] = $sqlFile; } } uksort($steps, 'version_compare'); return $steps; } /** * 执行 SQL 内容,处理包含 DELIMITER 的存储过程脚本 * * DELIMITER 是 MySQL 客户端指令,MySQL 服务器不认识它, * 必须在客户端侧解析并拆分为独立的语句后逐条执行。 */ function executeSqlContent($pdo, $sqlContent) { $sqlContent = trim($sqlContent); if ($sqlContent === '' || $sqlContent === '--') { return; } // 检查是否包含 DELIMITER 指令 if (stripos($sqlContent, 'DELIMITER') !== false) { $lines = explode("\n", $sqlContent); $currentBlock = []; $inProcedure = false; $buffer = ''; foreach ($lines as $line) { $trimmed = trim($line); // 跳过纯注释行(存储过程内部注释保留) if (!$inProcedure && (strpos($trimmed, '--') === 0 || strpos($trimmed, '#') === 0)) { continue; } if (strtoupper(substr($trimmed, 0, 12)) === 'DELIMITER $$') { // 开始存储过程定义 $inProcedure = true; $currentBlock = []; continue; } elseif (strtoupper($trimmed) === 'DELIMITER ;') { // 执行累积的存储过程块 if (!empty($currentBlock)) { $procSql = trim(implode("\n", $currentBlock)); if ($procSql !== '') { // 移除存储过程结尾的 $$ 定界符(发送给 MySQL 服务器时不需要) $procSql = preg_replace('/\$\$\s*$/', '', $procSql); $pdo->exec($procSql); } } $inProcedure = false; $currentBlock = []; continue; } elseif (strtoupper(substr($trimmed, 0, 9)) === 'DELIMITER') { // 其他 DELIMITER 指令,跳过 continue; } if ($inProcedure) { $currentBlock[] = $line; } else { // 普通 SQL,累积直到遇到分号 if ($trimmed !== '') { $buffer .= ($buffer !== '' ? ' ' : '') . $trimmed; if (rtrim($buffer) !== '' && substr(rtrim($buffer), -1) === ';') { $stmt = rtrim(rtrim($buffer), ';'); $stmt = trim($stmt); if ($stmt !== '' && $stmt !== '--') { $pdo->exec($stmt); } $buffer = ''; } } } } // 处理缓冲区中剩余的语句 if ($buffer !== '') { $stmt = rtrim(rtrim($buffer), ';'); $stmt = trim($stmt); if ($stmt !== '' && $stmt !== '--') { $pdo->exec($stmt); } } } else { // 无 DELIMITER,按分号+换行分割语句 $statements = preg_split('/;\s*\n/', $sqlContent); foreach ($statements as $stmt) { $stmt = trim($stmt); if ($stmt !== '' && $stmt !== '--') { $pdo->exec($stmt); } } } } /** * 验证升级结果:检查版本号是否已正确更新 * * @return array ['ok' => bool, 'message' => string] */ function verifyUpgrade($pdo, $expectedVersion) { // 检查 system_settings 表是否存在 try { $check = $pdo->query("SELECT 1 FROM system_settings LIMIT 1"); } catch (PDOException $e) { return ['ok' => false, 'message' => 'system_settings 表不存在,升级脚本可能未正确执行']; } // 检查版本号是否匹配 $stmt = $pdo->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'"); $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!$row) { return ['ok' => false, 'message' => 'db_version 记录不存在']; } if ($row['setting_value'] !== $expectedVersion) { return ['ok' => false, 'message' => "版本号不匹配:期望 {$expectedVersion},实际 {$row['setting_value']}"]; } return ['ok' => true, 'message' => '验证通过']; } /** * 执行单个版本的升级 SQL(含验证与重试) * * @param PDO $pdo 数据库连接 * @param string $version 目标版本号 * @param string $sqlFile SQL 文件路径 * @param int $maxRetries 最大重试次数 * @throws RuntimeException 升级失败时抛出 */ function executeUpgrade($pdo, $version, $sqlFile, $maxRetries = 2) { if (!file_exists($sqlFile)) { throw new RuntimeException("SQL 文件不存在: {$sqlFile}"); } $sql = file_get_contents($sqlFile); $isEmpty = (trim($sql) === '' || trim($sql) === '--'); $lastError = null; for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { try { if (!$isEmpty) { executeSqlContent($pdo, $sql); } // 更新版本号(使用预处理语句防止 SQL 注入) $stmt = $pdo->prepare( "INSERT INTO system_settings (setting_key, setting_value) VALUES ('db_version', :version) ON DUPLICATE KEY UPDATE setting_value = :version" ); $stmt->execute([':version' => $version]); // 验证版本号是否正确写入 $verify = verifyUpgrade($pdo, $version); if ($verify['ok']) { return; // 成功 } // 验证失败,准备重试 $lastError = "升级验证失败: {$verify['message']}"; if ($attempt < $maxRetries) { // 回滚版本号到升级前状态,以便重试 $prevStmt = $pdo->prepare( "UPDATE system_settings SET setting_value = :ver WHERE setting_key = 'db_version'" ); // 获取升级前版本(从 getUpgradeSteps 推断,这里用 0.0.0 作为安全回退) $prevStmt->execute([':ver' => '0.0.0']); continue; } } catch (PDOException $e) { $lastError = "SQL 执行失败: " . $e->getMessage(); if ($attempt < $maxRetries) { continue; } } catch (Exception $e) { $lastError = $e->getMessage(); if ($attempt < $maxRetries) { continue; } } // 所有重试均失败 break; } throw new RuntimeException("升级至 v{$version} 失败 (尝试 {$maxRetries} 次): {$lastError}"); } // =========================================== // 主逻辑 // =========================================== // POST 模式:执行单个升级步骤(依次执行) if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'step') { header('Content-Type: application/json; charset=utf-8'); $stepVersion = $_GET['version'] ?? ''; if (empty($stepVersion)) { http_response_code(400); echo json_encode(['success' => false, 'error' => '缺少版本号参数']); exit(); } try { $envPath = __DIR__ . '/backend/.env'; $config = readEnvConfig($envPath); $dsn = "mysql:host={$config['DB_HOST']};port={$config['DB_PORT']};dbname={$config['DB_NAME']};charset=utf8mb4"; $pdo = new PDO($dsn, $config['DB_USER'], $config['DB_PASSWORD'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); // 获取该版本对应的 SQL 文件 if (!isset($UPGRADE_VERSIONS[$stepVersion])) { throw new RuntimeException("未知版本: {$stepVersion}"); } $sqlFile = $UPGRADE_VERSIONS[$stepVersion]; $shortFile = basename($sqlFile); executeUpgrade($pdo, $stepVersion, $sqlFile); // 重新检测当前版本 $newVersion = detectCurrentVersion($pdo); echo json_encode([ 'success' => true, 'version' => $stepVersion, 'message' => "升级至 v{$stepVersion} 成功 ({$shortFile})", 'current' => $newVersion ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'version' => $stepVersion, 'error' => "升级至 v{$stepVersion} 失败: " . $e->getMessage() ]); } exit(); } // POST 模式:执行升级(AJAX 请求) if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'execute') { header('Content-Type: application/json; charset=utf-8'); $upgradeLog = []; $currentVersion = '未知'; $targetVersion = '未知'; try { $envPath = __DIR__ . '/backend/.env'; $config = readEnvConfig($envPath); $dsn = "mysql:host={$config['DB_HOST']};port={$config['DB_PORT']};dbname={$config['DB_NAME']};charset=utf8mb4"; $pdo = new PDO($dsn, $config['DB_USER'], $config['DB_PASSWORD'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); $currentVersion = detectCurrentVersion($pdo); $versionFile = __DIR__ . '/VERSION'; if (!file_exists($versionFile)) { throw new RuntimeException('VERSION 文件不存在'); } $targetVersion = trim(file_get_contents($versionFile)); $upgradeSteps = getUpgradeSteps($currentVersion, $targetVersion); if (empty($upgradeSteps)) { echo json_encode([ 'success' => true, 'current' => $currentVersion, 'target' => $targetVersion, 'steps' => [['version' => '', 'status' => 'uptodate', 'message' => '数据库已是最新版本,无需升级。']] ]); exit(); } $pdo->beginTransaction(); try { foreach ($upgradeSteps as $version => $sqlFile) { $shortFile = basename($sqlFile); try { executeUpgrade($pdo, $version, $sqlFile); $upgradeLog[] = [ 'version' => $version, 'status' => 'success', 'message' => "升级至 v{$version} 成功 ({$shortFile})" ]; } catch (Exception $e) { $upgradeLog[] = [ 'version' => $version, 'status' => 'error', 'message' => "升级至 v{$version} 失败 ({$shortFile}): " . $e->getMessage() ]; throw $e; } } $pdo->commit(); } catch (Exception $e) { $pdo->rollBack(); throw $e; } echo json_encode([ 'success' => true, 'current' => $currentVersion, 'target' => $targetVersion, 'steps' => $upgradeLog ]); } catch (Exception $e) { http_response_code(500); echo json_encode([ 'success' => false, 'current' => $currentVersion, 'target' => $targetVersion, 'steps' => $upgradeLog, 'error' => $e->getMessage() ]); } exit(); } // GET 模式:显示升级信息页面 $currentVersion = '未知'; $targetVersion = '未知'; $upgradeSteps = []; $hasError = false; $errorMessage = ''; $isUpToDate = false; try { $envPath = __DIR__ . '/backend/.env'; $config = readEnvConfig($envPath); $dsn = "mysql:host={$config['DB_HOST']};port={$config['DB_PORT']};dbname={$config['DB_NAME']};charset=utf8mb4"; $pdo = new PDO($dsn, $config['DB_USER'], $config['DB_PASSWORD'], [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); $currentVersion = detectCurrentVersion($pdo); $versionFile = __DIR__ . '/VERSION'; if (!file_exists($versionFile)) { throw new RuntimeException('VERSION 文件不存在: ' . $versionFile); } $targetVersion = trim(file_get_contents($versionFile)); $upgradeSteps = getUpgradeSteps($currentVersion, $targetVersion); $isUpToDate = empty($upgradeSteps); } catch (Exception $e) { $hasError = true; $errorMessage = $e->getMessage(); } ?> 系统升级 - 班级操行分管理系统

班级操行分管理系统 - 数据库升级

自动检测版本并执行增量升级

当前数据库版本
目标版本
错误:
💡 解决方法:
1. 进入 backend/ 目录
2. 复制配置模板:cp .env.example .env
3. 编辑 .env 文件,填入实际的数据库连接信息
4. 刷新此页面
✓ 数据库已是最新版本,无需升级。
待执行升级步骤
$sqlFile): ?>
升级至 v ()
⚠️ 升级前请确保已备份数据库,升级过程中请勿关闭页面。