721 lines
25 KiB
PHP
721 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* 班级操行分管理系统 - 自动升级脚本
|
||
*
|
||
* 读取 VERSION 文件确定目标版本,自动检测数据库当前版本,
|
||
* 依次执行增量 SQL 升级脚本。
|
||
*/
|
||
|
||
// ===========================================
|
||
// 辅助函数
|
||
// ===========================================
|
||
|
||
// 版本升级列表(唯一数据源)
|
||
$UPGRADE_VERSIONS = [
|
||
'1.0' => __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();
|
||
}
|
||
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>系统升级 - 班级操行分管理系统</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||
background: #f5f7fa;
|
||
color: #333;
|
||
line-height: 1.6;
|
||
padding: 20px;
|
||
}
|
||
.container {
|
||
max-width: 720px;
|
||
margin: 40px auto;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
padding: 24px 32px;
|
||
}
|
||
.header h1 { font-size: 20px; font-weight: 600; }
|
||
.header p { font-size: 13px; opacity: 0.85; margin-top: 4px; }
|
||
.content { padding: 24px 32px; }
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
font-size: 14px;
|
||
}
|
||
.info-row:last-child { border-bottom: none; }
|
||
.info-label { color: #888; }
|
||
.info-value { font-weight: 600; }
|
||
.section-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
margin: 20px 0 12px;
|
||
color: #444;
|
||
}
|
||
.step {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 10px 12px;
|
||
margin-bottom: 8px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
background: #fafafa;
|
||
border-left: 3px solid #ddd;
|
||
}
|
||
.step.success { border-left-color: #52c41a; background: #f6ffed; }
|
||
.step.error { border-left-color: #ff4d4f; background: #fff2f0; }
|
||
.step.pending { border-left-color: #faad14; background: #fffbe6; }
|
||
.step-icon { margin-right: 10px; font-size: 16px; flex-shrink: 0; }
|
||
.step.success .step-icon { color: #52c41a; }
|
||
.step.error .step-icon { color: #ff4d4f; }
|
||
.step.pending .step-icon { color: #faad14; }
|
||
.step-message { word-break: break-all; }
|
||
.error-box {
|
||
background: #fff2f0;
|
||
border: 1px solid #ffccc7;
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
margin-top: 16px;
|
||
color: #cf1322;
|
||
font-size: 13px;
|
||
word-break: break-all;
|
||
}
|
||
.warning-box {
|
||
background: #fffbe6;
|
||
border: 1px solid #ffe58f;
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
margin-top: 16px;
|
||
color: #ad6800;
|
||
font-size: 13px;
|
||
}
|
||
.btn-upgrade {
|
||
display: inline-block;
|
||
padding: 10px 32px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
margin-top: 20px;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.btn-upgrade:hover { opacity: 0.9; }
|
||
.btn-upgrade:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
.btn-upgrade.loading {
|
||
opacity: 0.7;
|
||
cursor: wait;
|
||
}
|
||
.action-area {
|
||
text-align: center;
|
||
margin-top: 16px;
|
||
}
|
||
.result-area {
|
||
margin-top: 16px;
|
||
}
|
||
.footer {
|
||
text-align: center;
|
||
padding: 16px 32px;
|
||
border-top: 1px solid #f0f0f0;
|
||
color: #aaa;
|
||
font-size: 12px;
|
||
}
|
||
.success-box {
|
||
background: #f6ffed;
|
||
border: 1px solid #b7eb8f;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
margin-top: 16px;
|
||
text-align: center;
|
||
color: #389e0d;
|
||
font-size: 14px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>班级操行分管理系统 - 数据库升级</h1>
|
||
<p>自动检测版本并执行增量升级</p>
|
||
</div>
|
||
<div class="content">
|
||
<div class="info-row">
|
||
<span class="info-label">当前数据库版本</span>
|
||
<span class="info-value" id="currentVersion"><?php echo htmlspecialchars($currentVersion); ?></span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="info-label">目标版本</span>
|
||
<span class="info-value" id="targetVersion"><?php echo htmlspecialchars($targetVersion); ?></span>
|
||
</div>
|
||
|
||
<?php if ($hasError): ?>
|
||
<div class="error-box">
|
||
<strong>错误:</strong><?php echo htmlspecialchars($errorMessage); ?>
|
||
</div>
|
||
<?php if ($hasError && strpos($errorMessage, '配置文件不存在') !== false): ?>
|
||
<div class="warning-box">
|
||
<strong>💡 解决方法:</strong><br>
|
||
1. 进入 <code>backend/</code> 目录<br>
|
||
2. 复制配置模板:<code>cp .env.example .env</code><br>
|
||
3. 编辑 <code>.env</code> 文件,填入实际的数据库连接信息<br>
|
||
4. 刷新此页面
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php elseif ($isUpToDate): ?>
|
||
<div class="success-box">
|
||
✓ 数据库已是最新版本,无需升级。
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="section-title">待执行升级步骤</div>
|
||
<?php foreach ($upgradeSteps as $version => $sqlFile): ?>
|
||
<div class="step pending" id="step-<?php echo htmlspecialchars($version); ?>">
|
||
<span class="step-icon">○</span>
|
||
<span class="step-message">升级至 v<?php echo htmlspecialchars($version); ?> (<?php echo htmlspecialchars(basename($sqlFile)); ?>)</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
|
||
<div class="warning-box">
|
||
⚠️ 升级前请确保已备份数据库,升级过程中请勿关闭页面。
|
||
</div>
|
||
|
||
<div class="action-area">
|
||
<button class="btn-upgrade" id="btnUpgrade" onclick="executeUpgrade()">立即升级</button>
|
||
</div>
|
||
|
||
<div class="result-area" id="resultArea" style="display:none;"></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="footer">
|
||
班级操行分管理系统 v<?php echo htmlspecialchars($targetVersion); ?>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (!$hasError && !$isUpToDate): ?>
|
||
<script>
|
||
function escapeHtml(str) {
|
||
if (typeof str !== 'string') return '';
|
||
var div = document.createElement('div');
|
||
div.appendChild(document.createTextNode(str));
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function executeUpgrade() {
|
||
var btn = document.getElementById('btnUpgrade');
|
||
var resultArea = document.getElementById('resultArea');
|
||
|
||
btn.disabled = true;
|
||
btn.classList.add('loading');
|
||
btn.textContent = '升级中...';
|
||
resultArea.style.display = 'none';
|
||
|
||
// 收集所有待执行步骤
|
||
var steps = [];
|
||
<?php foreach ($upgradeSteps as $version => $sqlFile): ?>
|
||
steps.push('<?php echo htmlspecialchars($version); ?>');
|
||
<?php endforeach; ?>
|
||
|
||
var currentIndex = 0;
|
||
|
||
function executeNextStep() {
|
||
if (currentIndex >= steps.length) {
|
||
btn.classList.remove('loading');
|
||
btn.textContent = '升级完成';
|
||
resultArea.style.display = 'block';
|
||
resultArea.innerHTML = '<div class="success-box">✓ 升级成功!数据库已更新至最新版本<br><br><small style="color:#888">建议升级完成后删除 upgrade.php 文件</small></div>';
|
||
return;
|
||
}
|
||
|
||
var version = steps[currentIndex];
|
||
fetch('?action=step&version=' + encodeURIComponent(version), { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
var el = document.getElementById('step-' + version);
|
||
if (el) {
|
||
el.className = 'step ' + (data.success ? 'success' : 'error');
|
||
el.querySelector('.step-icon').textContent = data.success ? '✓' : '✗';
|
||
}
|
||
|
||
if (data.success) {
|
||
currentIndex++;
|
||
executeNextStep();
|
||
} else {
|
||
btn.classList.remove('loading');
|
||
btn.textContent = '升级失败';
|
||
btn.disabled = false;
|
||
resultArea.style.display = 'block';
|
||
resultArea.innerHTML = '<div class="error-box"><strong>升级失败:</strong>' + escapeHtml(data.error || '未知错误') + '</div>';
|
||
}
|
||
})
|
||
.catch(function(err) {
|
||
var el = document.getElementById('step-' + version);
|
||
if (el) {
|
||
el.className = 'step error';
|
||
el.querySelector('.step-icon').textContent = '✗';
|
||
}
|
||
btn.classList.remove('loading');
|
||
btn.disabled = false;
|
||
btn.textContent = '立即升级';
|
||
resultArea.style.display = 'block';
|
||
resultArea.innerHTML = '<div class="error-box"><strong>请求失败:</strong>' + escapeHtml(err.message) + '</div>';
|
||
});
|
||
}
|
||
|
||
executeNextStep();
|
||
}
|
||
</script>
|
||
<?php endif; ?>
|
||
</body>
|
||
</html>
|