v2.2更新

This commit is contained in:
2026-05-28 15:38:32 +08:00
parent f84c9d3efb
commit ca53fdc349
38 changed files with 688 additions and 686 deletions

2
.gitignore vendored
View File

@@ -52,5 +52,5 @@ docs/guide/student.pdf
docs/guide/teacher.pdf docs/guide/teacher.pdf
qrcode.png qrcode.png
# 展示内容 # example
example example

View File

@@ -272,7 +272,7 @@ classmanager/
| v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php、清理废弃全局变量、角色权限表精确化 | | v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php、清理废弃全局变量、角色权限表精确化 |
| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分manual/homework/attendance、学生端作业详情优化 | | v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分manual/homework/attendance、学生端作业详情优化 |
| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 | | v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 |
| v2.1 | 2025.7.14 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 | | v2.1 | 2026.5.26 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 |
## 许可证 ## 许可证

View File

@@ -1 +1 @@
2.1 2.2

View File

@@ -21,7 +21,7 @@ from utils.logger import setup_logger, log_access
from utils.database import init_db_pool, close_db_pool from utils.database import init_db_pool, close_db_pool
from utils.redis_client import init_redis_pool, close_redis_pool from utils.redis_client import init_redis_pool, close_redis_pool
from middleware.auth_middleware import AuthMiddleware from middleware.auth_middleware import AuthMiddleware
from routes import auth, student, parent, admin, subject, semester, debug from routes import auth, student, parent, admin, subject, semester, debug, upgrade
from routes.config import router as config_router from routes.config import router as config_router
@@ -119,6 +119,7 @@ app.include_router(admin.router, prefix="/api/admin", tags=["管理端"])
app.include_router(subject.router, prefix="/api/subject", tags=["科目管理"]) app.include_router(subject.router, prefix="/api/subject", tags=["科目管理"])
app.include_router(semester.router, prefix="/api/semester", tags=["学期管理"]) app.include_router(semester.router, prefix="/api/semester", tags=["学期管理"])
app.include_router(config_router, prefix="/api/config", tags=["配置"]) app.include_router(config_router, prefix="/api/config", tags=["配置"])
app.include_router(upgrade.router, prefix="/api/upgrade", tags=["升级管理"])
app.include_router(debug.router, tags=["调试"]) app.include_router(debug.router, tags=["调试"])

View File

@@ -209,6 +209,7 @@ class ConductModel:
student_id: int = None, student_id: int = None,
start_date: str = None, start_date: str = None,
end_date: str = None, end_date: str = None,
related_type: str = None,
page: int = 1, page: int = 1,
page_size: int = 20 page_size: int = 20
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -217,6 +218,8 @@ class ConductModel:
start_date = None start_date = None
if end_date == "": if end_date == "":
end_date = None end_date = None
if related_type == "":
related_type = None
conditions = ["cr.is_revoked = 0"] conditions = ["cr.is_revoked = 0"]
params = [] params = []
@@ -230,6 +233,9 @@ class ConductModel:
if end_date: if end_date:
conditions.append("cr.created_at <= %s") conditions.append("cr.created_at <= %s")
params.append(end_date + ' 23:59:59') params.append(end_date + ' 23:59:59')
if related_type:
conditions.append("cr.related_type = %s")
params.append(related_type)
where_clause = " AND ".join(conditions) where_clause = " AND ".join(conditions)

View File

@@ -1,117 +0,0 @@
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
from typing import Optional, Dict, Any, List
from utils.database import execute_one, execute_query, execute_insert, execute_update
class HomeworkModel:
"""作业数据模型"""
@staticmethod
async def get_all_assignments() -> List[Dict[str, Any]]:
sql = """
SELECT a.*, s.subject_name, u.real_name as created_by_name
FROM assignments a
JOIN subjects s ON a.subject_id = s.subject_id
JOIN users u ON a.created_by = u.user_id
ORDER BY a.deadline ASC, a.created_at DESC
"""
return await execute_query(sql)
@staticmethod
async def get_assignments_by_subjects(subject_ids: List[int]) -> List[Dict[str, Any]]:
if not subject_ids:
return []
placeholders = ','.join(['%s'] * len(subject_ids))
sql = f"""
SELECT a.*, s.subject_name, u.real_name as created_by_name
FROM assignments a
JOIN subjects s ON a.subject_id = s.subject_id
JOIN users u ON a.created_by = u.user_id
WHERE a.subject_id IN ({placeholders})
ORDER BY a.deadline ASC, a.created_at DESC
"""
return await execute_query(sql, tuple(subject_ids))
@staticmethod
async def get_student_homework(student_id: int) -> List[Dict[str, Any]]:
sql = """
SELECT a.assignment_id, a.title, a.description, a.deadline, a.created_at,
s.subject_name, hs.status, hs.submit_time, hs.comments, hs.deduction_applied,
cr.points_change AS points
FROM assignments a
JOIN subjects s ON a.subject_id = s.subject_id
LEFT JOIN homework_submissions hs ON a.assignment_id = hs.assignment_id AND hs.student_id = %s
LEFT JOIN conduct_records cr ON cr.related_type = 'homework'
AND cr.related_id = a.assignment_id AND cr.student_id = %s AND cr.is_revoked = 0
GROUP BY a.assignment_id
ORDER BY a.deadline ASC, a.created_at DESC
"""
return await execute_query(sql, (student_id, student_id))
@staticmethod
async def get_submission(submission_id: int) -> Optional[Dict[str, Any]]:
sql = """
SELECT hs.*, a.title, a.subject_id, a.assignment_id, s.name as student_name
FROM homework_submissions hs
JOIN assignments a ON hs.assignment_id = a.assignment_id
JOIN students s ON hs.student_id = s.student_id
WHERE hs.submission_id = %s
"""
return await execute_one(sql, (submission_id,))
@staticmethod
async def create_assignment(
subject_id: int,
title: str,
description: str,
deadline: str,
created_by: int
) -> int:
sql = """
INSERT INTO assignments (subject_id, title, description, deadline, created_by)
VALUES (%s, %s, %s, %s, %s)
"""
assignment_id = await execute_insert(sql, (subject_id, title, description, deadline, created_by))
# 为所有学生创建提交记录
from models.student import StudentModel
students = await StudentModel.get_all(include_disabled=False)
for student in students:
sql_sub = """
INSERT INTO homework_submissions (assignment_id, student_id, status)
VALUES (%s, %s, 'not_submitted')
"""
await execute_insert(sql_sub, (assignment_id, student["student_id"]))
return assignment_id
@staticmethod
async def update_submission(
submission_id: int,
status: str,
comments: str = None,
updated_by: int = None
) -> bool:
sql = """
UPDATE homework_submissions
SET status = %s, comments = %s, updated_by = %s, updated_at = NOW()
WHERE submission_id = %s
"""
result = await execute_update(sql, (status, comments, updated_by, submission_id))
return result > 0
@staticmethod
async def mark_deduction_applied(submission_id: int) -> bool:
sql = "UPDATE homework_submissions SET deduction_applied = 1 WHERE submission_id = %s"
result = await execute_update(sql, (submission_id,))
return result > 0

View File

@@ -53,6 +53,18 @@ class StudentModel:
sql += " ORDER BY student_no" sql += " ORDER BY student_no"
return await execute_query(sql) return await execute_query(sql)
@staticmethod
async def get_dormitory_list() -> List[str]:
"""获取所有不重复的宿舍号列表"""
sql = """
SELECT DISTINCT dormitory_number
FROM students
WHERE status = 1 AND dormitory_number IS NOT NULL AND dormitory_number != ''
ORDER BY dormitory_number
"""
rows = await execute_query(sql)
return [row["dormitory_number"] for row in rows]
@staticmethod @staticmethod
async def create( async def create(
student_no: str, student_no: str,

View File

@@ -20,16 +20,15 @@ from middleware.permission import (
) )
from services.admin_service import AdminService from services.admin_service import AdminService
from services.conduct_service import ConductService from services.conduct_service import ConductService
from services.homework_service import HomeworkService
from services.attendance_service import AttendanceService from services.attendance_service import AttendanceService
from services.log_service import LogService from services.log_service import LogService
from utils.redis_client import RedisClient from utils.redis_client import RedisClient
from schemas.admin import ( from schemas.admin import (
AddPointsRequest, RevokeRequest, AddAdminRequest, AddPointsRequest, RevokeRequest, AddAdminRequest,
AddStudentRequest, UpdateStudentRequest, AddStudentRequest, UpdateStudentRequest,
UpdateHomeworkStatusRequest, AddAttendanceRequest, AddAttendanceRequest,
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest, UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
CreateAssignmentRequest, UnlockUserRequest UnlockUserRequest
) )
from utils.response import success_response, error_response from utils.response import success_response, error_response
from utils.logger import get_logger from utils.logger import get_logger
@@ -41,18 +40,31 @@ logger = get_logger(__name__)
# ========== 学生管理 ========== # ========== 学生管理 ==========
@router.get("/students/dormitories")
async def get_dormitory_list(request: Request):
"""获取宿舍号列表"""
user = await get_current_user(request)
if user["user_type"] != "admin":
return error_response(message="仅管理员可查看", code=403)
from models.student import StudentModel
dormitories = await StudentModel.get_dormitory_list()
return success_response(data={"dormitories": dormitories})
@router.get("/students") @router.get("/students")
async def get_students( async def get_students(
request: Request, request: Request,
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=1000), page_size: int = Query(20, ge=1, le=1000),
search: Optional[str] = None search: Optional[str] = None,
dormitory_number: Optional[str] = None
): ):
"""获取所有学生列表(单班级)""" """获取所有学生列表(单班级)"""
user = await get_current_user(request) user = await get_current_user(request)
if user["user_type"] != "admin": if user["user_type"] != "admin":
return error_response(message="仅管理员可查看学生列表", code=403) return error_response(message="仅管理员可查看学生列表", code=403)
result = await AdminService.get_students(page=page, page_size=page_size, search=search) result = await AdminService.get_students(page=page, page_size=page_size, search=search, dormitory_number=dormitory_number)
return success_response(data=result) return success_response(data=result)
@@ -139,7 +151,8 @@ async def update_student(request: Request, student_id: int, req: UpdateStudentRe
result = await AdminService.update_student( result = await AdminService.update_student(
student_id=student_id, student_id=student_id,
name=req.name, name=req.name,
parent_phone=req.parent_phone parent_phone=req.parent_phone,
dormitory_number=req.dormitory_number
) )
if result["success"]: if result["success"]:
await LogService.write_operation_log( await LogService.write_operation_log(
@@ -207,6 +220,9 @@ async def reset_student_password(request: Request, student_id: int, req: ResetPa
async def add_conduct_points(request: Request, req: AddPointsRequest): async def add_conduct_points(request: Request, req: AddPointsRequest):
"""批量加减分""" """批量加减分"""
user = await get_current_user(request) user = await get_current_user(request)
# 仅管理员(班主任/班干部)可操作
if user["user_type"] != "admin":
return error_response(message="无权进行此操作", code=403)
result = await ConductService.add_points( result = await ConductService.add_points(
student_ids=req.student_ids, student_ids=req.student_ids,
points_change=req.points_change, points_change=req.points_change,
@@ -236,6 +252,9 @@ async def add_conduct_points(request: Request, req: AddPointsRequest):
async def revoke_conduct_record(request: Request, req: RevokeRequest): async def revoke_conduct_record(request: Request, req: RevokeRequest):
"""撤销扣分记录""" """撤销扣分记录"""
user = await get_current_user(request) user = await get_current_user(request)
# 仅管理员(班主任/班干部)可操作
if user["user_type"] != "admin":
return error_response(message="无权进行此操作", code=403)
result = await ConductService.revoke_record( result = await ConductService.revoke_record(
record_id=req.record_id, record_id=req.record_id,
revoker_id=user["user_id"] revoker_id=user["user_id"]
@@ -264,6 +283,9 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
async def restore_conduct_record(request: Request, req: RevokeRequest): async def restore_conduct_record(request: Request, req: RevokeRequest):
"""反撤销(恢复)已撤销的记录""" """反撤销(恢复)已撤销的记录"""
user = await get_current_user(request) user = await get_current_user(request)
# 仅管理员(班主任/班干部)可操作
if user["user_type"] != "admin":
return error_response(message="无权进行此操作", code=403)
result = await ConductService.restore_record( result = await ConductService.restore_record(
record_id=req.record_id, record_id=req.record_id,
restorer_id=user["user_id"] restorer_id=user["user_id"]
@@ -319,86 +341,6 @@ async def get_conduct_history(
return error_response(message=f"获取历史记录失败: {str(e)}") return error_response(message=f"获取历史记录失败: {str(e)}")
# ========== 作业管理 ==========
@router.get("/homework/assignments")
async def get_assignments(request: Request):
"""获取作业列表"""
user = await get_current_user(request)
role = await PermissionChecker.get_user_role(user["user_id"])
if role not in ["班主任", "学习委员"]:
return error_response(message="无权限", code=403)
result = await HomeworkService.get_assignments(user["user_id"])
return success_response(data=result)
@router.get("/homework/submissions/{assignment_id}")
async def get_submissions(request: Request, assignment_id: int):
"""获取作业提交记录"""
user = await get_current_user(request)
role = await PermissionChecker.get_user_role(user["user_id"])
if role not in ["班主任", "学习委员"]:
return error_response(message="无权限", code=403)
result = await HomeworkService.get_submissions(
assignment_id=assignment_id,
user_id=user["user_id"]
)
return success_response(data=result)
@router.post("/homework/assignment")
async def create_assignment(request: Request, req: CreateAssignmentRequest):
"""发布作业(班主任)"""
user = await get_current_user(request)
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
if not is_teacher:
return error_response(message="仅班主任可发布作业", code=403)
result = await HomeworkService.create_assignment(
subject_id=req.subject_id,
title=req.title,
description=req.description,
deadline=req.deadline,
created_by=user["user_id"]
)
if result["success"]:
await LogService.write_operation_log(
operator_id=user["user_id"], operator_name=user["real_name"],
operator_role="班主任", operation_type="create_assignment",
target_type="homework",
details=f"发布作业: {title}",
ip=request.client.host
)
return success_response(data=result, message="作业发布成功")
else:
return error_response(message=result["message"])
@router.put("/homework/submission")
async def update_submission_status(request: Request, req: UpdateHomeworkStatusRequest):
"""更新作业提交状态(班主任或学习委员)"""
user = await get_current_user(request)
role = await PermissionChecker.get_user_role(user["user_id"])
if role not in ["班主任", "学习委员"]:
return error_response(message="无权进行此操作", code=403)
result = await HomeworkService.update_submission_status(
submission_id=req.submission_id,
status=req.status,
comments=req.comments,
apply_deduction=req.apply_deduction,
operator_id=user["user_id"]
)
if result["success"]:
await LogService.write_operation_log(
operator_id=user["user_id"], operator_name=user["real_name"],
operator_role=role, operation_type="update_submission",
target_type="homework", target_id=req.submission_id,
details=f"状态: {req.status}",
ip=request.client.host
)
return success_response(message="状态更新成功")
else:
return error_response(message=result["message"])
# ========== 考勤管理 ========== # ========== 考勤管理 ==========

View File

@@ -36,21 +36,6 @@ async def get_child_conduct(request: Request):
return success_response(data=result) return success_response(data=result)
@router.get("/child/homework")
async def get_child_homework(request: Request):
"""
获取子女作业情况
"""
user = await get_current_user(request)
if user["user_type"] != "parent":
return error_response(message="仅限家长访问", code=403)
result = await ParentService.get_child_homework(user["user_id"])
return success_response(data=result)
@router.get("/child/attendance") @router.get("/child/attendance")
async def get_child_attendance(request: Request): async def get_child_attendance(request: Request):
""" """

231
backend/routes/upgrade.py Normal file
View File

@@ -0,0 +1,231 @@
# ===========================================
# 班级操行分管理系统 - 升级管理路由
#
# 开发者: Canglan
# 版权归属: Sea Network Technology Studio
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
from fastapi import APIRouter, Request
from utils.database import execute_query, execute_update, get_pool
from utils.response import success_response, error_response
from utils.logger import setup_logger
from middleware.permission import PermissionChecker
import os
import re
logger = setup_logger()
router = APIRouter()
# 版本列表(按顺序)
ALL_VERSIONS = {
'1.7': 'v1.7.sql',
'1.8': 'v1.8.sql',
'2.0': 'v2.0.sql',
'2.0.1': 'v2.0.1.sql',
'2.1': 'v2.1.sql',
'2.2': 'v2.2.sql',
}
@router.get("/check")
async def check_upgrade(request: Request):
"""检查数据库版本是否需要升级"""
# 权限检查:仅班主任可执行升级操作
user_type = getattr(request.state, 'user_type', None)
if user_type != 'admin':
return error_response(message="仅管理员可执行升级操作", code=403)
is_teacher = await PermissionChecker.check_is_teacher(
getattr(request.state, 'user_id', 0)
)
if not is_teacher:
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'
try:
row = await execute_query(
"SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'"
)
if row:
current_version = row[0]['setting_value']
except Exception:
pass # 表不存在时使用默认值
# 读取目标版本(从 VERSION 文件)
version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'VERSION')
version_file = os.path.normpath(version_file)
target_version = '0.0.0'
try:
if os.path.exists(version_file):
with open(version_file, 'r') as f:
target_version = f.read().strip()
except Exception:
pass
# 计算需要升级的步骤
needs_upgrade = _compare_versions(target_version, current_version) > 0
steps = []
for version, file_name in sorted(ALL_VERSIONS.items(), key=lambda x: _version_tuple(x[0])):
if _compare_versions(version, current_version) > 0 and _compare_versions(version, target_version) <= 0:
steps.append({'version': version, 'file': file_name})
return success_response(data={
'needs_upgrade': needs_upgrade,
'current': current_version,
'target': target_version,
'steps': steps
})
@router.post("/step")
async def execute_upgrade_step(request: Request):
"""执行单个升级步骤"""
# 权限检查:仅班主任可执行升级操作
user_type = getattr(request.state, 'user_type', None)
if user_type != 'admin':
return error_response(message="仅管理员可执行升级操作", code=403)
is_teacher = await PermissionChecker.check_is_teacher(
getattr(request.state, 'user_id', 0)
)
if not is_teacher:
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()
version = body.get('version', '')
if not version:
return error_response(message='缺少版本号参数', code=400)
if version not in ALL_VERSIONS:
return error_response(message=f'未知版本: {version}', code=400)
# SQL 文件路径
sql_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'sql', 'upgrades')
sql_file = os.path.normpath(os.path.join(sql_dir, ALL_VERSIONS[version]))
if not os.path.exists(sql_file):
return error_response(message=f'SQL 文件不存在: {ALL_VERSIONS[version]}', code=500)
try:
# 读取并执行 SQL
with open(sql_file, 'r', encoding='utf-8') as f:
sql_content = f.read().strip()
if sql_content and sql_content != '--':
# 使用 aiomysql 直接执行多条 SQL
pool = get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cursor:
# 分割 SQL 语句(按 DELIMITER 处理存储过程)
await _execute_sql_content(cursor, sql_content)
await conn.commit()
# 更新版本号
await execute_update(
"INSERT INTO system_settings (setting_key, setting_value) VALUES ('db_version', %s) "
"ON DUPLICATE KEY UPDATE setting_value = %s",
(version, version)
)
# 重新检测版本
new_version = '0.0.0'
try:
row = await execute_query(
"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]})")
return success_response(data={
'success': True,
'version': version,
'message': f"升级至 v{version} 成功 ({ALL_VERSIONS[version]})",
'current': new_version
})
except Exception as e:
logger.error(f"数据库升级失败: v{version} - {str(e)}")
return error_response(message=f"升级至 v{version} 失败: {str(e)}", code=500)
def _compare_versions(v1: str, v2: str) -> int:
"""比较两个版本号,返回 1/0/-1"""
t1 = _version_tuple(v1)
t2 = _version_tuple(v2)
if t1 > t2:
return 1
elif t1 < t2:
return -1
return 0
def _version_tuple(v: str) -> tuple:
"""将版本字符串转为可比较的元组"""
parts = []
for p in v.split('.'):
try:
parts.append(int(p))
except ValueError:
parts.append(0)
return tuple(parts)
async def _execute_sql_content(cursor, sql_content: str):
"""执行 SQL 内容,处理存储过程中的 DELIMITER"""
# 如果包含 DELIMITER需要特殊处理
if 'DELIMITER' in sql_content:
# 移除 DELIMITER 行,按 $$ 分割存储过程
lines = sql_content.split('\n')
current_block = []
in_procedure = False
for line in lines:
stripped = line.strip()
if stripped.upper().startswith('DELIMITER $$'):
in_procedure = True
current_block = []
continue
elif stripped.upper() == 'DELIMITER ;':
# 执行累积的存储过程块
if current_block:
proc_sql = '\n'.join(current_block).strip()
if proc_sql:
await cursor.execute(proc_sql)
in_procedure = False
current_block = []
continue
elif stripped.upper().startswith('DELIMITER'):
continue
if in_procedure:
current_block.append(line)
else:
# 普通SQL按分号分割执行
if stripped and not stripped.startswith('--'):
# 简单的按分号分割
for stmt in stripped.split(';'):
stmt = stmt.strip()
if stmt:
await cursor.execute(stmt)
else:
# 无 DELIMITER简单执行
# 按 CREATE 分割以支持多语句
# 分割 SQL 语句
statements = re.split(r';\s*\n', sql_content)
for stmt in statements:
stmt = stmt.strip()
if stmt and stmt != '--':
await cursor.execute(stmt)

View File

@@ -64,14 +64,6 @@ class AddAdminResponse(BaseModel):
message: str message: str
class UpdateHomeworkStatusRequest(BaseModel):
"""更新作业状态请求"""
submission_id: int = Field(..., gt=0, description="提交记录ID")
status: str = Field(..., pattern=r'^(submitted|not_submitted|late|excused)$', description="状态")
comments: Optional[str] = Field(None, max_length=500, description="评语")
apply_deduction: bool = False
class AddStudentRequest(BaseModel): class AddStudentRequest(BaseModel):
"""新增学生请求""" """新增学生请求"""
student_no: str = Field(..., min_length=1, max_length=20, pattern=r'^[a-zA-Z0-9]+$', description="学号") student_no: str = Field(..., min_length=1, max_length=20, pattern=r'^[a-zA-Z0-9]+$', description="学号")
@@ -114,14 +106,6 @@ class UpdateStudentRequest(BaseModel):
dormitory_number: Optional[str] = Field(None, max_length=20, description="宿舍号") dormitory_number: Optional[str] = Field(None, max_length=20, description="宿舍号")
class CreateAssignmentRequest(BaseModel):
"""创建作业请求"""
subject_id: int = Field(..., gt=0, description="科目ID")
title: str = Field(..., min_length=1, max_length=200, description="作业标题")
description: Optional[str] = Field(None, max_length=1000, description="作业描述")
deadline: str = Field(..., min_length=1, max_length=20, description="截止日期")
class UnlockUserRequest(BaseModel): class UnlockUserRequest(BaseModel):
"""解除用户登录锁定请求""" """解除用户登录锁定请求"""
username: str = Field(..., min_length=1, max_length=50, description="用户名") username: str = Field(..., min_length=1, max_length=50, description="用户名")

View File

@@ -47,18 +47,6 @@ class ConductHistoryResponse(BaseModel):
records: List[ConductRecord] records: List[ConductRecord]
class HomeworkSubmission(BaseModel):
"""作业提交情况"""
assignment_id: int
title: str
subject: str
deadline: date
status: str
submit_time: Optional[datetime] = None
comments: Optional[str] = None
deduction_applied: bool
class AttendanceRecord(BaseModel): class AttendanceRecord(BaseModel):
"""考勤记录""" """考勤记录"""
attendance_id: int attendance_id: int

View File

@@ -27,13 +27,14 @@ class AdminService:
async def get_students( async def get_students(
page: int = 1, page: int = 1,
page_size: int = 20, page_size: int = 20,
search: str = None search: str = None,
dormitory_number: str = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""获取所有学生列表""" """获取所有学生列表"""
offset = (page - 1) * page_size offset = (page - 1) * page_size
sql = """ sql = """
SELECT student_id, student_no, name, total_points, parent_phone, status SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status
FROM students FROM students
WHERE status = 1 WHERE status = 1
""" """
@@ -43,6 +44,10 @@ class AdminService:
sql += " AND (student_no LIKE %s OR name LIKE %s)" sql += " AND (student_no LIKE %s OR name LIKE %s)"
params.extend([f"%{search}%", f"%{search}%"]) params.extend([f"%{search}%", f"%{search}%"])
if dormitory_number:
sql += " AND dormitory_number = %s"
params.append(dormitory_number)
sql += " ORDER BY student_no LIMIT %s OFFSET %s" sql += " ORDER BY student_no LIMIT %s OFFSET %s"
params.extend([page_size, offset]) params.extend([page_size, offset])
@@ -50,12 +55,17 @@ class AdminService:
# 获取总数 # 获取总数
count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1" count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1"
count_params = []
if search: if search:
count_sql += " AND (student_no LIKE %s OR name LIKE %s)" count_sql += " AND (student_no LIKE %s OR name LIKE %s)"
total_result = await execute_one(count_sql, (f"%{search}%", f"%{search}%")) count_params.extend([f"%{search}%", f"%{search}%"])
if dormitory_number:
count_sql += " AND dormitory_number = %s"
count_params.append(dormitory_number)
if count_params:
total_result = await execute_one(count_sql, tuple(count_params))
else: else:
total_result = await execute_one(count_sql) total_result = await execute_one(count_sql)
total = total_result["total"] if total_result else 0 total = total_result["total"] if total_result else 0
return { return {

View File

@@ -235,6 +235,7 @@ class ConductService:
student_id=student_id, student_id=student_id,
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
related_type=related_type,
page=page, page=page,
page_size=page_size page_size=page_size
) )

View File

@@ -1,135 +0,0 @@
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
from typing import Dict, Any, List, Optional
from datetime import datetime
from models.homework import HomeworkModel
from models.student import StudentModel
from models.conduct import ConductModel
from middleware.permission import PermissionChecker
from config import settings
from utils.logger import get_logger
logger = get_logger(__name__)
class HomeworkService:
"""作业服务"""
@staticmethod
async def get_assignments(user_id: int) -> Dict[str, Any]:
"""获取作业列表"""
role = await PermissionChecker.get_user_role(user_id)
if role == "班主任":
assignments = await HomeworkModel.get_all_assignments()
elif role == "学习委员":
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
assignments = await HomeworkModel.get_assignments_by_subjects(subject_ids)
else:
assignments = []
return {"assignments": assignments}
@staticmethod
async def create_assignment(
subject_id: int,
title: str,
description: Optional[str],
deadline: str,
created_by: int
) -> Dict[str, Any]:
"""创建作业"""
assignment_id = await HomeworkModel.create_assignment(
subject_id=subject_id,
title=title,
description=description,
deadline=deadline,
created_by=created_by
)
if assignment_id:
logger.info(f"用户[{created_by}] 创建作业[{assignment_id}]: {title}")
return {"success": True, "assignment_id": assignment_id}
else:
return {"success": False, "message": "创建作业失败"}
@staticmethod
async def update_submission_status(
submission_id: int,
status: str,
comments: Optional[str],
apply_deduction: bool,
operator_id: int
) -> Dict[str, Any]:
"""更新作业提交状态"""
# 获取提交记录信息
submission = await HomeworkModel.get_submission(submission_id)
if not submission:
return {"success": False, "message": "提交记录不存在"}
# 检查权限
role = await PermissionChecker.get_user_role(operator_id)
if role == "学习委员":
# 检查是否管理该科目
subject_ids = await PermissionChecker.get_user_subject_ids(operator_id)
if submission["subject_id"] not in subject_ids:
return {"success": False, "message": "无权操作此作业"}
elif role != "班主任":
return {"success": False, "message": "无权进行此操作"}
# 更新状态
result = await HomeworkModel.update_submission(
submission_id=submission_id,
status=status,
comments=comments,
updated_by=operator_id
)
if not result:
return {"success": False, "message": "更新失败"}
# 应用扣分
if apply_deduction and status in ["not_submitted", "late"]:
# 确定扣分数值
if status == "not_submitted":
points_change = -settings.DEDUCTION_HOMEWORK_NOT_SUBMIT
else:
points_change = -settings.DEDUCTION_HOMEWORK_LATE
# 创建扣分记录
student = await StudentModel.get_by_id(submission["student_id"])
if student:
# 获取操作人姓名
from models.user import UserModel
user = await UserModel.get_by_user_id(operator_id)
recorder_name = user.get("real_name", "班主任") if user else "班主任"
await ConductModel.create_record(
student_id=submission["student_id"],
points_change=points_change,
reason=f"作业未提交/迟交: {submission['title']}",
recorder_id=operator_id,
recorder_name=recorder_name,
related_type="homework",
related_id=submission["assignment_id"]
)
# 更新学生总分
await StudentModel.update_total_points(submission["student_id"], points_change)
# 标记已应用扣分
await HomeworkModel.mark_deduction_applied(submission_id)
logger.info(f"用户[{operator_id}] 更新作业提交状态[{submission_id}] -> {status}")
return {"success": True, "message": "状态更新成功"}

View File

@@ -15,7 +15,6 @@ from typing import Dict, Any, Optional, List
from models.user import UserModel from models.user import UserModel
from models.student import StudentModel from models.student import StudentModel
from models.conduct import ConductModel from models.conduct import ConductModel
from models.homework import HomeworkModel
from models.attendance import AttendanceModel from models.attendance import AttendanceModel
from utils.logger import get_logger from utils.logger import get_logger
@@ -46,24 +45,6 @@ class ParentService:
} }
@staticmethod @staticmethod
async def get_child_homework(parent_id: int) -> Dict[str, Any]:
"""获取子女作业情况"""
user = await UserModel.get_by_user_id(parent_id)
if not user or not user["student_id"]:
return {"error": "未关联学生"}
student = await StudentModel.get_by_id(user["student_id"])
if not student:
return {"error": "学生不存在"}
homework = await HomeworkModel.get_student_homework(user["student_id"])
return {
"student_id": student["student_id"],
"student_name": student["name"],
"homework": homework
}
@staticmethod
async def get_child_attendance(parent_id: int) -> Dict[str, Any]: async def get_child_attendance(parent_id: int) -> Dict[str, Any]:
"""获取子女考勤记录""" """获取子女考勤记录"""
user = await UserModel.get_by_user_id(parent_id) user = await UserModel.get_by_user_id(parent_id)

View File

@@ -14,9 +14,9 @@ from datetime import datetime, timedelta
from models.student import StudentModel from models.student import StudentModel
from models.conduct import ConductModel from models.conduct import ConductModel
from models.homework import HomeworkModel
from models.attendance import AttendanceModel from models.attendance import AttendanceModel
from middleware.permission import PermissionChecker from middleware.permission import PermissionChecker
from utils.database import execute_query
from utils.logger import get_logger from utils.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -57,29 +57,33 @@ class StudentService:
@staticmethod @staticmethod
async def get_homework_status(student_id: int) -> Dict[str, Any]: async def get_homework_status(student_id: int) -> Dict[str, Any]:
"""获取学生作业情况""" """获取学生作业扣分记录"""
student = await StudentModel.get_by_id(student_id) student = await StudentModel.get_by_id(student_id)
if not student: if not student:
return {"error": "学生不存在"} return {"error": "学生不存在"}
homework = await HomeworkModel.get_student_homework(student_id) # 查询作业相关的操行分记录
sql = """
SELECT cr.record_id, cr.points_change, cr.reason, cr.created_at,
cr.related_type, cr.recorder_name
FROM conduct_records cr
WHERE cr.student_id = %s AND cr.related_type = 'homework' AND cr.is_revoked = 0
ORDER BY cr.created_at DESC
"""
records = await execute_query(sql, (student_id,))
# 统计 # 统计
total = len(homework) total = len(records)
submitted = sum(1 for h in homework if h["status"] == "submitted") deductions = sum(1 for r in records if r["points_change"] < 0)
not_submitted = sum(1 for h in homework if h["status"] == "not_submitted")
late = sum(1 for h in homework if h["status"] == "late")
return { return {
"student_id": student_id, "student_id": student_id,
"student_name": student["name"], "student_name": student["name"],
"statistics": { "statistics": {
"total": total, "total": total,
"submitted": submitted, "deductions": deductions
"not_submitted": not_submitted,
"late": late
}, },
"homework": homework "homework": records
} }
@staticmethod @staticmethod

View File

@@ -35,6 +35,7 @@ include __DIR__ . '/../includes/header.php';
<div class="action-bar"> <div class="action-bar">
<div class="action-buttons"> <div class="action-buttons">
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button> <button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
<button class="btn btn-secondary" onclick="showDormitoryPointsModal()">宿舍加分</button>
<?php if ($role === '班主任'): ?> <?php if ($role === '班主任'): ?>
<button class="btn btn-secondary" onclick="exportMoralityRecords()">导出德育分记录</button> <button class="btn btn-secondary" onclick="exportMoralityRecords()">导出德育分记录</button>
<?php endif; ?> <?php endif; ?>
@@ -110,4 +111,40 @@ include __DIR__ . '/../includes/header.php';
</div> </div>
</div> </div>
<!-- 宿舍集体加分模态框 -->
<div id="dormitoryPointsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>宿舍集体加分</h3>
<button class="modal-close" onclick="closeModal('dormitoryPointsModal')">&times;</button>
</div>
<form onsubmit="event.preventDefault(); submitDormitoryPoints()">
<div class="form-group">
<label>选择宿舍</label>
<select id="dormitorySelect" onchange="onDormitorySelected()" required>
<option value="">-- 请选择宿舍 --</option>
</select>
</div>
<div class="form-group" id="dormitoryStudentsGroup" style="display:none;">
<label>宿舍成员</label>
<div id="dormitoryStudentsList" style="max-height: 150px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 4px; padding: 8px;">
</div>
<small id="dormitoryStudentsCount"></small>
</div>
<div class="form-group">
<label>分数变动</label>
<input type="number" id="dormitoryPointsChange" required placeholder="正数为加分,负数为扣分">
</div>
<div class="form-group">
<label>原因</label>
<textarea id="dormitoryPointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">确认提交</button>
<button type="button" class="btn" onclick="closeModal('dormitoryPointsModal')">取消</button>
</div>
</form>
</div>
</div>
<?php include __DIR__ . '/../includes/footer.php'; ?> <?php include __DIR__ . '/../includes/footer.php'; ?>

View File

@@ -154,7 +154,7 @@ include __DIR__ . '/../includes/header.php';
} }
if (iconEl) iconEl.textContent = '⟳'; if (iconEl) iconEl.textContent = '⟳';
fetch('/upgrade.php?action=step&version=' + encodeURIComponent(step.version), { method: 'POST' }) fetch('/api/execute_upgrade.php?action=step&version=' + encodeURIComponent(step.version), { method: 'POST' })
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(data) { .then(function(data) {
if (data.success) { if (data.success) {

View File

@@ -54,7 +54,7 @@ include __DIR__ . '/../includes/header.php';
<div class="filter-group" style="min-width:auto;"> <div class="filter-group" style="min-width:auto;">
<label>&nbsp;</label> <label>&nbsp;</label>
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;"> <label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;">
<input type="checkbox" id="historyGrouped" onchange="loadHistory(1)"> 批次合并 <input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
</label> </label>
</div> </div>
<?php if ($role === '班主任'): ?> <?php if ($role === '班主任'): ?>

View File

@@ -1,7 +1,6 @@
<?php <?php
/** /**
* 检查数据库版本是否需要升级 * 检查数据库版本是否需要升级(代理至后端 API
* 返回 JSON: {needs_upgrade: bool, current: string, target: string}
*/ */
require_once __DIR__ . '/../config.php'; require_once __DIR__ . '/../config.php';
@@ -20,84 +19,43 @@ if ($role !== '班主任') {
exit(); exit();
} }
// 读取后端 .env 获取数据库配置 // 从 session 获取 JWT token
$envPath = __DIR__ . '/../../backend/.env'; $token = $_SESSION['jwt_token'] ?? '';
if (!file_exists($envPath)) { if (empty($token)) {
echo json_encode(['error' => '数据库配置文件不存在']); echo json_encode(['error' => '会话已过期,请重新登录']);
exit(); exit();
} }
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); // 调用后端 API
$dbConfig = []; $apiUrl = API_BASE_URL . '/api/upgrade/check';
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || strpos($line, '#') === 0) {
continue;
}
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$dbConfig[trim($key)] = trim($value);
}
}
$required = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']; $ch = curl_init();
foreach ($required as $key) { curl_setopt_array($ch, [
if (!isset($dbConfig[$key]) || $dbConfig[$key] === '') { CURLOPT_URL => $apiUrl,
echo json_encode(['error' => "缺少数据库配置: {$key}"]); CURLOPT_RETURNTRANSFER => true,
exit(); CURLOPT_TIMEOUT => API_TIMEOUT,
} CURLOPT_HTTPHEADER => [
} 'Authorization: Bearer ' . $token,
'Content-Type: application/json'
try { ],
$dsn = "mysql:host={$dbConfig['DB_HOST']};port={$dbConfig['DB_PORT']};dbname={$dbConfig['DB_NAME']};charset=utf8mb4"; CURLOPT_SSL_VERIFYPEER => false,
$pdo = new PDO($dsn, $dbConfig['DB_USER'], $dbConfig['DB_PASSWORD'], [ CURLOPT_SSL_VERIFYHOST => 0
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]); ]);
// 检测当前版本 $apiResponse = curl_exec($ch);
$currentVersion = '0.0.0'; $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
try { curl_close($ch);
$stmt = $pdo->query("SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$currentVersion = $row['setting_value'];
}
} catch (PDOException $e) {
// 表不存在,使用默认值
}
// 读取目标版本 if ($httpCode !== 200 || empty($apiResponse)) {
$versionFile = __DIR__ . '/../../VERSION'; echo json_encode(['error' => '无法连接升级服务']);
if (!file_exists($versionFile)) {
echo json_encode(['error' => 'VERSION 文件不存在']);
exit(); exit();
} }
$targetVersion = trim(file_get_contents($versionFile));
$needsUpgrade = version_compare($targetVersion, $currentVersion, '>'); $result = json_decode($apiResponse, true);
if (!$result || !isset($result['success']) || !$result['success']) {
echo json_encode(['error' => $result['message'] ?? '升级检查失败']);
exit();
}
$allVersions = [ // 转发后端返回的升级数据
'1.7' => 'v1.7.sql', echo json_encode($result['data']);
'1.8' => 'v1.8.sql',
'2.0' => 'v2.0.sql',
'2.0.1' => 'v2.0.1.sql',
'2.1' => 'v2.1.sql',
];
$steps = [];
foreach ($allVersions as $version => $file) {
if (version_compare($version, $currentVersion, '>') &&
version_compare($version, $targetVersion, '<=')) {
$steps[] = ['version' => $version, 'file' => $file];
}
}
usort($steps, function($a, $b) { return version_compare($a['version'], $b['version']); });
echo json_encode([
'needs_upgrade' => $needsUpgrade,
'current' => $currentVersion,
'target' => $targetVersion,
'steps' => $steps
]);
} catch (PDOException $e) {
echo json_encode(['error' => '数据库连接失败: ' . $e->getMessage()]);
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* 执行单个升级步骤(代理至后端 API
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../config.php';
// 验证登录和权限
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
http_response_code(401);
echo json_encode(['success' => false, 'error' => '未授权']);
exit();
}
$role = $_SESSION['role'] ?? '';
if ($role !== '班主任') {
http_response_code(403);
echo json_encode(['success' => false, 'error' => '权限不足']);
exit();
}
// 只接受 POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'error' => '无效请求']);
exit();
}
$stepVersion = $_GET['version'] ?? '';
if (empty($stepVersion)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => '缺少版本号参数']);
exit();
}
// 从 session 获取 JWT token
$token = $_SESSION['jwt_token'] ?? '';
if (empty($token)) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => '会话已过期,请重新登录']);
exit();
}
// 调用后端 API
$apiUrl = API_BASE_URL . '/api/upgrade/step';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $apiUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['version' => $stepVersion]),
CURLOPT_TIMEOUT => API_TIMEOUT,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $token,
'Content-Type: application/json'
],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0
]);
$apiResponse = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || empty($apiResponse)) {
http_response_code(500);
echo json_encode([
'success' => false,
'version' => $stepVersion,
'error' => '无法连接升级服务'
]);
exit();
}
$result = json_decode($apiResponse, true);
if (!$result || !isset($result['success']) || !$result['success']) {
http_response_code(500);
echo json_encode([
'success' => false,
'version' => $stepVersion,
'error' => $result['message'] ?? '升级失败'
]);
exit();
}
// 转发后端返回的数据
echo json_encode($result['data']);

View File

@@ -160,6 +160,7 @@ $_SESSION['username'] = $data['username'];
$_SESSION['real_name'] = $data['real_name'] ?? ''; $_SESSION['real_name'] = $data['real_name'] ?? '';
$_SESSION['role'] = $data['role'] ?? ''; $_SESSION['role'] = $data['role'] ?? '';
$_SESSION['login_time'] = time(); $_SESSION['login_time'] = time();
$_SESSION['jwt_token'] = $token;
// 如果是学生,额外设置 student_id // 如果是学生,额外设置 student_id
if ($data['user_type'] === 'student') { if ($data['user_type'] === 'student') {

View File

@@ -132,13 +132,8 @@ function formatDateTime(dateStr) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
} }
function getStatusBadge(status, type = 'homework') { function getStatusBadge(status, type = 'attendance') {
const statusMap = { const statusMap = {
homework: {
'submitted': '已提交',
'not_submitted': '未提交',
'late': '迟交'
},
attendance: { attendance: {
'present': '出勤', 'present': '出勤',
'absent': '缺勤', 'absent': '缺勤',
@@ -146,15 +141,13 @@ function getStatusBadge(status, type = 'homework') {
'leave': '请假' 'leave': '请假'
} }
}; };
const texts = statusMap[type] || statusMap.homework; const texts = statusMap[type] || statusMap.attendance;
const text = texts[status] || status; const text = texts[status] || status;
let className = 'status-badge '; let className = 'status-badge ';
switch (status) { switch (status) {
case 'submitted':
case 'present': case 'present':
className += 'status-submitted'; className += 'status-submitted';
break; break;
case 'not_submitted':
case 'absent': case 'absent':
className += 'status-not_submitted'; className += 'status-not_submitted';
break; break;

View File

@@ -118,11 +118,113 @@ async function exportMoralityRecords() {
console.error('导出失败:', err); console.error('导出失败:', err);
} }
} }
// 宿舍集体加分相关
var dormitoryStudentIds = [];
async function showDormitoryPointsModal() {
dormitoryStudentIds = [];
document.getElementById('dormitorySelect').innerHTML = '<option value="">-- 请选择宿舍 --</option>';
document.getElementById('dormitoryStudentsGroup').style.display = 'none';
document.getElementById('dormitoryStudentsList').innerHTML = '';
document.getElementById('dormitoryPointsChange').value = '';
document.getElementById('dormitoryPointsReason').value = '';
// 加载宿舍列表
const res = await apiGet('/api/admin/students/dormitories');
if (res && res.success && res.data.dormitories) {
const select = document.getElementById('dormitorySelect');
res.data.dormitories.forEach(d => {
const option = document.createElement('option');
option.value = d;
option.textContent = d;
select.appendChild(option);
});
}
document.getElementById('dormitoryPointsModal').style.display = 'flex';
}
async function onDormitorySelected() {
const dormitory = document.getElementById('dormitorySelect').value;
const studentsGroup = document.getElementById('dormitoryStudentsGroup');
const studentsList = document.getElementById('dormitoryStudentsList');
const studentsCount = document.getElementById('dormitoryStudentsCount');
dormitoryStudentIds = [];
studentsList.innerHTML = '';
if (!dormitory) {
studentsGroup.style.display = 'none';
return;
}
// 加载该宿舍的学生
const res = await apiGet('/api/admin/students', { dormitory_number: dormitory, page_size: 1000 });
if (res && res.success && res.data.students) {
const students = res.data.students;
if (students.length === 0) {
studentsList.innerHTML = '<p style="color: var(--text-secondary);">该宿舍暂无学生</p>';
studentsCount.textContent = '';
} else {
students.forEach(s => {
dormitoryStudentIds.push(s.student_id);
const div = document.createElement('div');
div.style.cssText = 'display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid var(--border-color);';
div.innerHTML = `<span>${escapeHtml(s.name)}</span><span style="color: var(--text-secondary);">${escapeHtml(s.student_no)}</span>`;
studentsList.appendChild(div);
});
studentsCount.textContent = `${students.length}`;
}
studentsGroup.style.display = 'block';
} else {
studentsList.innerHTML = '<p style="color: var(--text-secondary);">加载失败</p>';
studentsGroup.style.display = 'block';
}
}
async function submitDormitoryPoints() {
if (dormitoryStudentIds.length === 0) {
showToast('该宿舍没有学生', 'warning');
return;
}
const pointsChange = parseInt(document.getElementById('dormitoryPointsChange').value);
const reason = document.getElementById('dormitoryPointsReason').value;
if (isNaN(pointsChange) || pointsChange === 0) {
showToast('分值不能为0', 'error');
return;
}
if (!reason.trim()) {
showToast('请填写原因', 'error');
return;
}
const data = {
student_ids: dormitoryStudentIds,
points_change: pointsChange,
reason: reason
};
const res = await apiPost('/api/admin/conduct/add', data);
if (res && res.success) {
showToast(`操作成功: ${res.data.success_count} 人成功`);
closeModal('dormitoryPointsModal');
loadStudents();
} else {
showToast(res?.message || '操作失败', 'error');
}
}
loadStudents(); loadStudents();
window.loadStudents = loadStudents; window.loadStudents = loadStudents;
window.showSinglePointsModal = showSinglePointsModal; window.showSinglePointsModal = showSinglePointsModal;
window.exportMoralityRecords = exportMoralityRecords; window.exportMoralityRecords = exportMoralityRecords;
window.showDormitoryPointsModal = showDormitoryPointsModal;
window.onDormitorySelected = onDormitorySelected;
window.submitDormitoryPoints = submitDormitoryPoints;
})(); })();

View File

@@ -25,9 +25,15 @@ async function loadDashboard() {
} }
let quickActions = ''; let quickActions = '';
if (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') { if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>'; quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
} }
if (role === '班主任' || role === '学习委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/homework.php\'">作业扣分</button>';
}
if (role === '班主任' || role === '考勤委员') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/attendance.php\'">考勤管理</button>';
}
if (role === '班主任') { if (role === '班主任') {
quickActions += '<button class="btn btn-outline" onclick="location.href=\'/admin/students.php\'">导入学生</button>'; quickActions += '<button class="btn btn-outline" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>'; quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';

View File

@@ -40,7 +40,7 @@ async function loadHistory(page = 1) {
end_date: endDate end_date: endDate
}; };
if (studentId) params.student_id = studentId; if (studentId) params.student_id = studentId;
if (relatedType && !isGrouped) params.related_type = relatedType; if (relatedType) params.related_type = relatedType;
if (isGrouped) params.grouped = true; if (isGrouped) params.grouped = true;
const res = await apiGet('/api/admin/conduct/history', params); const res = await apiGet('/api/admin/conduct/history', params);

View File

@@ -146,7 +146,7 @@
const students = data.students || []; const students = data.students || [];
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>'; let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>'; html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>宿舍号</th><th>初始密码</th>';
html += '</tr></thead><tbody>'; html += '</tr></thead><tbody>';
students.forEach(s => { students.forEach(s => {
@@ -154,6 +154,7 @@
<td>${escapeHtml(s.student_no || '')}</td> <td>${escapeHtml(s.student_no || '')}</td>
<td>${escapeHtml(s.name || '')}</td> <td>${escapeHtml(s.name || '')}</td>
<td>${escapeHtml(s.parent_phone || '')}</td> <td>${escapeHtml(s.parent_phone || '')}</td>
<td>${escapeHtml(s.dormitory_number || '-')}</td>
<td>${escapeHtml(s.password || '123456')}</td> <td>${escapeHtml(s.password || '123456')}</td>
</tr>`; </tr>`;
}); });

View File

@@ -16,25 +16,18 @@ async function loadHomework() {
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`); const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (res && res.success) { if (res && res.success) {
let html = ''; let html = '';
res.data.homework.forEach(hw => { res.data.homework.forEach(record => {
// 提交状态 const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
let statusDisplay = '-'; const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e';
if (hw.status) {
statusDisplay = getStatusBadge(hw.status, 'homework');
}
// 扣分显示
const pointsDisplay = hw.points ? `<span class="text-danger">${hw.points}分</span>` : '-';
html += `<tr> html += `<tr>
<td>${escapeHtml(hw.title)}</td> <td>${formatDateTime(record.created_at)}</td>
<td>${escapeHtml(hw.subject_name)}</td> <td style="color: ${pointsColor}; font-weight: bold;">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${hw.deadline || '-'}</td> <td>${escapeHtml(record.reason)}</td>
<td>${statusDisplay}</td> <td>${escapeHtml(record.recorder_name || '-')}</td>
<td>${pointsDisplay}</td>
</tr>`; </tr>`;
}); });
if (res.data.homework.length === 0) { if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业记录</td></tr>'; html = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业扣分记录</td></tr>';
} }
document.getElementById('homeworkList').innerHTML = html; document.getElementById('homeworkList').innerHTML = html;
} }

View File

@@ -1,102 +0,0 @@
/**
* 班级操行分管理系统 - 科目管理页JS
*
* 开发者: Canglan
* 版权归属: Sea Network Technology Studio
*
* 版权所有 © Sea Network Technology Studio
*/
(function() {
'use strict';
async function loadSubjects() {
const res = await apiGet('/api/subject/list');
if (res && res.success) {
let html = '';
res.data.subjects.forEach(sub => {
html += `
<div class="subject-item">
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
${sub.is_active ? '启用' : '禁用'}
</span>
<button class="btn btn-sm btn-outline" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
<button class="btn btn-sm btn-ghost" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
${sub.is_active ? '禁用' : '启用'}
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
</div>
`;
});
if (res.data.subjects.length === 0) {
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
}
document.getElementById('subjectList').innerHTML = html;
}
}
async function toggleSubject(subjectId, enable) {
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
if (res && res.success) {
showToast(enable ? '科目已启用' : '科目已禁用');
loadSubjects();
} else {
showToast(res?.message || '操作失败', 'error');
}
}
async function deleteSubject(subjectId) {
if (!confirm('确定要删除该科目吗?')) return;
const res = await apiDelete('/api/subject/delete/' + subjectId);
if (res && res.success) {
showToast('科目删除成功');
loadSubjects();
} else {
showToast(res?.message || '删除失败', 'error');
}
}
function showEditSubjectModal(subjectId, name, code, sortOrder) {
document.getElementById('editSubjectId').value = subjectId;
document.getElementById('editSubjectName').value = name;
document.getElementById('editSubjectCode').value = code;
document.getElementById('editSubjectSortOrder').value = sortOrder;
document.getElementById('editSubjectModal').style.display = 'flex';
}
async function submitEditSubject() {
const subjectId = document.getElementById('editSubjectId').value;
const subjectName = document.getElementById('editSubjectName').value.trim();
const subjectCode = document.getElementById('editSubjectCode').value.trim();
const sortOrder = document.getElementById('editSubjectSortOrder').value;
if (!subjectName) {
showToast('请填写科目名称', 'warning');
return;
}
const data = { subject_name: subjectName };
if (subjectCode) data.subject_code = subjectCode;
if (sortOrder !== '') data.sort_order = parseInt(sortOrder);
const res = await apiPut(`/api/subject/update/${subjectId}`, data);
if (res && res.success) {
showToast('科目更新成功');
closeModal('editSubjectModal');
loadSubjects();
} else {
showToast(res?.message || '更新失败', 'error');
}
}
loadSubjects();
window.loadSubjects = loadSubjects;
window.toggleSubject = toggleSubject;
window.deleteSubject = deleteSubject;
window.showEditSubjectModal = showEditSubjectModal;
window.submitEditSubject = submitEditSubject;
})();

View File

@@ -7,37 +7,42 @@
"_comment6": " student_no - 必填,学生学号,唯一标识", "_comment6": " student_no - 必填,学生学号,唯一标识",
"_comment7": " name - 必填,学生姓名", "_comment7": " name - 必填,学生姓名",
"_comment8": " parent_phone - 可选家长手机号11位手机号", "_comment8": " parent_phone - 可选家长手机号11位手机号",
"_comment9": " password - 可选,初始密码,不填则默认 123456", "_comment9": " dormitory_number - 可选,宿舍号(支持字母数字组合,如 301-A",
"_comment10": "================================================", "_comment10": " password - 可选,初始密码,不填则默认 123456",
"_comment11": "导入规则:", "_comment11": "================================================",
"_comment12": " 1. 学生操行分初始值 = 60分", "_comment12": "导入规则:",
"_comment13": " 2. 学生账号 = 学号,密码 = 指定的password或123456", "_comment13": " 1. 学生操行分初始值 = 60分",
"_comment14": " 3. 家长账号 = 手机号若parent_phone有值,密码 = 指定的password或123456", "_comment14": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
"_comment15": " 4. 家长姓名默认显示为 '学生姓名家长'", "_comment15": " 3. 家长账号 = 手机号若parent_phone有值密码 = 指定的password或123456",
"_comment16": "================================================", "_comment16": " 4. 家长姓名默认显示为 '学生姓名家长'",
"_comment17": "================================================",
"students": [ "students": [
{ {
"student_no": "20240001", "student_no": "20240001",
"name": "张三", "name": "张三",
"parent_phone": "13800138001", "parent_phone": "13800138001",
"dormitory_number": "301-A",
"password": "123456" "password": "123456"
}, },
{ {
"student_no": "20240002", "student_no": "20240002",
"name": "李四", "name": "李四",
"parent_phone": "13800138002", "parent_phone": "13800138002",
"dormitory_number": "205",
"password": "123456" "password": "123456"
}, },
{ {
"student_no": "20240003", "student_no": "20240003",
"name": "王五", "name": "王五",
"parent_phone": "", "parent_phone": "",
"dormitory_number": "",
"password": "" "password": ""
}, },
{ {
"student_no": "20240004", "student_no": "20240004",
"name": "赵六", "name": "赵六",
"parent_phone": "13800138004", "parent_phone": "13800138004",
"dormitory_number": "102-B",
"password": "" "password": ""
} }
] ]

View File

@@ -1,7 +1,7 @@
<div class="nav"> <div class="nav">
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a> <a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a> <a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员' || $role === '劳动委员' || $role === '志愿委员'): ?> <?php if ($role === '班主任' || $role === '班长' || $role === '学习委员' || $role === '考勤委员' || $role === '劳动委员' || $role === '志愿委员'): ?>
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a> <a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
<?php endif; ?> <?php endif; ?>
<?php if ($role === '班主任' || $role === '学习委员'): ?> <?php if ($role === '班主任' || $role === '学习委员'): ?>

View File

@@ -42,10 +42,6 @@ include __DIR__ . '/../includes/header.php';
<div class="stat-label">班级排名</div> <div class="stat-label">班级排名</div>
<div class="stat-value" id="studentRank">--</div> <div class="stat-value" id="studentRank">--</div>
</div> </div>
<div class="stat-card">
<div class="stat-label">缺交次数</div>
<div class="stat-value" id="homeworkMissing">--</div>
</div>
</div> </div>
<div class="initial-points-hint" id="initialPointsHint"></div> <div class="initial-points-hint" id="initialPointsHint"></div>
</div> </div>
@@ -93,15 +89,6 @@ async function loadDashboard() {
} }
} }
// 加载作业缺交次数
const hwRes = await apiGet('/api/parent/child/homework');
if (hwRes && hwRes.success && hwRes.data.homework) {
const homework = hwRes.data.homework;
const total = homework.length;
const notSubmitted = homework.filter(h => h.status === 'not_submitted' || h.status === 'late').length;
document.getElementById('homeworkMissing').textContent = `缺交 ${notSubmitted}/${total} 次`;
}
// 显示初始分提示 // 显示初始分提示
const initialPoints = window.STUDENT_INITIAL_POINTS || 60; const initialPoints = window.STUDENT_INITIAL_POINTS || 60;
document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`; document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`;

View File

@@ -94,7 +94,7 @@ include __DIR__ . '/../includes/header.php';
<div class="stat-value" id="studentRank">--</div> <div class="stat-value" id="studentRank">--</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">缺交次数</div> <div class="stat-label">作业扣分</div>
<div class="stat-value" id="homeworkRate">--</div> <div class="stat-value" id="homeworkRate">--</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
@@ -140,11 +140,10 @@ include __DIR__ . '/../includes/header.php';
<table> <table>
<thead> <thead>
<tr> <tr>
<th>科目</th>
<th>时间</th> <th>时间</th>
<th>分值</th> <th>分值</th>
<th>备注</th> <th>原因</th>
<th>作业</th> <th>操作人</th>
</tr> </tr>
</thead> </thead>
<tbody id="homeworkList"></tbody> <tbody id="homeworkList"></tbody>
@@ -319,14 +318,13 @@ include __DIR__ . '/../includes/header.php';
document.getElementById('studentRank').textContent = '--'; document.getElementById('studentRank').textContent = '--';
} }
} }
// 获取作业统计 - 缺交次数 // 获取作业扣分统计
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`); const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (homeworkRes && homeworkRes.success) { if (homeworkRes && homeworkRes.success) {
const stats = homeworkRes.data.statistics; const stats = homeworkRes.data.statistics;
const notSubmitted = (stats.not_submitted || 0) + (stats.late || 0); const deductions = stats.deductions || 0;
const total = stats.total || 0; const total = stats.total || 0;
document.getElementById('homeworkRate').textContent = `缺交 ${notSubmitted}/${total} 次`; document.getElementById('homeworkRate').textContent = `${deductions} 次扣分`;
}
} }
// 获取考勤统计 // 获取考勤统计
@@ -394,25 +392,24 @@ include __DIR__ . '/../includes/header.php';
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`); const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
if (res && res.success) { if (res && res.success) {
let html = ''; let html = '';
res.data.homework.forEach(hw => { res.data.homework.forEach(record => {
const pointsDisplay = hw.points ? hw.points + '分' : '-'; const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
html += ` html += `
<tr> <tr>
<td>${escapeHtml(hw.subject_name)}</td> <td>${formatDateTime(record.created_at)}</td>
<td>${hw.deadline || hw.created_at}</td> <td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
<td>${pointsDisplay}</td> <td>${escapeHtml(record.reason)}</td>
<td>${escapeHtml(hw.comments || '-')}</td> <td>${escapeHtml(record.recorder_name || '-')}</td>
<td>${escapeHtml(hw.title)}</td>
</tr> </tr>
`; `;
}); });
if (res.data.homework.length === 0) { if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>'; html = '<tr><td colspan="4" style="text-align:center;">暂无作业扣分记录</td></tr>';
} }
document.getElementById('homeworkList').innerHTML = html; document.getElementById('homeworkList').innerHTML = html;
} }
} catch (error) { } catch (error) {
console.error('加载作业失败:', error); console.error('加载作业记录失败:', error);
} }
} }

View File

@@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
<div class="table-wrapper"> <div class="table-wrapper">
<table class="table"> <table class="table">
<thead> <thead>
<tr><th>作业名称</th><th>科目</th><th>截止时间</th><th>提交状态</th><th>扣分</th></tr> <tr><th>时间</th><th>分值</th><th>原因</th><th>操作人</th></tr>
</thead> </thead>
<tbody id="homeworkList"></tbody> <tbody id="homeworkList"></tbody>
</table> </table>

View File

@@ -202,6 +202,26 @@ CREATE TABLE IF NOT EXISTS `semester_archives` (
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 系统设置表
CREATE TABLE IF NOT EXISTS `system_settings` (
`setting_key` VARCHAR(50) PRIMARY KEY,
`setting_value` VARCHAR(255) NOT NULL,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 性能索引v2.0
CREATE INDEX `idx_conduct_semester` ON `conduct_records`(`semester_id`);
CREATE INDEX `idx_attendance_semester` ON `attendance_records`(`semester_id`);
CREATE INDEX `idx_conduct_student` ON `conduct_records`(`student_id`);
-- 性能索引v2.1
CREATE INDEX `idx_student_created` ON `conduct_records`(`student_id`, `created_at`);
CREATE INDEX `idx_recorder_id` ON `conduct_records`(`recorder_id`);
CREATE INDEX `idx_date` ON `attendance_records`(`date`);
CREATE INDEX `idx_username_created` ON `login_logs`(`username`, `created_at`);
CREATE INDEX `idx_operator_created` ON `operation_logs`(`operator_id`, `created_at`);
CREATE INDEX `idx_semester_id` ON `semester_archives`(`semester_id`);
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;
-- 插入初始科目(仅语数英,如不存在) -- 插入初始科目(仅语数英,如不存在)
@@ -210,4 +230,10 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL
('数学', 'MATH', 2), ('数学', 'MATH', 2),
('英语', 'ENG', 3); ('英语', 'ENG', 3);
SELECT '数据库初始化完成!' AS message; -- 初始化系统版本号
INSERT INTO `system_settings` (`setting_key`, `setting_value`)
VALUES ('db_version', '2.2')
ON DUPLICATE KEY UPDATE `setting_value` = '2.2';
-- 控制台输出初始化结果(含版本号)
SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message;

11
sql/upgrades/v2.2.sql Normal file
View File

@@ -0,0 +1,11 @@
-- ===========================================
-- 班级操行分管理系统 - v2.1 → v2.2 升级脚本
-- 字符集: utf8mb4
--
-- 说明: v2.2 为安全修复与功能增强版本,无数据库 schema 变更。
-- 主要变更:
-- 1. 修复管理员操作越权漏洞(加分/撤销/恢复操作增加权限校验)
-- 2. 新增宿舍集体加分功能(前端+后端)
-- 3. 学生导入支持宿舍号字段
-- 4. 导入预览表格显示宿舍号列
-- ===========================================

View File

@@ -10,6 +10,16 @@
// 辅助函数 // 辅助函数
// =========================================== // ===========================================
// 版本升级列表(唯一数据源)
$UPGRADE_VERSIONS = [
'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',
];
/** /**
* 读取 backend/.env 文件并解析数据库配置 * 读取 backend/.env 文件并解析数据库配置
*/ */
@@ -59,16 +69,10 @@ function detectCurrentVersion($pdo) {
* 获取需要执行的升级步骤 * 获取需要执行的升级步骤
*/ */
function getUpgradeSteps($currentVersion, $targetVersion) { function getUpgradeSteps($currentVersion, $targetVersion) {
$allVersions = [ global $UPGRADE_VERSIONS;
'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 = []; $steps = [];
foreach ($allVersions as $version => $sqlFile) { foreach ($UPGRADE_VERSIONS as $version => $sqlFile) {
if (version_compare($version, $currentVersion, '>') && if (version_compare($version, $currentVersion, '>') &&
version_compare($version, $targetVersion, '<=')) { version_compare($version, $targetVersion, '<=')) {
$steps[$version] = $sqlFile; $steps[$version] = $sqlFile;
@@ -128,19 +132,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'step')
]); ]);
// 获取该版本对应的 SQL 文件 // 获取该版本对应的 SQL 文件
$allVersions = [ if (!isset($UPGRADE_VERSIONS[$stepVersion])) {
'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}"); throw new RuntimeException("未知版本: {$stepVersion}");
} }
$sqlFile = $allVersions[$stepVersion]; $sqlFile = $UPGRADE_VERSIONS[$stepVersion];
$shortFile = basename($sqlFile); $shortFile = basename($sqlFile);
executeUpgrade($pdo, $stepVersion, $sqlFile); executeUpgrade($pdo, $stepVersion, $sqlFile);
@@ -432,6 +428,15 @@ try {
<div class="error-box"> <div class="error-box">
<strong>错误:</strong><?php echo htmlspecialchars($errorMessage); ?> <strong>错误:</strong><?php echo htmlspecialchars($errorMessage); ?>
</div> </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): ?> <?php elseif ($isUpToDate): ?>
<div class="success-box"> <div class="success-box">
✓ 数据库已是最新版本,无需升级。 ✓ 数据库已是最新版本,无需升级。