v2.1更新

This commit is contained in:
2026-05-26 13:47:01 +08:00
parent c575d711ee
commit f84c9d3efb
26 changed files with 1482 additions and 567 deletions

532
upgrade.php Normal file
View File

@@ -0,0 +1,532 @@
<?php
/**
* 班级操行分管理系统 - 自动升级脚本
*
* 读取 VERSION 文件确定目标版本,自动检测数据库当前版本,
* 依次执行增量 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) {
$allVersions = [
'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',
];
$steps = [];
foreach ($allVersions as $version => $sqlFile) {
if (version_compare($version, $currentVersion, '>') &&
version_compare($version, $targetVersion, '<=')) {
$steps[$version] = $sqlFile;
}
}
uksort($steps, 'version_compare');
return $steps;
}
/**
* 执行单个版本的升级 SQL
*/
function executeUpgrade($pdo, $version, $sqlFile) {
if (!file_exists($sqlFile)) {
throw new RuntimeException("SQL 文件不存在: {$sqlFile}");
}
$sql = file_get_contents($sqlFile);
if (trim($sql) === '' || trim($sql) === '--') {
// 空文件或纯注释,无需执行 SQL仅更新版本号
} else {
$pdo->exec($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]);
}
// ===========================================
// 主逻辑
// ===========================================
// 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 文件
$allVersions = [
'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',
];
if (!isset($allVersions[$stepVersion])) {
throw new RuntimeException("未知版本: {$stepVersion}");
}
$sqlFile = $allVersions[$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 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 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>' + (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>' + err.message + '</div>';
});
}
executeNextStep();
}
</script>
<?php endif; ?>
</body>
</html>