diff --git a/README.md b/README.md index 9e4d223..c8906d4 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,8 @@ classmanager/ | v1.1 | 2026.4.20 | 更新家长端查看加减分记录功能 | | v1.2 | 2026.4.22 | 学期管理、env配置加减分上限、排行榜百分比筛选、撤销操作日志、调试入口开关 | | v1.3 | 2026.4.27 | 考勤时段系统(早上/中午/晚修三时段)、历史记录扣分类型筛选、管理员/科目信息编辑、全链路输入安全校验 | -| v1.4 | 2026.4.28 | 全量代码审查修复:双重密码哈希bug、学生端XSS漏洞、性能优化(COUNT替代全量加载)、Pydantic schema统一、权限检查补全、考勤委员撤销权限 | +| v1.4 | 2026.4.28 | 全量代码审查修复:双重密码哈希bug、学生端XSS漏洞、性能优化、Pydantic schema统一、权限检查补全、考勤委员撤销权限 | +| v1.5 | 2026.4.29 | 用户反馈修复:登录封禁5分钟+手动解锁、加减分回显修复、学习委员5分限制修复、按钮样式补全 | ## 许可证 diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 81fa719..bee850d 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -23,12 +23,13 @@ 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, UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest, - CreateAssignmentRequest + CreateAssignmentRequest, UnlockUserRequest ) from utils.response import success_response, error_response from utils.logger import get_logger @@ -214,14 +215,17 @@ async def add_conduct_points(request: Request, req: AddPointsRequest): recorder_name=user["username"] ) 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["real_name"], - operator_role=role, operation_type="add_points", - target_type="conduct", - details=f"批量加减分: {req.points_change}分, 原因: {req.reason}, 对象: {req.student_ids}", - ip=request.client.host - ) + try: + role = await PermissionChecker.get_user_role(user["user_id"]) + await LogService.write_operation_log( + 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}", + ip=request.client.host + ) + except Exception as e: + logger.error(f"写入加减分操作日志失败: {e}") return success_response(data=result, message="操作成功") else: return error_response(message=result["message"]) @@ -592,4 +596,26 @@ async def reset_admin_password(request: Request, user_id: int, req: ResetPasswor ) return success_response(message="密码重置成功") else: - return error_response(message="密码重置失败") \ No newline at end of file + return error_response(message="密码重置失败") + + +# ========== 登录黑名单管理 ========== + +@router.post("/unlock-user") +async def unlock_user(request: Request, req: UnlockUserRequest): + """解除用户登录锁定(班主任)""" + 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) + + await RedisClient.clear_login_attempts(req.username) + + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["real_name"], + operator_role="班主任", operation_type="unlock_user", + target_type="user", + details=f"解除用户登录锁定: {req.username}", + ip=request.client.host + ) + return success_response(message=f"已解除用户 {req.username} 的登录锁定") \ No newline at end of file diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index 6a6b839..fa6d1ec 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -48,7 +48,7 @@ class ImportResult(BaseModel): class AddAdminRequest(BaseModel): """添加管理员请求""" - username: str = Field(..., min_length=2, max_length=50, pattern=r'^[a-zA-Z0-9_\u4e00-\u9fa]+$', description="登录账号") + username: str = Field(..., min_length=2, max_length=50, pattern=r'^[a-zA-Z0-9_\u4e00-\u9fa5]+$', description="登录账号") real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名") password: Optional[str] = Field(None, min_length=6, max_length=50, description="密码(不填则自动生成)") role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型") @@ -116,4 +116,9 @@ 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="截止日期") \ No newline at end of file + 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/services/auth_service.py b/backend/services/auth_service.py index 6ea675e..45687b7 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -37,7 +37,7 @@ class AuthService: attempts = await RedisClient.get(f"login_attempts:{username}") if attempts and int(attempts) >= 5: await LogService.write_login_log(username, 0, ip, user_agent, "登录失败次数过多") - return {"success": False, "message": "登录失败次数过多,请15分钟后重试"} + return {"success": False, "message": "登录失败次数过多,请5分钟后重试"} # 获取用户信息 user = await UserModel.get_by_username(username) diff --git a/backend/utils/redis_client.py b/backend/utils/redis_client.py index 9551030..3cd376b 100644 --- a/backend/utils/redis_client.py +++ b/backend/utils/redis_client.py @@ -130,7 +130,7 @@ class RedisClient: key = f"login_attempts:{username}" attempts = await RedisClient.get(key) attempts = int(attempts) + 1 if attempts else 1 - await RedisClient.set(key, attempts, 900) # 15分钟锁定 + await RedisClient.set(key, attempts, 300) # 5分钟锁定 return attempts @staticmethod diff --git a/frontend/admin/admins.php b/frontend/admin/admins.php index 54f78ba..abccb93 100644 --- a/frontend/admin/admins.php +++ b/frontend/admin/admins.php @@ -163,6 +163,7 @@ async function loadAdmins() { + `; @@ -253,7 +254,6 @@ async function deleteAdmin(userId, realName) { showToast(res?.message || '删除失败', 'error'); } } - function resetAdminPassword(userId, realName) { currentResetUserId = userId; document.getElementById('resetPasswordUserId').value = userId; @@ -262,6 +262,23 @@ function resetAdminPassword(userId, realName) { document.getElementById('resetPasswordModal').style.display = 'flex'; } +async function unlockUser(username, realName) { + if (!confirm(`确定要解除用户 "${realName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) { + return; + } + + const res = await apiPost('/api/admin/unlock-user', { + username: username + }); + + if (res && res.success) { + showToast(res.message || '解锁成功'); + } else { + showToast(res?.message || '解锁失败', 'error'); + } +} +} + async function submitResetPassword() { if (!currentResetUserId) return; diff --git a/frontend/admin/homework.php b/frontend/admin/homework.php index 2cf6218..796d623 100644 --- a/frontend/admin/homework.php +++ b/frontend/admin/homework.php @@ -83,8 +83,11 @@ include __DIR__ . '/../includes/header.php';
- - 每次加减分不超过 + +
@@ -103,7 +106,7 @@ var selectedStudentIds = []; const hwRole = ''; // 初始化扣分配置 -const hwMaxPoints = window.HOMEWORK_MAX_POINTS || 3; +const hwMaxPoints = hwRole === '班主任' ? 100 : (window.HOMEWORK_MAX_POINTS || 5); const hwNotSubmit = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2; const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1; diff --git a/frontend/admin/students.php b/frontend/admin/students.php index 57f27aa..9969d14 100644 --- a/frontend/admin/students.php +++ b/frontend/admin/students.php @@ -184,6 +184,7 @@ async function loadStudents(page = 1) { ${userRole === '班主任' ? ` + ` : ''} `; @@ -223,6 +224,23 @@ function toggleSelectAll() { } } +// 解锁学生登录 +async function unlockStudent(studentNo, studentName) { + if (!confirm(`确定要解除学生 "${studentName}" 的登录锁定吗?\n(适用于多次登录失败被禁止登录的情况)`)) { + return; + } + + const res = await apiPost('/api/admin/unlock-user', { + username: studentNo + }); + + if (res && res.success) { + showToast(res.message || '解锁成功'); + } else { + showToast(res?.message || '解锁失败', 'error'); + } +} + // 页面加载 loadStudents(); diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index 1f9e9fe..147525a 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -350,6 +350,33 @@ tr:hover { background: #2f855a; } +.btn-warning { + background: #d69e2e; + color: white; +} + +.btn-warning:hover { + background: #b7791f; +} + +.btn-info { + background: #3182ce; + color: white; +} + +.btn-info:hover { + background: #2b6cb0; +} + +.btn-secondary { + background: #718096; + color: white; +} + +.btn-secondary:hover { + background: #4a5568; +} + .btn-sm { padding: 4px 10px; font-size: 12px; diff --git a/frontend/config.php b/frontend/config.php index 7c6aa66..271ee7c 100644 --- a/frontend/config.php +++ b/frontend/config.php @@ -64,7 +64,7 @@ define('ICP_NUMBER', $config['ICP_NUMBER'] ?? ''); // 扣分规则配置(有默认值,不强制要求在.env中配置) define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2)); define('DEDUCTION_HOMEWORK_LATE', (int)($config['DEDUCTION_HOMEWORK_LATE'] ?? 1)); -define('HOMEWORK_MAX_POINTS', (int)($config['HOMEWORK_MAX_POINTS'] ?? 3)); +define('HOMEWORK_MAX_POINTS', (int)($config['HOMEWORK_MAX_POINTS'] ?? 5)); define('DEDUCTION_ATTENDANCE_ABSENT', (int)($config['DEDUCTION_ATTENDANCE_ABSENT'] ?? 3)); define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 1)); define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 0));