v2.3更新
This commit is contained in:
205
upgrade.php
205
upgrade.php
@@ -12,12 +12,20 @@
|
||||
|
||||
// 版本升级列表(唯一数据源)
|
||||
$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',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -84,27 +92,189 @@ function getUpgradeSteps($currentVersion, $targetVersion) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个版本的升级 SQL
|
||||
* 执行 SQL 内容,处理包含 DELIMITER 的存储过程脚本
|
||||
*
|
||||
* DELIMITER 是 MySQL 客户端指令,MySQL 服务器不认识它,
|
||||
* 必须在客户端侧解析并拆分为独立的语句后逐条执行。
|
||||
*/
|
||||
function executeUpgrade($pdo, $version, $sqlFile) {
|
||||
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) === '--');
|
||||
|
||||
if (trim($sql) === '' || trim($sql) === '--') {
|
||||
// 空文件或纯注释,无需执行 SQL,仅更新版本号
|
||||
} else {
|
||||
$pdo->exec($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;
|
||||
}
|
||||
|
||||
// 更新版本号(使用预处理语句防止 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]);
|
||||
throw new RuntimeException("升级至 v{$version} 失败 (尝试 {$maxRetries} 次): {$lastError}");
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
@@ -468,6 +638,13 @@ try {
|
||||
|
||||
<?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');
|
||||
@@ -512,7 +689,7 @@ try {
|
||||
btn.textContent = '升级失败';
|
||||
btn.disabled = false;
|
||||
resultArea.style.display = 'block';
|
||||
resultArea.innerHTML = '<div class="error-box"><strong>升级失败:</strong>' + (data.error || '未知错误') + '</div>';
|
||||
resultArea.innerHTML = '<div class="error-box"><strong>升级失败:</strong>' + escapeHtml(data.error || '未知错误') + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
@@ -525,7 +702,7 @@ try {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '立即升级';
|
||||
resultArea.style.display = 'block';
|
||||
resultArea.innerHTML = '<div class="error-box"><strong>请求失败:</strong>' + err.message + '</div>';
|
||||
resultArea.innerHTML = '<div class="error-box"><strong>请求失败:</strong>' + escapeHtml(err.message) + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user