v2.3更新

This commit is contained in:
2026-05-28 20:48:29 +08:00
parent ca53fdc349
commit 7dbe98ee02
15 changed files with 749 additions and 86 deletions

View File

@@ -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>';
});
}