From ca53fdc349fb60f9a6b6e26a5eb8bd177fda1427 Mon Sep 17 00:00:00 2001 From: canglan Date: Thu, 28 May 2026 15:38:32 +0800 Subject: [PATCH] =?UTF-8?q?v2.2=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- README.md | 2 +- VERSION | 2 +- backend/main.py | 3 +- backend/models/conduct.py | 6 + backend/models/homework.py | 117 ----------- backend/models/student.py | 12 ++ backend/routes/admin.py | 114 +++------- backend/routes/parent.py | 15 -- backend/routes/upgrade.py | 231 +++++++++++++++++++++ backend/schemas/admin.py | 16 -- backend/schemas/student.py | 12 -- backend/services/admin_service.py | 20 +- backend/services/conduct_service.py | 1 + backend/services/homework_service.py | 135 ------------ backend/services/parent_service.py | 19 -- backend/services/student_service.py | 26 ++- frontend/admin/conduct.php | 37 ++++ frontend/admin/dashboard.php | 2 +- frontend/admin/history.php | 2 +- frontend/api/check_upgrade.php | 110 +++------- frontend/api/execute_upgrade.php | 89 ++++++++ frontend/api/save_session.php | 1 + frontend/assets/js/common.js | 11 +- frontend/assets/js/conduct.js | 102 +++++++++ frontend/assets/js/dashboard.js | 8 +- frontend/assets/js/history.js | 2 +- frontend/assets/js/modules/student-mgmt.js | 3 +- frontend/assets/js/student-homework.js | 23 +- frontend/assets/js/subjects.js | 102 --------- frontend/assets/uploads/sample_import.json | 21 +- frontend/includes/nav.php | 2 +- frontend/parent/dashboard.php | 13 -- frontend/student/dashboard.php | 31 ++- frontend/student/homework.php | 2 +- sql/init.sql | 28 ++- sql/upgrades/v2.2.sql | 11 + upgrade.php | 41 ++-- 38 files changed, 688 insertions(+), 686 deletions(-) delete mode 100644 backend/models/homework.py create mode 100644 backend/routes/upgrade.py delete mode 100644 backend/services/homework_service.py create mode 100644 frontend/api/execute_upgrade.php delete mode 100644 frontend/assets/js/subjects.js create mode 100644 sql/upgrades/v2.2.sql diff --git a/.gitignore b/.gitignore index 0e10557..eca4a9a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,5 @@ docs/guide/student.pdf docs/guide/teacher.pdf qrcode.png -# 展示内容 +# example example \ No newline at end of file diff --git a/README.md b/README.md index cfd8f91..de0e323 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ classmanager/ | v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 | | v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 | | v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 | -| v2.1 | 2025.7.14 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 | +| v2.1 | 2026.5.26 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 | ## 许可证 diff --git a/VERSION b/VERSION index 879b416..8bbe6cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1 +2.2 diff --git a/backend/main.py b/backend/main.py index d23f649..2b7f9bc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -21,7 +21,7 @@ from utils.logger import setup_logger, log_access from utils.database import init_db_pool, close_db_pool from utils.redis_client import init_redis_pool, close_redis_pool 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 @@ -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(semester.router, prefix="/api/semester", 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=["调试"]) diff --git a/backend/models/conduct.py b/backend/models/conduct.py index 5725de1..8c6140d 100644 --- a/backend/models/conduct.py +++ b/backend/models/conduct.py @@ -209,6 +209,7 @@ class ConductModel: student_id: int = None, start_date: str = None, end_date: str = None, + related_type: str = None, page: int = 1, page_size: int = 20 ) -> Dict[str, Any]: @@ -217,6 +218,8 @@ class ConductModel: start_date = None if end_date == "": end_date = None + if related_type == "": + related_type = None conditions = ["cr.is_revoked = 0"] params = [] @@ -230,6 +233,9 @@ class ConductModel: if end_date: conditions.append("cr.created_at <= %s") 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) diff --git a/backend/models/homework.py b/backend/models/homework.py deleted file mode 100644 index f69584a..0000000 --- a/backend/models/homework.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/models/student.py b/backend/models/student.py index befbbac..26fd61a 100644 --- a/backend/models/student.py +++ b/backend/models/student.py @@ -53,6 +53,18 @@ class StudentModel: sql += " ORDER BY student_no" 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 async def create( student_no: str, diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 2a21980..8a19f9a 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -20,16 +20,15 @@ from middleware.permission import ( ) from services.admin_service import AdminService from services.conduct_service import ConductService -from services.homework_service import HomeworkService from services.attendance_service import AttendanceService from services.log_service import LogService from utils.redis_client import RedisClient from schemas.admin import ( AddPointsRequest, RevokeRequest, AddAdminRequest, AddStudentRequest, UpdateStudentRequest, - UpdateHomeworkStatusRequest, AddAttendanceRequest, + AddAttendanceRequest, UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest, - CreateAssignmentRequest, UnlockUserRequest + UnlockUserRequest ) from utils.response import success_response, error_response 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") async def get_students( request: Request, page: int = Query(1, ge=1), 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) if user["user_type"] != "admin": 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) @@ -139,7 +151,8 @@ async def update_student(request: Request, student_id: int, req: UpdateStudentRe result = await AdminService.update_student( student_id=student_id, name=req.name, - parent_phone=req.parent_phone + parent_phone=req.parent_phone, + dormitory_number=req.dormitory_number ) if result["success"]: 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): """批量加减分""" user = await get_current_user(request) + # 仅管理员(班主任/班干部)可操作 + if user["user_type"] != "admin": + return error_response(message="无权进行此操作", code=403) result = await ConductService.add_points( student_ids=req.student_ids, 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): """撤销扣分记录""" user = await get_current_user(request) + # 仅管理员(班主任/班干部)可操作 + if user["user_type"] != "admin": + return error_response(message="无权进行此操作", code=403) result = await ConductService.revoke_record( record_id=req.record_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): """反撤销(恢复)已撤销的记录""" user = await get_current_user(request) + # 仅管理员(班主任/班干部)可操作 + if user["user_type"] != "admin": + return error_response(message="无权进行此操作", code=403) result = await ConductService.restore_record( record_id=req.record_id, restorer_id=user["user_id"] @@ -319,86 +341,6 @@ async def get_conduct_history( 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"]) - # ========== 考勤管理 ========== diff --git a/backend/routes/parent.py b/backend/routes/parent.py index f1598e0..ba214f1 100644 --- a/backend/routes/parent.py +++ b/backend/routes/parent.py @@ -36,21 +36,6 @@ async def get_child_conduct(request: Request): 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") async def get_child_attendance(request: Request): """ diff --git a/backend/routes/upgrade.py b/backend/routes/upgrade.py new file mode 100644 index 0000000..5ef0760 --- /dev/null +++ b/backend/routes/upgrade.py @@ -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) diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index 1814602..a50029a 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -64,14 +64,6 @@ class AddAdminResponse(BaseModel): 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): """新增学生请求""" 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="宿舍号") -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): """解除用户登录锁定请求""" username: str = Field(..., min_length=1, max_length=50, description="用户名") \ No newline at end of file diff --git a/backend/schemas/student.py b/backend/schemas/student.py index 1bfe1b4..ff4adb4 100644 --- a/backend/schemas/student.py +++ b/backend/schemas/student.py @@ -47,18 +47,6 @@ class ConductHistoryResponse(BaseModel): 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): """考勤记录""" attendance_id: int diff --git a/backend/services/admin_service.py b/backend/services/admin_service.py index 2fbf183..6c5e3fc 100644 --- a/backend/services/admin_service.py +++ b/backend/services/admin_service.py @@ -27,14 +27,15 @@ class AdminService: async def get_students( page: int = 1, page_size: int = 20, - search: str = None + search: str = None, + dormitory_number: str = None ) -> Dict[str, Any]: """获取所有学生列表""" offset = (page - 1) * page_size sql = """ - SELECT student_id, student_no, name, total_points, parent_phone, status - FROM students + SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status + FROM students WHERE status = 1 """ params = [] @@ -43,6 +44,10 @@ class AdminService: sql += " AND (student_no LIKE %s OR name LIKE %s)" 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" params.extend([page_size, offset]) @@ -50,12 +55,17 @@ class AdminService: # 获取总数 count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1" + count_params = [] if search: 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: total_result = await execute_one(count_sql) - total = total_result["total"] if total_result else 0 return { diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index c4da850..105464e 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -235,6 +235,7 @@ class ConductService: student_id=student_id, start_date=start_date, end_date=end_date, + related_type=related_type, page=page, page_size=page_size ) diff --git a/backend/services/homework_service.py b/backend/services/homework_service.py deleted file mode 100644 index 788b825..0000000 --- a/backend/services/homework_service.py +++ /dev/null @@ -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": "状态更新成功"} \ No newline at end of file diff --git a/backend/services/parent_service.py b/backend/services/parent_service.py index 2f8eec7..9a17ccc 100644 --- a/backend/services/parent_service.py +++ b/backend/services/parent_service.py @@ -15,7 +15,6 @@ from typing import Dict, Any, Optional, List from models.user import UserModel from models.student import StudentModel from models.conduct import ConductModel -from models.homework import HomeworkModel from models.attendance import AttendanceModel from utils.logger import get_logger @@ -46,24 +45,6 @@ class ParentService: } @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]: """获取子女考勤记录""" user = await UserModel.get_by_user_id(parent_id) diff --git a/backend/services/student_service.py b/backend/services/student_service.py index 8ce50be..403f354 100644 --- a/backend/services/student_service.py +++ b/backend/services/student_service.py @@ -14,9 +14,9 @@ from datetime import datetime, timedelta from models.student import StudentModel from models.conduct import ConductModel -from models.homework import HomeworkModel from models.attendance import AttendanceModel from middleware.permission import PermissionChecker +from utils.database import execute_query from utils.logger import get_logger logger = get_logger(__name__) @@ -57,29 +57,33 @@ class StudentService: @staticmethod async def get_homework_status(student_id: int) -> Dict[str, Any]: - """获取学生作业情况""" + """获取学生作业扣分记录""" student = await StudentModel.get_by_id(student_id) if not student: 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) - submitted = sum(1 for h in homework if h["status"] == "submitted") - not_submitted = sum(1 for h in homework if h["status"] == "not_submitted") - late = sum(1 for h in homework if h["status"] == "late") + total = len(records) + deductions = sum(1 for r in records if r["points_change"] < 0) return { "student_id": student_id, "student_name": student["name"], "statistics": { "total": total, - "submitted": submitted, - "not_submitted": not_submitted, - "late": late + "deductions": deductions }, - "homework": homework + "homework": records } @staticmethod diff --git a/frontend/admin/conduct.php b/frontend/admin/conduct.php index c894432..633dd53 100644 --- a/frontend/admin/conduct.php +++ b/frontend/admin/conduct.php @@ -35,6 +35,7 @@ include __DIR__ . '/../includes/header.php';
+ @@ -110,4 +111,40 @@ include __DIR__ . '/../includes/header.php';
+ + + \ No newline at end of file diff --git a/frontend/admin/dashboard.php b/frontend/admin/dashboard.php index 0f11295..735ac4e 100644 --- a/frontend/admin/dashboard.php +++ b/frontend/admin/dashboard.php @@ -154,7 +154,7 @@ include __DIR__ . '/../includes/header.php'; } 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(data) { if (data.success) { diff --git a/frontend/admin/history.php b/frontend/admin/history.php index 6f417e5..d4699fe 100644 --- a/frontend/admin/history.php +++ b/frontend/admin/history.php @@ -54,7 +54,7 @@ include __DIR__ . '/../includes/header.php';
diff --git a/frontend/api/check_upgrade.php b/frontend/api/check_upgrade.php index 728c3f8..6a09d7c 100644 --- a/frontend/api/check_upgrade.php +++ b/frontend/api/check_upgrade.php @@ -1,7 +1,6 @@ '数据库配置文件不存在']); +// 从 session 获取 JWT token +$token = $_SESSION['jwt_token'] ?? ''; +if (empty($token)) { + echo json_encode(['error' => '会话已过期,请重新登录']); exit(); } -$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); -$dbConfig = []; -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); - } +// 调用后端 API +$apiUrl = API_BASE_URL . '/api/upgrade/check'; + +$ch = curl_init(); +curl_setopt_array($ch, [ + CURLOPT_URL => $apiUrl, + CURLOPT_RETURNTRANSFER => true, + 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)) { + echo json_encode(['error' => '无法连接升级服务']); + exit(); } -$required = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME']; -foreach ($required as $key) { - if (!isset($dbConfig[$key]) || $dbConfig[$key] === '') { - echo json_encode(['error' => "缺少数据库配置: {$key}"]); - exit(); - } +$result = json_decode($apiResponse, true); +if (!$result || !isset($result['success']) || !$result['success']) { + echo json_encode(['error' => $result['message'] ?? '升级检查失败']); + exit(); } -try { - $dsn = "mysql:host={$dbConfig['DB_HOST']};port={$dbConfig['DB_PORT']};dbname={$dbConfig['DB_NAME']};charset=utf8mb4"; - $pdo = new PDO($dsn, $dbConfig['DB_USER'], $dbConfig['DB_PASSWORD'], [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - ]); - - // 检测当前版本 - $currentVersion = '0.0.0'; - try { - $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) { - // 表不存在,使用默认值 - } - - // 读取目标版本 - $versionFile = __DIR__ . '/../../VERSION'; - if (!file_exists($versionFile)) { - echo json_encode(['error' => 'VERSION 文件不存在']); - exit(); - } - $targetVersion = trim(file_get_contents($versionFile)); - - $needsUpgrade = version_compare($targetVersion, $currentVersion, '>'); - - $allVersions = [ - '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', - ]; - $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()]); -} +// 转发后端返回的升级数据 +echo json_encode($result['data']); diff --git a/frontend/api/execute_upgrade.php b/frontend/api/execute_upgrade.php new file mode 100644 index 0000000..5cb4841 --- /dev/null +++ b/frontend/api/execute_upgrade.php @@ -0,0 +1,89 @@ + 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']); diff --git a/frontend/api/save_session.php b/frontend/api/save_session.php index c1b3c6d..6d0b4f0 100644 --- a/frontend/api/save_session.php +++ b/frontend/api/save_session.php @@ -160,6 +160,7 @@ $_SESSION['username'] = $data['username']; $_SESSION['real_name'] = $data['real_name'] ?? ''; $_SESSION['role'] = $data['role'] ?? ''; $_SESSION['login_time'] = time(); +$_SESSION['jwt_token'] = $token; // 如果是学生,额外设置 student_id if ($data['user_type'] === 'student') { diff --git a/frontend/assets/js/common.js b/frontend/assets/js/common.js index c57cd0f..a3b2160 100644 --- a/frontend/assets/js/common.js +++ b/frontend/assets/js/common.js @@ -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')}`; } -function getStatusBadge(status, type = 'homework') { +function getStatusBadge(status, type = 'attendance') { const statusMap = { - homework: { - 'submitted': '已提交', - 'not_submitted': '未提交', - 'late': '迟交' - }, attendance: { 'present': '出勤', 'absent': '缺勤', @@ -146,15 +141,13 @@ function getStatusBadge(status, type = 'homework') { 'leave': '请假' } }; - const texts = statusMap[type] || statusMap.homework; + const texts = statusMap[type] || statusMap.attendance; const text = texts[status] || status; let className = 'status-badge '; switch (status) { - case 'submitted': case 'present': className += 'status-submitted'; break; - case 'not_submitted': case 'absent': className += 'status-not_submitted'; break; diff --git a/frontend/assets/js/conduct.js b/frontend/assets/js/conduct.js index 259b8d4..78d4bc5 100644 --- a/frontend/assets/js/conduct.js +++ b/frontend/assets/js/conduct.js @@ -118,11 +118,113 @@ async function exportMoralityRecords() { console.error('导出失败:', err); } } +// 宿舍集体加分相关 +var dormitoryStudentIds = []; + +async function showDormitoryPointsModal() { + dormitoryStudentIds = []; + document.getElementById('dormitorySelect').innerHTML = ''; + 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 = '

该宿舍暂无学生

'; + 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 = `${escapeHtml(s.name)}${escapeHtml(s.student_no)}`; + studentsList.appendChild(div); + }); + studentsCount.textContent = `共 ${students.length} 人`; + } + studentsGroup.style.display = 'block'; + } else { + studentsList.innerHTML = '

加载失败

'; + 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(); window.loadStudents = loadStudents; window.showSinglePointsModal = showSinglePointsModal; window.exportMoralityRecords = exportMoralityRecords; +window.showDormitoryPointsModal = showDormitoryPointsModal; +window.onDormitorySelected = onDormitorySelected; +window.submitDormitoryPoints = submitDormitoryPoints; })(); diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js index 7695740..fea9407 100644 --- a/frontend/assets/js/dashboard.js +++ b/frontend/assets/js/dashboard.js @@ -25,9 +25,15 @@ async function loadDashboard() { } let quickActions = ''; - if (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') { + if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') { quickActions += ''; } + if (role === '班主任' || role === '学习委员') { + quickActions += ''; + } + if (role === '班主任' || role === '考勤委员') { + quickActions += ''; + } if (role === '班主任') { quickActions += ''; quickActions += ''; diff --git a/frontend/assets/js/history.js b/frontend/assets/js/history.js index 939c043..bab68f9 100644 --- a/frontend/assets/js/history.js +++ b/frontend/assets/js/history.js @@ -40,7 +40,7 @@ async function loadHistory(page = 1) { end_date: endDate }; if (studentId) params.student_id = studentId; - if (relatedType && !isGrouped) params.related_type = relatedType; + if (relatedType) params.related_type = relatedType; if (isGrouped) params.grouped = true; const res = await apiGet('/api/admin/conduct/history', params); diff --git a/frontend/assets/js/modules/student-mgmt.js b/frontend/assets/js/modules/student-mgmt.js index a884e5e..91df684 100644 --- a/frontend/assets/js/modules/student-mgmt.js +++ b/frontend/assets/js/modules/student-mgmt.js @@ -146,7 +146,7 @@ const students = data.students || []; let html = '

预览数据

'; - html += ''; + html += ''; html += ''; students.forEach(s => { @@ -154,6 +154,7 @@ + `; }); diff --git a/frontend/assets/js/student-homework.js b/frontend/assets/js/student-homework.js index 1860f23..4ff9d5e 100644 --- a/frontend/assets/js/student-homework.js +++ b/frontend/assets/js/student-homework.js @@ -16,25 +16,18 @@ async function loadHomework() { const res = await apiGet(`/api/student/homework/${STUDENT_ID}`); if (res && res.success) { let html = ''; - res.data.homework.forEach(hw => { - // 提交状态 - let statusDisplay = '-'; - if (hw.status) { - statusDisplay = getStatusBadge(hw.status, 'homework'); - } - // 扣分显示 - const pointsDisplay = hw.points ? `${hw.points}分` : '-'; - + res.data.homework.forEach(record => { + const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; + const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e'; html += ` - - - - - + + + + `; }); if (res.data.homework.length === 0) { - html = ''; + html = ''; } document.getElementById('homeworkList').innerHTML = html; } diff --git a/frontend/assets/js/subjects.js b/frontend/assets/js/subjects.js deleted file mode 100644 index 628c80a..0000000 --- a/frontend/assets/js/subjects.js +++ /dev/null @@ -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 += ` -
- ${escapeHtml(sub.subject_name)} - ${escapeHtml(sub.subject_code || '')} - - ${sub.is_active ? '启用' : '禁用'} - - - - -
- `; - }); - if (res.data.subjects.length === 0) { - html = '

暂无科目,请点击"添加科目"

'; - } - 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; - -})(); diff --git a/frontend/assets/uploads/sample_import.json b/frontend/assets/uploads/sample_import.json index 944c300..279cf25 100644 --- a/frontend/assets/uploads/sample_import.json +++ b/frontend/assets/uploads/sample_import.json @@ -7,37 +7,42 @@ "_comment6": " student_no - 必填,学生学号,唯一标识", "_comment7": " name - 必填,学生姓名", "_comment8": " parent_phone - 可选,家长手机号(11位手机号)", - "_comment9": " password - 可选,初始密码,不填则默认 123456", - "_comment10": "================================================", - "_comment11": "导入规则:", - "_comment12": " 1. 学生操行分初始值 = 60分", - "_comment13": " 2. 学生账号 = 学号,密码 = 指定的password或123456", - "_comment14": " 3. 家长账号 = 手机号(若parent_phone有值),密码 = 指定的password或123456", - "_comment15": " 4. 家长姓名默认显示为 '学生姓名家长'", - "_comment16": "================================================", + "_comment9": " dormitory_number - 可选,宿舍号(支持字母数字组合,如 301-A)", + "_comment10": " password - 可选,初始密码,不填则默认 123456", + "_comment11": "================================================", + "_comment12": "导入规则:", + "_comment13": " 1. 学生操行分初始值 = 60分", + "_comment14": " 2. 学生账号 = 学号,密码 = 指定的password或123456", + "_comment15": " 3. 家长账号 = 手机号(若parent_phone有值),密码 = 指定的password或123456", + "_comment16": " 4. 家长姓名默认显示为 '学生姓名家长'", + "_comment17": "================================================", "students": [ { "student_no": "20240001", "name": "张三", "parent_phone": "13800138001", + "dormitory_number": "301-A", "password": "123456" }, { "student_no": "20240002", "name": "李四", "parent_phone": "13800138002", + "dormitory_number": "205", "password": "123456" }, { "student_no": "20240003", "name": "王五", "parent_phone": "", + "dormitory_number": "", "password": "" }, { "student_no": "20240004", "name": "赵六", "parent_phone": "13800138004", + "dormitory_number": "102-B", "password": "" } ] diff --git a/frontend/includes/nav.php b/frontend/includes/nav.php index a3b8631..0635bda 100644 --- a/frontend/includes/nav.php +++ b/frontend/includes/nav.php @@ -1,7 +1,7 @@ -
-
缺交次数
-
--
-
@@ -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; document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`; diff --git a/frontend/student/dashboard.php b/frontend/student/dashboard.php index 9e795d6..8c3fb29 100644 --- a/frontend/student/dashboard.php +++ b/frontend/student/dashboard.php @@ -94,7 +94,7 @@ include __DIR__ . '/../includes/header.php';
--
-
缺交次数
+
作业扣分
--
@@ -140,11 +140,10 @@ include __DIR__ . '/../includes/header.php';
学号姓名家长手机号初始密码学号姓名家长手机号宿舍号初始密码
${escapeHtml(s.student_no || '')} ${escapeHtml(s.name || '')} ${escapeHtml(s.parent_phone || '')}${escapeHtml(s.dormitory_number || '-')} ${escapeHtml(s.password || '123456')}
${escapeHtml(hw.title)}${escapeHtml(hw.subject_name)}${hw.deadline || '-'}${statusDisplay}${pointsDisplay}${formatDateTime(record.created_at)}${record.points_change > 0 ? '+' : ''}${record.points_change}${escapeHtml(record.reason)}${escapeHtml(record.recorder_name || '-')}
📝 暂无作业记录
📝 暂无作业扣分记录
- - - + + @@ -319,14 +318,13 @@ include __DIR__ . '/../includes/header.php'; document.getElementById('studentRank').textContent = '--'; } } - // 获取作业统计 - 缺交次数 + // 获取作业扣分统计 const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`); if (homeworkRes && homeworkRes.success) { const stats = homeworkRes.data.statistics; - const notSubmitted = (stats.not_submitted || 0) + (stats.late || 0); + const deductions = stats.deductions || 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}`); if (res && res.success) { let html = ''; - res.data.homework.forEach(hw => { - const pointsDisplay = hw.points ? hw.points + '分' : '-'; + res.data.homework.forEach(record => { + const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; html += ` - - - - - + + + + `; }); if (res.data.homework.length === 0) { - html = ''; + html = ''; } document.getElementById('homeworkList').innerHTML = html; } } catch (error) { - console.error('加载作业失败:', error); + console.error('加载作业记录失败:', error); } } diff --git a/frontend/student/homework.php b/frontend/student/homework.php index c951b77..fe7c9de 100644 --- a/frontend/student/homework.php +++ b/frontend/student/homework.php @@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
科目 时间 分值备注作业原因操作人
${escapeHtml(hw.subject_name)}${hw.deadline || hw.created_at}${pointsDisplay}${escapeHtml(hw.comments || '-')}${escapeHtml(hw.title)}${formatDateTime(record.created_at)}${record.points_change > 0 ? '+' : ''}${record.points_change}${escapeHtml(record.reason)}${escapeHtml(record.recorder_name || '-')}
暂无作业
暂无作业扣分记录
- +
作业名称科目截止时间提交状态扣分
时间分值原因操作人
diff --git a/sql/init.sql b/sql/init.sql index 99f7dc0..38c0b2c 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -202,6 +202,26 @@ CREATE TABLE IF NOT EXISTS `semester_archives` ( FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ) 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; -- 插入初始科目(仅语数英,如不存在) @@ -210,4 +230,10 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL ('数学', 'MATH', 2), ('英语', '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; diff --git a/sql/upgrades/v2.2.sql b/sql/upgrades/v2.2.sql new file mode 100644 index 0000000..d9f8816 --- /dev/null +++ b/sql/upgrades/v2.2.sql @@ -0,0 +1,11 @@ +-- =========================================== +-- 班级操行分管理系统 - v2.1 → v2.2 升级脚本 +-- 字符集: utf8mb4 +-- +-- 说明: v2.2 为安全修复与功能增强版本,无数据库 schema 变更。 +-- 主要变更: +-- 1. 修复管理员操作越权漏洞(加分/撤销/恢复操作增加权限校验) +-- 2. 新增宿舍集体加分功能(前端+后端) +-- 3. 学生导入支持宿舍号字段 +-- 4. 导入预览表格显示宿舍号列 +-- =========================================== diff --git a/upgrade.php b/upgrade.php index 5258b9c..5deff33 100644 --- a/upgrade.php +++ b/upgrade.php @@ -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 文件并解析数据库配置 */ @@ -59,16 +69,10 @@ function detectCurrentVersion($pdo) { * 获取需要执行的升级步骤 */ function getUpgradeSteps($currentVersion, $targetVersion) { - $allVersions = [ - '1.7' => __DIR__ . '/sql/upgrades/v1.7.sql', - '1.8' => __DIR__ . '/sql/upgrades/v1.8.sql', - '2.0' => __DIR__ . '/sql/upgrades/v2.0.sql', - '2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql', - '2.1' => __DIR__ . '/sql/upgrades/v2.1.sql', - ]; + global $UPGRADE_VERSIONS; $steps = []; - foreach ($allVersions as $version => $sqlFile) { + foreach ($UPGRADE_VERSIONS as $version => $sqlFile) { if (version_compare($version, $currentVersion, '>') && version_compare($version, $targetVersion, '<=')) { $steps[$version] = $sqlFile; @@ -128,19 +132,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'step') ]); // 获取该版本对应的 SQL 文件 - $allVersions = [ - '1.7' => __DIR__ . '/sql/upgrades/v1.7.sql', - '1.8' => __DIR__ . '/sql/upgrades/v1.8.sql', - '2.0' => __DIR__ . '/sql/upgrades/v2.0.sql', - '2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql', - '2.1' => __DIR__ . '/sql/upgrades/v2.1.sql', - ]; - - if (!isset($allVersions[$stepVersion])) { + if (!isset($UPGRADE_VERSIONS[$stepVersion])) { throw new RuntimeException("未知版本: {$stepVersion}"); } - $sqlFile = $allVersions[$stepVersion]; + $sqlFile = $UPGRADE_VERSIONS[$stepVersion]; $shortFile = basename($sqlFile); executeUpgrade($pdo, $stepVersion, $sqlFile); @@ -432,6 +428,15 @@ try {
错误:
+ +
+ 💡 解决方法:
+ 1. 进入 backend/ 目录
+ 2. 复制配置模板:cp .env.example .env
+ 3. 编辑 .env 文件,填入实际的数据库连接信息
+ 4. 刷新此页面 +
+
✓ 数据库已是最新版本,无需升级。