v2.3更新
This commit is contained in:
@@ -18,17 +18,25 @@ import re
|
|||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# 版本列表(按顺序)
|
||||||
# 版本列表(按顺序)
|
# 版本列表(按顺序)
|
||||||
ALL_VERSIONS = {
|
ALL_VERSIONS = {
|
||||||
|
'1.0': 'v1.0.sql',
|
||||||
|
'1.1': 'v1.1.sql',
|
||||||
|
'1.2': 'v1.2.sql',
|
||||||
|
'1.3': 'v1.3.sql',
|
||||||
|
'1.4': 'v1.4.sql',
|
||||||
|
'1.5': 'v1.5.sql',
|
||||||
|
'1.6': 'v1.6.sql',
|
||||||
'1.7': 'v1.7.sql',
|
'1.7': 'v1.7.sql',
|
||||||
'1.8': 'v1.8.sql',
|
'1.8': 'v1.8.sql',
|
||||||
'2.0': 'v2.0.sql',
|
'2.0': 'v2.0.sql',
|
||||||
'2.0.1': 'v2.0.1.sql',
|
'2.0.1': 'v2.0.1.sql',
|
||||||
'2.1': 'v2.1.sql',
|
'2.1': 'v2.1.sql',
|
||||||
'2.2': 'v2.2.sql',
|
'2.2': 'v2.2.sql',
|
||||||
|
'2.3': 'v2.3.sql',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/check")
|
@router.get("/check")
|
||||||
async def check_upgrade(request: Request):
|
async def check_upgrade(request: Request):
|
||||||
"""检查数据库版本是否需要升级"""
|
"""检查数据库版本是否需要升级"""
|
||||||
@@ -43,8 +51,6 @@ async def check_upgrade(request: Request):
|
|||||||
if not is_teacher:
|
if not is_teacher:
|
||||||
return error_response(message="仅班主任可执行升级操作", code=403)
|
return error_response(message="仅班主任可执行升级操作", code=403)
|
||||||
|
|
||||||
user_id = request.state.user.get('user_id') if hasattr(request.state, 'user') else getattr(request.state, 'user_id', None)
|
|
||||||
|
|
||||||
# 检测当前数据库版本
|
# 检测当前数据库版本
|
||||||
current_version = '0.0.0'
|
current_version = '0.0.0'
|
||||||
try:
|
try:
|
||||||
@@ -83,9 +89,32 @@ async def check_upgrade(request: Request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
async def _verify_upgrade(expected_version: str) -> dict:
|
||||||
|
"""验证升级结果:检查版本号是否已正确更新
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'ok': bool, 'message': str}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
row = await execute_query(
|
||||||
|
"SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'"
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return {'ok': False, 'message': 'db_version 记录不存在'}
|
||||||
|
actual = row[0]['setting_value']
|
||||||
|
if actual != expected_version:
|
||||||
|
return {'ok': False, 'message': f'版本号不匹配:期望 {expected_version},实际 {actual}'}
|
||||||
|
return {'ok': True, 'message': '验证通过'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'ok': False, 'message': f'验证查询失败: {str(e)}'}
|
||||||
|
|
||||||
|
|
||||||
|
MAX_RETRIES = 2
|
||||||
|
|
||||||
|
|
||||||
@router.post("/step")
|
@router.post("/step")
|
||||||
async def execute_upgrade_step(request: Request):
|
async def execute_upgrade_step(request: Request):
|
||||||
"""执行单个升级步骤"""
|
"""执行单个升级步骤(含验证与重试)"""
|
||||||
# 权限检查:仅班主任可执行升级操作
|
# 权限检查:仅班主任可执行升级操作
|
||||||
user_type = getattr(request.state, 'user_type', None)
|
user_type = getattr(request.state, 'user_type', None)
|
||||||
if user_type != 'admin':
|
if user_type != 'admin':
|
||||||
@@ -97,8 +126,6 @@ async def execute_upgrade_step(request: Request):
|
|||||||
if not is_teacher:
|
if not is_teacher:
|
||||||
return error_response(message="仅班主任可执行升级操作", code=403)
|
return error_response(message="仅班主任可执行升级操作", code=403)
|
||||||
|
|
||||||
user_id = request.state.user.get('user_id') if hasattr(request.state, 'user') else getattr(request.state, 'user_id', None)
|
|
||||||
|
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
version = body.get('version', '')
|
version = body.get('version', '')
|
||||||
|
|
||||||
@@ -115,17 +142,18 @@ async def execute_upgrade_step(request: Request):
|
|||||||
if not os.path.exists(sql_file):
|
if not os.path.exists(sql_file):
|
||||||
return error_response(message=f'SQL 文件不存在: {ALL_VERSIONS[version]}', code=500)
|
return error_response(message=f'SQL 文件不存在: {ALL_VERSIONS[version]}', code=500)
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_RETRIES + 1):
|
||||||
try:
|
try:
|
||||||
# 读取并执行 SQL
|
# 读取并执行 SQL
|
||||||
with open(sql_file, 'r', encoding='utf-8') as f:
|
with open(sql_file, 'r', encoding='utf-8') as f:
|
||||||
sql_content = f.read().strip()
|
sql_content = f.read().strip()
|
||||||
|
|
||||||
if sql_content and sql_content != '--':
|
if sql_content and sql_content != '--':
|
||||||
# 使用 aiomysql 直接执行多条 SQL
|
|
||||||
pool = get_pool()
|
pool = get_pool()
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
async with conn.cursor() as cursor:
|
async with conn.cursor() as cursor:
|
||||||
# 分割 SQL 语句(按 DELIMITER 处理存储过程)
|
|
||||||
await _execute_sql_content(cursor, sql_content)
|
await _execute_sql_content(cursor, sql_content)
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
@@ -136,19 +164,11 @@ async def execute_upgrade_step(request: Request):
|
|||||||
(version, version)
|
(version, version)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 重新检测版本
|
# 验证版本号是否正确写入
|
||||||
new_version = '0.0.0'
|
verify = await _verify_upgrade(version)
|
||||||
try:
|
if verify['ok']:
|
||||||
row = await execute_query(
|
new_version = version
|
||||||
"SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'"
|
|
||||||
)
|
|
||||||
if row:
|
|
||||||
new_version = row[0]['setting_value']
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
logger.info(f"数据库升级成功: v{version} ({ALL_VERSIONS[version]})")
|
logger.info(f"数据库升级成功: v{version} ({ALL_VERSIONS[version]})")
|
||||||
|
|
||||||
return success_response(data={
|
return success_response(data={
|
||||||
'success': True,
|
'success': True,
|
||||||
'version': version,
|
'version': version,
|
||||||
@@ -156,9 +176,24 @@ async def execute_upgrade_step(request: Request):
|
|||||||
'current': new_version
|
'current': new_version
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 验证失败,准备重试
|
||||||
|
last_error = f"升级验证失败: {verify['message']}"
|
||||||
|
if attempt < MAX_RETRIES:
|
||||||
|
logger.warning(f"v{version} 升级验证失败,准备第 {attempt + 1} 次重试: {last_error}")
|
||||||
|
continue
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"数据库升级失败: v{version} - {str(e)}")
|
last_error = str(e)
|
||||||
return error_response(message=f"升级至 v{version} 失败: {str(e)}", code=500)
|
logger.warning(f"v{version} 升级第 {attempt} 次失败: {last_error}")
|
||||||
|
if attempt < MAX_RETRIES:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 所有重试均失败
|
||||||
|
logger.error(f"数据库升级失败: v{version} (尝试 {MAX_RETRIES} 次) - {last_error}")
|
||||||
|
return error_response(
|
||||||
|
message=f"升级至 v{version} 失败 (尝试 {MAX_RETRIES} 次): {last_error}",
|
||||||
|
code=500
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _compare_versions(v1: str, v2: str) -> int:
|
def _compare_versions(v1: str, v2: str) -> int:
|
||||||
@@ -185,16 +220,29 @@ def _version_tuple(v: str) -> tuple:
|
|||||||
|
|
||||||
async def _execute_sql_content(cursor, sql_content: str):
|
async def _execute_sql_content(cursor, sql_content: str):
|
||||||
"""执行 SQL 内容,处理存储过程中的 DELIMITER"""
|
"""执行 SQL 内容,处理存储过程中的 DELIMITER"""
|
||||||
|
sql_content = sql_content.strip()
|
||||||
|
if not sql_content or sql_content == '--':
|
||||||
|
return # 空文件或纯注释,无需执行
|
||||||
|
|
||||||
# 如果包含 DELIMITER,需要特殊处理
|
# 如果包含 DELIMITER,需要特殊处理
|
||||||
if 'DELIMITER' in sql_content:
|
if 'DELIMITER' in sql_content.upper():
|
||||||
# 移除 DELIMITER 行,按 $$ 分割存储过程
|
|
||||||
lines = sql_content.split('\n')
|
lines = sql_content.split('\n')
|
||||||
current_block = []
|
current_block = []
|
||||||
in_procedure = False
|
in_procedure = False
|
||||||
|
buffer = '' # 使用局部变量而非函数属性,避免跨调用泄漏
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
|
# 跳过纯注释行
|
||||||
|
if stripped.startswith('--') or stripped.startswith('#'):
|
||||||
|
if not in_procedure:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
current_block.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
if stripped.upper().startswith('DELIMITER $$'):
|
if stripped.upper().startswith('DELIMITER $$'):
|
||||||
|
# 开始存储过程定义
|
||||||
in_procedure = True
|
in_procedure = True
|
||||||
current_block = []
|
current_block = []
|
||||||
continue
|
continue
|
||||||
@@ -203,27 +251,41 @@ async def _execute_sql_content(cursor, sql_content: str):
|
|||||||
if current_block:
|
if current_block:
|
||||||
proc_sql = '\n'.join(current_block).strip()
|
proc_sql = '\n'.join(current_block).strip()
|
||||||
if proc_sql:
|
if proc_sql:
|
||||||
|
# 移除存储过程结尾的 $$ 定界符(发送给 MySQL 服务器时不需要)
|
||||||
|
proc_sql = re.sub(r'\$\$\s*$', '', proc_sql)
|
||||||
await cursor.execute(proc_sql)
|
await cursor.execute(proc_sql)
|
||||||
in_procedure = False
|
in_procedure = False
|
||||||
current_block = []
|
current_block = []
|
||||||
continue
|
continue
|
||||||
elif stripped.upper().startswith('DELIMITER'):
|
elif stripped.upper().startswith('DELIMITER'):
|
||||||
|
# 其他 DELIMITER 指令,跳过
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if in_procedure:
|
if in_procedure:
|
||||||
current_block.append(line)
|
current_block.append(line)
|
||||||
else:
|
else:
|
||||||
# 普通SQL,按分号分割执行
|
# 普通SQL,按完整语句分割(以分号结尾)
|
||||||
if stripped and not stripped.startswith('--'):
|
if stripped:
|
||||||
# 简单的按分号分割
|
# 累积多行直到遇到分号
|
||||||
for stmt in stripped.split(';'):
|
if buffer:
|
||||||
stmt = stmt.strip()
|
buffer += ' ' + stripped
|
||||||
|
else:
|
||||||
|
buffer = stripped
|
||||||
|
|
||||||
|
# 如果以分号结尾,执行并清空缓冲区
|
||||||
|
if buffer.rstrip().endswith(';'):
|
||||||
|
stmt = buffer.rstrip(';').strip()
|
||||||
|
if stmt:
|
||||||
|
await cursor.execute(stmt)
|
||||||
|
buffer = ''
|
||||||
|
|
||||||
|
# 处理缓冲区中剩余的语句
|
||||||
|
if buffer:
|
||||||
|
stmt = buffer.rstrip(';').strip()
|
||||||
if stmt:
|
if stmt:
|
||||||
await cursor.execute(stmt)
|
await cursor.execute(stmt)
|
||||||
else:
|
else:
|
||||||
# 无 DELIMITER,简单执行
|
# 无 DELIMITER,按分号+换行分割语句
|
||||||
# 按 CREATE 分割以支持多语句
|
|
||||||
# 分割 SQL 语句
|
|
||||||
statements = re.split(r';\s*\n', sql_content)
|
statements = re.split(r';\s*\n', sql_content)
|
||||||
for stmt in statements:
|
for stmt in statements:
|
||||||
stmt = stmt.strip()
|
stmt = stmt.strip()
|
||||||
|
|||||||
@@ -88,9 +88,21 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
var upgradeSteps = [];
|
var upgradeSteps = [];
|
||||||
var currentStepIndex = 0;
|
var currentStepIndex = 0;
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (typeof str !== 'string') return '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/api/check_upgrade.php')
|
fetch('/api/check_upgrade.php')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
|
// 检查是否返回了错误
|
||||||
|
if (data.error) {
|
||||||
|
console.warn('升级检查失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.needs_upgrade) {
|
if (data.needs_upgrade) {
|
||||||
document.getElementById('currentDbVersion').textContent = data.current;
|
document.getElementById('currentDbVersion').textContent = data.current;
|
||||||
document.getElementById('targetDbVersion').textContent = data.target;
|
document.getElementById('targetDbVersion').textContent = data.target;
|
||||||
@@ -101,14 +113,16 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
for (var i = 0; i < upgradeSteps.length; i++) {
|
for (var i = 0; i < upgradeSteps.length; i++) {
|
||||||
listHtml += '<div style="display:flex;align-items:center;padding:8px 12px;margin:4px 0;border-radius:6px;font-size:13px;background:var(--color-hover);border-left:3px solid var(--color-border);" id="ustep-' + i + '">' +
|
listHtml += '<div style="display:flex;align-items:center;padding:8px 12px;margin:4px 0;border-radius:6px;font-size:13px;background:var(--color-hover);border-left:3px solid var(--color-border);" id="ustep-' + i + '">' +
|
||||||
'<span style="margin-right:8px;" id="ustep-icon-' + i + '">○</span>' +
|
'<span style="margin-right:8px;" id="ustep-icon-' + i + '">○</span>' +
|
||||||
'<span>升级至 v' + upgradeSteps[i].version + '</span>' +
|
'<span>升级至 v' + escapeHtml(upgradeSteps[i].version) + '</span>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
document.getElementById('upgradeStepsList').innerHTML = listHtml;
|
document.getElementById('upgradeStepsList').innerHTML = listHtml;
|
||||||
document.getElementById('upgradeModal').style.display = 'flex';
|
document.getElementById('upgradeModal').style.display = 'flex';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function() {});
|
.catch(function(err) {
|
||||||
|
console.warn('升级检查请求失败:', err);
|
||||||
|
});
|
||||||
|
|
||||||
window.startUpgrade = function() {
|
window.startUpgrade = function() {
|
||||||
var btn = document.getElementById('startUpgradeBtn');
|
var btn = document.getElementById('startUpgradeBtn');
|
||||||
@@ -196,7 +210,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
laterBtn.textContent = '关闭';
|
laterBtn.textContent = '关闭';
|
||||||
|
|
||||||
document.getElementById('upgradeResult').style.display = 'block';
|
document.getElementById('upgradeResult').style.display = 'block';
|
||||||
document.getElementById('upgradeResult').innerHTML = '<div style="background:var(--color-danger-light);border:1px solid #ffccc7;border-radius:6px;padding:12px;color:var(--color-danger-dark);font-size:13px;"><strong>升级失败:</strong>' + msg + '</div>';
|
document.getElementById('upgradeResult').innerHTML = '<div style="background:var(--color-danger-light);border:1px solid #ffccc7;border-radius:6px;padding:12px;color:var(--color-danger-dark);font-size:13px;"><strong>升级失败:</strong>' + escapeHtml(msg) + '</div>';
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -46,16 +46,24 @@ $apiResponse = curl_exec($ch);
|
|||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($httpCode !== 200 || empty($apiResponse)) {
|
if (empty($apiResponse)) {
|
||||||
echo json_encode(['error' => '无法连接升级服务']);
|
echo json_encode(['error' => '无法连接升级服务']);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = json_decode($apiResponse, true);
|
$result = json_decode($apiResponse, true);
|
||||||
if (!$result || !isset($result['success']) || !$result['success']) {
|
if (!$result) {
|
||||||
echo json_encode(['error' => $result['message'] ?? '升级检查失败']);
|
echo json_encode(['error' => '升级服务返回数据格式错误']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端返回非200时,尝试解析实际错误信息
|
||||||
|
if ($httpCode !== 200 || !isset($result['success']) || !$result['success']) {
|
||||||
|
$errorMsg = $result['message'] ?? ($result['error'] ?? '升级检查失败');
|
||||||
|
echo json_encode(['error' => $errorMsg]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转发后端返回的升级数据
|
// 转发后端返回的升级数据
|
||||||
echo json_encode($result['data']);
|
$data = $result['data'] ?? [];
|
||||||
|
echo json_encode($data);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ $apiResponse = curl_exec($ch);
|
|||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
if ($httpCode !== 200 || empty($apiResponse)) {
|
if (empty($apiResponse)) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
@@ -75,15 +75,28 @@ if ($httpCode !== 200 || empty($apiResponse)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = json_decode($apiResponse, true);
|
$result = json_decode($apiResponse, true);
|
||||||
if (!$result || !isset($result['success']) || !$result['success']) {
|
if (!$result) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'version' => $stepVersion,
|
'version' => $stepVersion,
|
||||||
'error' => $result['message'] ?? '升级失败'
|
'error' => '升级服务返回数据格式错误'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端返回非200或 success=false 时,提取实际错误信息
|
||||||
|
if ($httpCode !== 200 || !isset($result['success']) || !$result['success']) {
|
||||||
|
$errorMsg = $result['message'] ?? ($result['error'] ?? '升级失败');
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'version' => $stepVersion,
|
||||||
|
'error' => $errorMsg
|
||||||
]);
|
]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转发后端返回的数据
|
// 转发后端返回的数据
|
||||||
echo json_encode($result['data']);
|
$data = $result['data'] ?? [];
|
||||||
|
echo json_encode($data);
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL
|
|||||||
|
|
||||||
-- 初始化系统版本号
|
-- 初始化系统版本号
|
||||||
INSERT INTO `system_settings` (`setting_key`, `setting_value`)
|
INSERT INTO `system_settings` (`setting_key`, `setting_value`)
|
||||||
VALUES ('db_version', '2.2')
|
VALUES ('db_version', '2.3')
|
||||||
ON DUPLICATE KEY UPDATE `setting_value` = '2.2';
|
ON DUPLICATE KEY UPDATE `setting_value` = '2.3';
|
||||||
|
|
||||||
-- 控制台输出初始化结果(含版本号)
|
-- 控制台输出初始化结果(含版本号)
|
||||||
SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message;
|
SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message;
|
||||||
|
|||||||
106
sql/upgrades/v1.0.sql
Normal file
106
sql/upgrades/v1.0.sql
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.0 初始数据库结构
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明: 系统初始版本,创建核心表结构。
|
||||||
|
-- 包含: semesters, subjects, students, users,
|
||||||
|
-- admin_roles, conduct_records, attendance_records
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用 IF NOT EXISTS 实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
-- 学期表
|
||||||
|
CREATE TABLE IF NOT EXISTS `semesters` (
|
||||||
|
`semester_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`semester_name` VARCHAR(100) NOT NULL COMMENT '学期名称',
|
||||||
|
`start_date` DATE DEFAULT NULL COMMENT '学期开始日期',
|
||||||
|
`end_date` DATE DEFAULT NULL COMMENT '学期结束日期',
|
||||||
|
`is_active` TINYINT DEFAULT 0 COMMENT '是否为当前活跃学期',
|
||||||
|
`is_archived` TINYINT DEFAULT 0 COMMENT '是否已归档',
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 科目表
|
||||||
|
CREATE TABLE IF NOT EXISTS `subjects` (
|
||||||
|
`subject_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID',
|
||||||
|
`subject_name` VARCHAR(50) NOT NULL COMMENT '科目名称',
|
||||||
|
`is_active` TINYINT DEFAULT 1 COMMENT '是否启用',
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `uk_subject_name` (`subject_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 学生表
|
||||||
|
CREATE TABLE IF NOT EXISTS `students` (
|
||||||
|
`student_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`student_no` VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
`name` VARCHAR(50) NOT NULL,
|
||||||
|
`total_points` INT DEFAULT 60,
|
||||||
|
`parent_phone` VARCHAR(20) DEFAULT NULL,
|
||||||
|
`status` TINYINT DEFAULT 1,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
|
`user_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
`password_hash` VARCHAR(64) NOT NULL,
|
||||||
|
`real_name` VARCHAR(50) NOT NULL,
|
||||||
|
`user_type` ENUM('student', 'parent', 'admin') NOT NULL,
|
||||||
|
`student_id` INT DEFAULT NULL,
|
||||||
|
`status` TINYINT DEFAULT 1,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 管理员角色表
|
||||||
|
CREATE TABLE IF NOT EXISTS `admin_roles` (
|
||||||
|
`admin_role_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`user_id` INT NOT NULL,
|
||||||
|
`role_type` ENUM('班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员') NOT NULL,
|
||||||
|
`subject_id` INT DEFAULT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY `uk_user_subject` (`user_id`, `subject_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 操行分记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS `conduct_records` (
|
||||||
|
`record_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`student_id` INT NOT NULL,
|
||||||
|
`points_change` INT NOT NULL,
|
||||||
|
`reason` VARCHAR(255) NOT NULL,
|
||||||
|
`recorder_id` INT NOT NULL,
|
||||||
|
`recorder_name` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`related_id` INT DEFAULT NULL,
|
||||||
|
`is_revoked` TINYINT DEFAULT 0,
|
||||||
|
`revoked_by` INT DEFAULT NULL,
|
||||||
|
`revoked_at` DATETIME DEFAULT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
|
||||||
|
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 考勤记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS `attendance_records` (
|
||||||
|
`attendance_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`student_id` INT NOT NULL,
|
||||||
|
`date` DATE NOT NULL,
|
||||||
|
`slot` ENUM('morning', 'afternoon', 'evening') DEFAULT 'morning' COMMENT '考勤时段',
|
||||||
|
`status` ENUM('present', 'absent', 'late', 'leave') DEFAULT 'present',
|
||||||
|
`reason` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`recorder_id` INT NOT NULL,
|
||||||
|
`deduction_applied` TINYINT DEFAULT 0,
|
||||||
|
`deduction_record_id` BIGINT DEFAULT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
|
||||||
|
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY `uk_student_date_slot` (`student_id`, `date`, `slot`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 插入初始科目
|
||||||
|
INSERT IGNORE INTO `subjects` (`subject_name`) VALUES ('语文'), ('数学'), ('英语');
|
||||||
97
sql/upgrades/v1.1.sql
Normal file
97
sql/upgrades/v1.1.sql
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.0 → v1.1 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 变更内容:
|
||||||
|
-- 1. users 表添加 need_change_password 列
|
||||||
|
-- 2. users 表添加 last_login_time 列
|
||||||
|
-- 3. users 表添加 last_login_ip 列
|
||||||
|
-- 4. users 表 password_hash 字段扩展至 VARCHAR(255)
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用存储过程实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
-- 升级步骤 1: users 添加 need_change_password 列
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `upgrade_step`()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'need_change_password'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN `need_change_password` TINYINT DEFAULT 1
|
||||||
|
AFTER `status`;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `upgrade_step`();
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
|
||||||
|
-- 升级步骤 2: users 添加 last_login_time 列
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `upgrade_step`()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'last_login_time'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN `last_login_time` DATETIME DEFAULT NULL
|
||||||
|
AFTER `need_change_password`;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `upgrade_step`();
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
|
||||||
|
-- 升级步骤 3: users 添加 last_login_ip 列
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `upgrade_step`()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'last_login_ip'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN `last_login_ip` VARCHAR(45) DEFAULT NULL
|
||||||
|
AFTER `last_login_time`;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `upgrade_step`();
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
|
||||||
|
-- 升级步骤 4: password_hash 字段扩展至 VARCHAR(255)
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `upgrade_step`()
|
||||||
|
BEGIN
|
||||||
|
DECLARE v_col_len INT;
|
||||||
|
SELECT CHARACTER_MAXIMUM_LENGTH INTO v_col_len
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'users'
|
||||||
|
AND COLUMN_NAME = 'password_hash';
|
||||||
|
|
||||||
|
IF v_col_len < 255 THEN
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY COLUMN `password_hash` VARCHAR(255) NOT NULL;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `upgrade_step`();
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
41
sql/upgrades/v1.2.sql
Normal file
41
sql/upgrades/v1.2.sql
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.1 → v1.2 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 变更内容:
|
||||||
|
-- 1. 创建 assignments(作业表)
|
||||||
|
-- 2. 创建 homework_submissions(作业提交记录表)
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用 IF NOT EXISTS 实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
-- 升级步骤 1: 创建作业表
|
||||||
|
CREATE TABLE IF NOT EXISTS `assignments` (
|
||||||
|
`assignment_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`subject_id` INT NOT NULL,
|
||||||
|
`title` VARCHAR(100) NOT NULL,
|
||||||
|
`description` TEXT,
|
||||||
|
`deadline` DATE NOT NULL,
|
||||||
|
`created_by` INT NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`),
|
||||||
|
FOREIGN KEY (`created_by`) REFERENCES `users`(`user_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 升级步骤 2: 创建作业提交记录表
|
||||||
|
CREATE TABLE IF NOT EXISTS `homework_submissions` (
|
||||||
|
`submission_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`assignment_id` INT NOT NULL,
|
||||||
|
`student_id` INT NOT NULL,
|
||||||
|
`status` ENUM('submitted', 'not_submitted', 'late') DEFAULT 'not_submitted',
|
||||||
|
`submit_time` DATETIME DEFAULT NULL,
|
||||||
|
`comments` TEXT,
|
||||||
|
`deduction_applied` TINYINT DEFAULT 0,
|
||||||
|
`deduction_record_id` BIGINT DEFAULT NULL,
|
||||||
|
`updated_by` INT DEFAULT NULL,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`assignment_id`) REFERENCES `assignments`(`assignment_id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
|
||||||
|
UNIQUE KEY `uk_assignment_student` (`assignment_id`, `student_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
22
sql/upgrades/v1.3.sql
Normal file
22
sql/upgrades/v1.3.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.2 → v1.3 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 变更内容:
|
||||||
|
-- 1. 创建 operation_logs(操作日志表)
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用 IF NOT EXISTS 实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `operation_logs` (
|
||||||
|
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`operator_id` INT NOT NULL,
|
||||||
|
`operator_name` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`operator_role` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`operation_type` VARCHAR(50) NOT NULL,
|
||||||
|
`target_type` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`target_id` INT DEFAULT NULL,
|
||||||
|
`details` TEXT,
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL,
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
19
sql/upgrades/v1.4.sql
Normal file
19
sql/upgrades/v1.4.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.3 → v1.4 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 变更内容:
|
||||||
|
-- 1. 创建 login_logs(登录日志表)
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用 IF NOT EXISTS 实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `login_logs` (
|
||||||
|
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(50) NOT NULL,
|
||||||
|
`login_result` TINYINT NOT NULL,
|
||||||
|
`fail_reason` VARCHAR(100) DEFAULT NULL,
|
||||||
|
`ip_address` VARCHAR(45) DEFAULT NULL,
|
||||||
|
`user_agent` VARCHAR(255) DEFAULT NULL,
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
30
sql/upgrades/v1.5.sql
Normal file
30
sql/upgrades/v1.5.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.4 → v1.5 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 变更内容:
|
||||||
|
-- 1. 创建 semester_archives(学期归档快照表)
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用 IF NOT EXISTS 实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `semester_archives` (
|
||||||
|
`archive_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`semester_id` INT NOT NULL,
|
||||||
|
`student_id` INT NOT NULL,
|
||||||
|
`student_no` VARCHAR(20) NOT NULL,
|
||||||
|
`student_name` VARCHAR(50) NOT NULL,
|
||||||
|
`final_points` INT NOT NULL COMMENT '学期最终操行分',
|
||||||
|
`rank_position` INT DEFAULT NULL COMMENT '排名',
|
||||||
|
`total_students` INT DEFAULT NULL COMMENT '班级总人数',
|
||||||
|
`attendance_present` INT DEFAULT 0 COMMENT '出勤次数',
|
||||||
|
`attendance_absent` INT DEFAULT 0 COMMENT '缺勤次数',
|
||||||
|
`attendance_late` INT DEFAULT 0 COMMENT '迟到次数',
|
||||||
|
`attendance_leave` INT DEFAULT 0 COMMENT '请假次数',
|
||||||
|
`homework_submitted` INT DEFAULT 0 COMMENT '已交作业数',
|
||||||
|
`homework_not_submitted` INT DEFAULT 0 COMMENT '未交作业数',
|
||||||
|
`homework_late` INT DEFAULT 0 COMMENT '迟交作业数',
|
||||||
|
`archived_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`),
|
||||||
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
63
sql/upgrades/v1.6.sql
Normal file
63
sql/upgrades/v1.6.sql
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v1.5 → v1.6 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 变更内容:
|
||||||
|
-- 1. subjects 表添加 subject_code 列(科目代码)
|
||||||
|
-- 2. subjects 表添加 sort_order 列(排序序号)
|
||||||
|
-- 3. 更新已有科目的 subject_code
|
||||||
|
--
|
||||||
|
-- 兼容性: 使用存储过程实现幂等,phpMyAdmin 可直接执行
|
||||||
|
-- ===========================================
|
||||||
|
|
||||||
|
-- 升级步骤 1: subjects 添加 subject_code 列
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `upgrade_step`()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'subjects'
|
||||||
|
AND COLUMN_NAME = 'subject_code'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `subjects`
|
||||||
|
ADD COLUMN `subject_code` VARCHAR(20) DEFAULT NULL COMMENT '科目代码'
|
||||||
|
AFTER `subject_name`;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `upgrade_step`();
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
|
||||||
|
-- 升级步骤 2: subjects 添加 sort_order 列
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE `upgrade_step`()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'subjects'
|
||||||
|
AND COLUMN_NAME = 'sort_order'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE `subjects`
|
||||||
|
ADD COLUMN `sort_order` INT DEFAULT 0 COMMENT '排序序号'
|
||||||
|
AFTER `is_active`;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL `upgrade_step`();
|
||||||
|
DROP PROCEDURE IF EXISTS `upgrade_step`;
|
||||||
|
|
||||||
|
-- 升级步骤 3: 更新已有科目的 subject_code
|
||||||
|
UPDATE `subjects` SET `subject_code` = 'CHI' WHERE `subject_name` = '语文' AND (`subject_code` IS NULL OR `subject_code` = '');
|
||||||
|
UPDATE `subjects` SET `subject_code` = 'MATH' WHERE `subject_name` = '数学' AND (`subject_code` IS NULL OR `subject_code` = '');
|
||||||
|
UPDATE `subjects` SET `subject_code` = 'ENG' WHERE `subject_name` = '英语' AND (`subject_code` IS NULL OR `subject_code` = '');
|
||||||
|
|
||||||
|
-- 升级步骤 4: 设置默认排序
|
||||||
|
UPDATE `subjects` SET `sort_order` = 1 WHERE `subject_name` = '语文' AND `sort_order` = 0;
|
||||||
|
UPDATE `subjects` SET `sort_order` = 2 WHERE `subject_name` = '数学' AND `sort_order` = 0;
|
||||||
|
UPDATE `subjects` SET `sort_order` = 3 WHERE `subject_name` = '英语' AND `sort_order` = 0;
|
||||||
11
sql/upgrades/v2.3.sql
Normal file
11
sql/upgrades/v2.3.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v2.2 → v2.3 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明: v2.3 为升级系统优化版本,无数据库 schema 变更。
|
||||||
|
-- 主要变更:
|
||||||
|
-- 1. 升级系统全面重构(错误处理、SQL 解析、XSS 防护)
|
||||||
|
-- 2. 新增升级验证 + 自动重试 + 失败回滚机制
|
||||||
|
-- 3. 补全 v1.0-v1.6 增量升级脚本
|
||||||
|
-- 4. 修复 DELIMITER SQL 执行问题
|
||||||
|
-- ===========================================
|
||||||
193
upgrade.php
193
upgrade.php
@@ -12,12 +12,20 @@
|
|||||||
|
|
||||||
// 版本升级列表(唯一数据源)
|
// 版本升级列表(唯一数据源)
|
||||||
$UPGRADE_VERSIONS = [
|
$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.7' => __DIR__ . '/sql/upgrades/v1.7.sql',
|
||||||
'1.8' => __DIR__ . '/sql/upgrades/v1.8.sql',
|
'1.8' => __DIR__ . '/sql/upgrades/v1.8.sql',
|
||||||
'2.0' => __DIR__ . '/sql/upgrades/v2.0.sql',
|
'2.0' => __DIR__ . '/sql/upgrades/v2.0.sql',
|
||||||
'2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql',
|
'2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql',
|
||||||
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
||||||
'2.2' => __DIR__ . '/sql/upgrades/v2.2.sql',
|
'2.2' => __DIR__ . '/sql/upgrades/v2.2.sql',
|
||||||
|
'2.3' => __DIR__ . '/sql/upgrades/v2.3.sql',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,19 +92,146 @@ 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)) {
|
if (!file_exists($sqlFile)) {
|
||||||
throw new RuntimeException("SQL 文件不存在: {$sqlFile}");
|
throw new RuntimeException("SQL 文件不存在: {$sqlFile}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql = file_get_contents($sqlFile);
|
$sql = file_get_contents($sqlFile);
|
||||||
|
$isEmpty = (trim($sql) === '' || trim($sql) === '--');
|
||||||
|
|
||||||
if (trim($sql) === '' || trim($sql) === '--') {
|
$lastError = null;
|
||||||
// 空文件或纯注释,无需执行 SQL,仅更新版本号
|
|
||||||
} else {
|
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
|
||||||
$pdo->exec($sql);
|
try {
|
||||||
|
if (!$isEmpty) {
|
||||||
|
executeSqlContent($pdo, $sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新版本号(使用预处理语句防止 SQL 注入)
|
// 更新版本号(使用预处理语句防止 SQL 注入)
|
||||||
@@ -105,6 +240,41 @@ function executeUpgrade($pdo, $version, $sqlFile) {
|
|||||||
ON DUPLICATE KEY UPDATE setting_value = :version"
|
ON DUPLICATE KEY UPDATE setting_value = :version"
|
||||||
);
|
);
|
||||||
$stmt->execute([':version' => $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}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================
|
// ===========================================
|
||||||
@@ -468,6 +638,13 @@ try {
|
|||||||
|
|
||||||
<?php if (!$hasError && !$isUpToDate): ?>
|
<?php if (!$hasError && !$isUpToDate): ?>
|
||||||
<script>
|
<script>
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (typeof str !== 'string') return '';
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
function executeUpgrade() {
|
function executeUpgrade() {
|
||||||
var btn = document.getElementById('btnUpgrade');
|
var btn = document.getElementById('btnUpgrade');
|
||||||
var resultArea = document.getElementById('resultArea');
|
var resultArea = document.getElementById('resultArea');
|
||||||
@@ -512,7 +689,7 @@ try {
|
|||||||
btn.textContent = '升级失败';
|
btn.textContent = '升级失败';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
resultArea.style.display = 'block';
|
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) {
|
.catch(function(err) {
|
||||||
@@ -525,7 +702,7 @@ try {
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '立即升级';
|
btn.textContent = '立即升级';
|
||||||
resultArea.style.display = 'block';
|
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