From 439c0745347b7cc3860a2fdb60fb60759de683b4 Mon Sep 17 00:00:00 2001 From: canglan Date: Mon, 27 Apr 2026 01:15:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=80=83=E5=8B=A4=E7=AE=A1?= =?UTF-8?q?=E7=90=86bug=E5=B9=B6=E5=8A=A0=E5=BC=BA=E4=BA=86=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E4=BF=9D=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config.py | 5 +- backend/middleware/auth_middleware.py | 8 ++-- backend/middleware/permission.py | 1 + backend/models/conduct.py | 20 +++++--- backend/models/user.py | 7 +++ backend/routes/admin.py | 40 ++++++++++------ backend/routes/semester.py | 12 ++--- backend/services/attendance_service.py | 5 +- backend/services/auth_service.py | 3 +- backend/services/conduct_service.py | 11 ++++- backend/utils/jwt_handler.py | 3 +- frontend/admin/admins.php | 2 +- frontend/admin/attendance.php | 23 +++++++--- frontend/admin/history.php | 20 +++++++- frontend/admin/students.php | 2 +- frontend/admin/subjects.php | 63 ++++++++++++++++++++++++++ 16 files changed, 176 insertions(+), 49 deletions(-) diff --git a/backend/config.py b/backend/config.py index 15d7b49..05b3bd8 100644 --- a/backend/config.py +++ b/backend/config.py @@ -44,7 +44,8 @@ class Settings: JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "") JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") - JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30")) + JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "60")) + JWT_IDLE_TIMEOUT_MINUTES: int = int(os.getenv("JWT_IDLE_TIMEOUT_MINUTES", "10")) PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "") DEBUG_ENABLED: bool = os.getenv("DEBUG_ENABLED", "False").lower() == "true" @@ -63,7 +64,7 @@ class Settings: MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5")) STUDY_COMMISSIONER_MAX_POINTS: int = int(os.getenv("STUDY_COMMISSIONER_MAX_POINTS", "5")) - ATTENDANCE_REP_MAX_POINTS: int = int(os.getenv("ATTENDANCE_REP_MAX_POINTS", "5")) + ATTENDANCE_REP_MAX_POINTS: int = int(os.getenv("ATTENDANCE_REP_MAX_POINTS", "8")) LABOR_REP_MAX_POINTS: int = int(os.getenv("LABOR_REP_MAX_POINTS", "1")) VOLUNTEER_REP_MAX_POINTS: int = int(os.getenv("VOLUNTEER_REP_MAX_POINTS", "5")) diff --git a/backend/middleware/auth_middleware.py b/backend/middleware/auth_middleware.py index f6fcbfd..0884c2b 100644 --- a/backend/middleware/auth_middleware.py +++ b/backend/middleware/auth_middleware.py @@ -92,15 +92,16 @@ class AuthMiddleware(BaseHTTPMiddleware): logger.warning(f"[Auth] {path} - Redis Token不匹配, user_id={user_id}, stored={'有' if stored_token else '无'}") return self._cors_response(request, 401, "令牌已失效,请重新登录") + # 将用户信息存储到request.state # 将用户信息存储到request.state request.state.user_id = payload.get("user_id") request.state.username = payload.get("username") + request.state.real_name = payload.get("real_name") or payload.get("username") request.state.user_type = payload.get("user_type") request.state.student_id = payload.get("student_id") request.state.role = payload.get("role") - - # 刷新Token过期时间 - await RedisClient.expire(f"user_token:{user_id}", settings.JWT_EXPIRE_MINUTES * 60) + # 刷新Token过期时间(空闲超时:10分钟无操作则需重新登录) + await RedisClient.expire(f"user_token:{user_id}", settings.JWT_IDLE_TIMEOUT_MINUTES * 60) logger.debug(f"[Auth] {path} - 认证成功, user_id={user_id}, username={payload.get('username')}") @@ -148,6 +149,7 @@ async def get_current_user(request: Request) -> Dict[str, Any]: return { "user_id": request.state.user_id, "username": request.state.username, + "real_name": getattr(request.state, 'real_name', None) or request.state.username, "user_type": request.state.user_type, "student_id": request.state.student_id, "role": request.state.role diff --git a/backend/middleware/permission.py b/backend/middleware/permission.py index 42539aa..5ed5769 100644 --- a/backend/middleware/permission.py +++ b/backend/middleware/permission.py @@ -126,6 +126,7 @@ class PermissionChecker: 检查是否可以撤销扣分记录 班主任:可以撤销任何记录 班长:可以撤销任何记录 + 考勤委员:可以撤销自己的记录 其他:只能撤销自己的记录 """ sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s" diff --git a/backend/models/conduct.py b/backend/models/conduct.py index 0d120eb..9c10124 100644 --- a/backend/models/conduct.py +++ b/backend/models/conduct.py @@ -83,20 +83,24 @@ class ConductModel: start_date: str = None, end_date: str = None, student_id: int = None, - include_revoked: bool = True + include_revoked: bool = True, + related_type: str = None ) -> List[Dict[str, Any]]: - """获取所有记录(班主任/班长专用)""" + """获取所有记录(班主任/班长专用)""" # 空字符串转为None if start_date == "": start_date = None if end_date == "": end_date = None - + if related_type == "": + related_type = None sql = """ - SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name + SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name, + ru.real_name as revoker_name FROM conduct_records cr JOIN students s ON cr.student_id = s.student_id JOIN users u ON cr.recorder_id = u.user_id + LEFT JOIN users ru ON cr.revoked_by = ru.user_id WHERE 1=1 """ if not include_revoked: @@ -115,6 +119,10 @@ class ConductModel: sql += " AND DATE(cr.created_at) <= %s" params.append(end_date) + if related_type: + sql += " AND cr.related_type = %s" + params.append(related_type) + sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s" params.extend([limit, offset]) @@ -128,7 +136,7 @@ class ConductModel: page: int = 1, page_size: int = 20 ) -> Dict[str, Any]: - """获取分组后的操行分记录(同批次合并)""" + """获取分组后的操行分记录(同批次合并)""" if start_date == "": start_date = None if end_date == "": @@ -215,7 +223,7 @@ class ConductModel: @staticmethod async def restore_record(record_id: int, restorer_id: int) -> bool: - """反撤销(恢复)已撤销的记录""" + """反撤销(恢复)已撤销的记录""" try: sql = """ UPDATE conduct_records diff --git a/backend/models/user.py b/backend/models/user.py index 2f7fe28..f993d87 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -105,4 +105,11 @@ class UserModel: """更新用户状态(0=禁用,1=启用)""" sql = "UPDATE users SET status = %s WHERE user_id = %s" result = await execute_update(sql, (status, user_id)) + return result > 0 + + @staticmethod + async def update_real_name(user_id: int, real_name: str) -> bool: + """更新用户真实姓名""" + sql = "UPDATE users SET real_name = %s WHERE user_id = %s" + result = await execute_update(sql, (real_name, user_id)) return result > 0 \ No newline at end of file diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 9fa538f..4338f46 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -87,7 +87,7 @@ async def import_students(request: Request, file: UploadFile = File(...)): initial_points=60 ) await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="import_students", target_type="student", details=f"批量导入: 成功{result['success_count']}人, 失败{result['failed_count']}人", @@ -113,7 +113,7 @@ async def add_student(request: Request, req: AddStudentRequest): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="add_student", target_type="student", target_id=result.get("student_id"), details=f"新增学生: {req.name}({req.student_no})", @@ -192,7 +192,7 @@ async def add_conduct_points(request: Request, req: AddPointsRequest): if result["success"]: role = await PermissionChecker.get_user_role(user["user_id"]) await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role=role, operation_type="add_points", target_type="conduct", details=f"批量加减分: {req.points_change}分, 原因: {req.reason}, 对象: {req.student_ids}", @@ -215,7 +215,7 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest): role = await PermissionChecker.get_user_role(user["user_id"]) record = result.get("record", {}) await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role=role, operation_type="revoke_record", target_type="conduct", target_id=req.record_id, details=( @@ -242,7 +242,7 @@ async def restore_conduct_record(request: Request, req: RevokeRequest): if result["success"]: record = result.get("record", {}) await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="restore_record", target_type="conduct", target_id=req.record_id, details=( @@ -266,7 +266,8 @@ async def get_conduct_history( page_size: int = Query(20, ge=1, le=1000), start_date: Optional[str] = None, end_date: Optional[str] = None, - grouped: bool = Query(False) + grouped: bool = Query(False), + related_type: Optional[str] = None ): """获取操行分历史记录""" try: @@ -278,7 +279,8 @@ async def get_conduct_history( page_size=page_size, start_date=start_date, end_date=end_date, - grouped=grouped + grouped=grouped, + related_type=related_type ) return success_response(data=result) except Exception as e: @@ -335,7 +337,7 @@ async def create_assignment( ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="create_assignment", target_type="homework", details=f"发布作业: {title}", @@ -362,7 +364,7 @@ async def update_submission_status(request: Request, req: UpdateHomeworkStatusRe ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + 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}", @@ -393,7 +395,7 @@ async def add_attendance(request: Request, req: AddAttendanceRequest): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role=role, operation_type="add_attendance", target_type="attendance", details=f"学生ID: {req.student_id}, 日期: {req.date}, 状态: {req.status}", @@ -440,7 +442,7 @@ async def add_admin(request: Request, req: AddAdminRequest): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="add_admin", target_type="admin", details=f"新增管理员: {req.real_name}({req.username}), 角色: {req.role_type}", @@ -477,16 +479,24 @@ async def update_admin(request: Request, user_id: int, req: UpdateAdminRequest): return error_response(message="无效的角色类型", code=400) from models.admin_role import AdminRoleModel + from models.user import UserModel + + # 更新角色 result = await AdminRoleModel.update_role( user_id=user_id, role_type=req.role_type ) + + # 更新姓名 + if req.real_name: + await UserModel.update_real_name(user_id, req.real_name) + if result: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="update_admin", target_type="admin", target_id=user_id, - details=f"更新管理员角色为: {req.role_type}", + details=f"更新管理员角色为: {req.role_type}, 姓名: {req.real_name}", ip=request.client.host ) return success_response(message="管理员更新成功") @@ -515,7 +525,7 @@ async def delete_admin(request: Request, user_id: int): # 再删除用户账号(软删除,将状态设为禁用) await UserModel.update_status(user_id, 0) await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="delete_admin", target_type="admin", target_id=user_id, details=f"删除管理员: ID={user_id}", @@ -548,7 +558,7 @@ async def reset_admin_password(request: Request, user_id: int, req: ResetPasswor updated = await UserModel.update_password(user_id, req.new_password) if updated: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="reset_password", target_type="admin", target_id=user_id, details=f"重置管理员密码: {target_user['real_name']}({target_user['username']})", diff --git a/backend/routes/semester.py b/backend/routes/semester.py index e76b704..a3e0115 100644 --- a/backend/routes/semester.py +++ b/backend/routes/semester.py @@ -67,7 +67,7 @@ async def create_semester(request: Request, req: CreateSemesterRequest): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="create_semester", target_type="semester", target_id=result.get("semester_id"), details=f"创建学期: {req.semester_name}", @@ -92,7 +92,7 @@ async def activate_semester(request: Request, semester_id: int): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="activate_semester", target_type="semester", target_id=semester_id, details=f"激活学期ID: {semester_id}", @@ -120,7 +120,7 @@ async def update_semester(request: Request, semester_id: int, req: UpdateSemeste ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="update_semester", target_type="semester", target_id=semester_id, details=f"编辑学期ID: {semester_id}", @@ -145,7 +145,7 @@ async def delete_semester(request: Request, semester_id: int): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="delete_semester", target_type="semester", target_id=semester_id, details=f"删除学期ID: {semester_id}", @@ -170,7 +170,7 @@ async def associate_records(request: Request, semester_id: int): ) if result["success"]: await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="associate_records", target_type="semester", target_id=semester_id, details=f"关联数据到学期ID: {semester_id}, 结果: {result.get('data', {})}", @@ -203,7 +203,7 @@ async def archive_semester( if reset_scores: log_detail += " 并重置学生操行分" await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["username"], + operator_id=user["user_id"], operator_name=user["real_name"], operator_role="班主任", operation_type="archive_semester", target_type="semester", target_id=semester_id, details=log_detail, diff --git a/backend/services/attendance_service.py b/backend/services/attendance_service.py index 24ab0f0..79e327d 100644 --- a/backend/services/attendance_service.py +++ b/backend/services/attendance_service.py @@ -82,11 +82,12 @@ class AttendanceService: user = await UserModel.get_by_user_id(recorder_id) recorder_name = user.get("real_name", "班主任") if user else "班主任" # 使用中文状态 + # 使用中文状态 status_text = ATTENDANCE_STATUS_MAP.get(status, status) await ConductModel.create_record( student_id=student_id, points_change=points_change, - reason=f"考勤异常: {status_text}", + reason=f"考勤:{status_text}", recorder_id=recorder_id, recorder_name=recorder_name, related_type="attendance", @@ -98,8 +99,6 @@ class AttendanceService: # 标记已应用扣分 await AttendanceModel.mark_deduction_applied(attendance_id) - await AttendanceModel.mark_deduction_applied(attendance_id) - logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status}") return {"success": True, "message": "考勤记录添加成功"} diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py index c224e13..6ea675e 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -76,7 +76,8 @@ class AuthService: username=user["username"], user_type=user["user_type"], student_id=user["student_id"], - role=role + role=role, + real_name=user["real_name"] ) # 存储Token到Redis diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index facd2ee..2f01739 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -196,7 +196,8 @@ class ConductService: page_size: int = 20, start_date: Optional[str] = None, end_date: Optional[str] = None, - grouped: bool = False + grouped: bool = False, + related_type: Optional[str] = None ) -> Dict[str, Any]: """获取历史记录""" # 空字符串转为None @@ -204,6 +205,8 @@ class ConductService: start_date = None if end_date == "": end_date = None + if related_type == "": + related_type = None role = await PermissionChecker.get_user_role(user_id) offset = (page - 1) * page_size @@ -224,7 +227,8 @@ class ConductService: offset=offset, start_date=start_date, end_date=end_date, - student_id=student_id + student_id=student_id, + related_type=related_type ) # 获取总数 @@ -240,6 +244,9 @@ class ConductService: if end_date: count_conditions.append("DATE(cr.created_at) <= %s") count_params.append(end_date) + if related_type: + count_conditions.append("cr.related_type = %s") + count_params.append(related_type) count_where = " AND ".join(count_conditions) count_sql = f""" SELECT COUNT(*) as total FROM conduct_records cr diff --git a/backend/utils/jwt_handler.py b/backend/utils/jwt_handler.py index 62b3d76..1862fbe 100644 --- a/backend/utils/jwt_handler.py +++ b/backend/utils/jwt_handler.py @@ -23,7 +23,7 @@ class JWTHandler: """JWT Token处理类""" @staticmethod - def create_token(user_id: int, username: str, user_type: str, student_id: int = None, role: str = None) -> str: + def create_token(user_id: int, username: str, user_type: str, student_id: int = None, role: str = None, real_name: str = None) -> str: """ 创建JWT Token """ @@ -33,6 +33,7 @@ class JWTHandler: 'user_type': user_type, 'student_id': student_id, 'role': role, + 'real_name': real_name, 'exp': datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES), 'iat': datetime.utcnow(), 'iss': settings.APP_NAME diff --git a/frontend/admin/admins.php b/frontend/admin/admins.php index 88a1c06..27acf0f 100644 --- a/frontend/admin/admins.php +++ b/frontend/admin/admins.php @@ -100,7 +100,7 @@ include __DIR__ . '/../includes/header.php';
- +
diff --git a/frontend/admin/attendance.php b/frontend/admin/attendance.php index e542ee6..7c54fab 100644 --- a/frontend/admin/attendance.php +++ b/frontend/admin/attendance.php @@ -82,6 +82,11 @@ include __DIR__ . '/../includes/header.php';
+