修复了一些已知问题
This commit is contained in:
@@ -265,7 +265,8 @@ classmanager/
|
|||||||
| v1.1 | 2026.4.20 | 更新家长端查看加减分记录功能 |
|
| v1.1 | 2026.4.20 | 更新家长端查看加减分记录功能 |
|
||||||
| v1.2 | 2026.4.22 | 学期管理、env配置加减分上限、排行榜百分比筛选、撤销操作日志、调试入口开关 |
|
| v1.2 | 2026.4.22 | 学期管理、env配置加减分上限、排行榜百分比筛选、撤销操作日志、调试入口开关 |
|
||||||
| v1.3 | 2026.4.27 | 考勤时段系统(早上/中午/晚修三时段)、历史记录扣分类型筛选、管理员/科目信息编辑、全链路输入安全校验 |
|
| 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分限制修复、按钮样式补全 |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ from services.conduct_service import ConductService
|
|||||||
from services.homework_service import HomeworkService
|
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 schemas.admin import (
|
from schemas.admin import (
|
||||||
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
||||||
AddStudentRequest, UpdateStudentRequest,
|
AddStudentRequest, UpdateStudentRequest,
|
||||||
UpdateHomeworkStatusRequest, AddAttendanceRequest,
|
UpdateHomeworkStatusRequest, AddAttendanceRequest,
|
||||||
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
|
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
|
||||||
CreateAssignmentRequest
|
CreateAssignmentRequest, 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
|
||||||
@@ -214,14 +215,17 @@ async def add_conduct_points(request: Request, req: AddPointsRequest):
|
|||||||
recorder_name=user["username"]
|
recorder_name=user["username"]
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
role = await PermissionChecker.get_user_role(user["user_id"])
|
try:
|
||||||
await LogService.write_operation_log(
|
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
await LogService.write_operation_log(
|
||||||
operator_role=role, operation_type="add_points",
|
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||||
target_type="conduct",
|
operator_role=role, operation_type="add_points",
|
||||||
details=f"批量加减分: {req.points_change}分, 原因: {req.reason}, 对象: {req.student_ids}",
|
target_type="conduct",
|
||||||
ip=request.client.host
|
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="操作成功")
|
return success_response(data=result, message="操作成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message=result["message"])
|
return error_response(message=result["message"])
|
||||||
@@ -593,3 +597,25 @@ async def reset_admin_password(request: Request, user_id: int, req: ResetPasswor
|
|||||||
return success_response(message="密码重置成功")
|
return success_response(message="密码重置成功")
|
||||||
else:
|
else:
|
||||||
return error_response(message="密码重置失败")
|
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} 的登录锁定")
|
||||||
@@ -48,7 +48,7 @@ class ImportResult(BaseModel):
|
|||||||
|
|
||||||
class AddAdminRequest(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="真实姓名")
|
real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名")
|
||||||
password: Optional[str] = Field(None, min_length=6, max_length=50, description="密码(不填则自动生成)")
|
password: Optional[str] = Field(None, min_length=6, max_length=50, description="密码(不填则自动生成)")
|
||||||
role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型")
|
role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型")
|
||||||
@@ -117,3 +117,8 @@ class CreateAssignmentRequest(BaseModel):
|
|||||||
title: str = Field(..., min_length=1, max_length=200, description="作业标题")
|
title: str = Field(..., min_length=1, max_length=200, description="作业标题")
|
||||||
description: Optional[str] = Field(None, max_length=1000, description="作业描述")
|
description: Optional[str] = Field(None, max_length=1000, description="作业描述")
|
||||||
deadline: str = Field(..., min_length=1, max_length=20, description="截止日期")
|
deadline: str = Field(..., min_length=1, max_length=20, description="截止日期")
|
||||||
|
|
||||||
|
|
||||||
|
class UnlockUserRequest(BaseModel):
|
||||||
|
"""解除用户登录锁定请求"""
|
||||||
|
username: str = Field(..., min_length=1, max_length=50, description="用户名")
|
||||||
@@ -37,7 +37,7 @@ class AuthService:
|
|||||||
attempts = await RedisClient.get(f"login_attempts:{username}")
|
attempts = await RedisClient.get(f"login_attempts:{username}")
|
||||||
if attempts and int(attempts) >= 5:
|
if attempts and int(attempts) >= 5:
|
||||||
await LogService.write_login_log(username, 0, ip, user_agent, "登录失败次数过多")
|
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)
|
user = await UserModel.get_by_username(username)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class RedisClient:
|
|||||||
key = f"login_attempts:{username}"
|
key = f"login_attempts:{username}"
|
||||||
attempts = await RedisClient.get(key)
|
attempts = await RedisClient.get(key)
|
||||||
attempts = int(attempts) + 1 if attempts else 1
|
attempts = int(attempts) + 1 if attempts else 1
|
||||||
await RedisClient.set(key, attempts, 900) # 15分钟锁定
|
await RedisClient.set(key, attempts, 300) # 5分钟锁定
|
||||||
return attempts
|
return attempts
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ async function loadAdmins() {
|
|||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-primary" onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</button>
|
<button class="btn btn-sm btn-primary" onclick="showEditAdminModal(${admin.user_id}, '${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}', '${escapeHtml(admin.role_type)}')">编辑</button>
|
||||||
<button class="btn btn-sm btn-warning" onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</button>
|
<button class="btn btn-sm btn-warning" onclick="resetAdminPassword(${admin.user_id}, '${escapeHtml(admin.real_name)}')">重置密码</button>
|
||||||
|
<button class="btn btn-sm btn-info" onclick="unlockUser('${escapeHtml(admin.username)}', '${escapeHtml(admin.real_name)}')">解锁</button>
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</button>
|
<button class="btn btn-sm btn-danger" onclick="deleteAdmin(${admin.user_id}, '${escapeHtml(admin.real_name)}')">删除</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@@ -253,7 +254,6 @@ async function deleteAdmin(userId, realName) {
|
|||||||
showToast(res?.message || '删除失败', 'error');
|
showToast(res?.message || '删除失败', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetAdminPassword(userId, realName) {
|
function resetAdminPassword(userId, realName) {
|
||||||
currentResetUserId = userId;
|
currentResetUserId = userId;
|
||||||
document.getElementById('resetPasswordUserId').value = userId;
|
document.getElementById('resetPasswordUserId').value = userId;
|
||||||
@@ -262,6 +262,23 @@ function resetAdminPassword(userId, realName) {
|
|||||||
document.getElementById('resetPasswordModal').style.display = 'flex';
|
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() {
|
async function submitResetPassword() {
|
||||||
if (!currentResetUserId) return;
|
if (!currentResetUserId) return;
|
||||||
|
|
||||||
|
|||||||
@@ -83,8 +83,11 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>分数变动</label>
|
<label>分数变动</label>
|
||||||
<input type="number" id="pointsChange" required min="-3" max="3" step="1" placeholder="正数加分,负数扣分">
|
<input type="number" id="pointsChange" required min="-5" max="5" step="1" placeholder="正数加分,负数扣分">
|
||||||
<small>每次加减分不超过<span class="hw-max"></span>分</small>
|
<small><?php
|
||||||
|
if ($role === '学习委员') echo '学习委员单次±5分以内';
|
||||||
|
else echo '班主任无限制';
|
||||||
|
?></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>原因</label>
|
<label>原因</label>
|
||||||
@@ -103,7 +106,7 @@ var selectedStudentIds = [];
|
|||||||
const hwRole = '<?php echo $role; ?>';
|
const hwRole = '<?php echo $role; ?>';
|
||||||
|
|
||||||
// 初始化扣分配置
|
// 初始化扣分配置
|
||||||
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 hwNotSubmit = window.DEDUCTION_HOMEWORK_NOT_SUBMIT || 2;
|
||||||
const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1;
|
const hwLate = window.DEDUCTION_HOMEWORK_LATE || 1;
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ async function loadStudents(page = 1) {
|
|||||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||||
${userRole === '班主任' ? `<button class="btn btn-sm btn-secondary" onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}')">编辑</button>
|
${userRole === '班主任' ? `<button class="btn btn-sm btn-secondary" onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}')">编辑</button>
|
||||||
<button class="btn btn-sm btn-warning" onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</button>
|
<button class="btn btn-sm btn-warning" onclick="showResetStudentPasswordModal(${student.student_id}, '${escapeHtml(student.name)}')">重置密码</button>
|
||||||
|
<button class="btn btn-sm btn-info" onclick="unlockStudent('${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}')">解锁</button>
|
||||||
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</button>` : ''}
|
<button class="btn btn-sm btn-danger" onclick="deleteStudent(${student.student_id}, '${escapeHtml(student.name)}')">删除</button>` : ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@@ -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();
|
loadStudents();
|
||||||
|
|
||||||
|
|||||||
@@ -350,6 +350,33 @@ tr:hover {
|
|||||||
background: #2f855a;
|
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 {
|
.btn-sm {
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ define('ICP_NUMBER', $config['ICP_NUMBER'] ?? '');
|
|||||||
// 扣分规则配置(有默认值,不强制要求在.env中配置)
|
// 扣分规则配置(有默认值,不强制要求在.env中配置)
|
||||||
define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2));
|
define('DEDUCTION_HOMEWORK_NOT_SUBMIT', (int)($config['DEDUCTION_HOMEWORK_NOT_SUBMIT'] ?? 2));
|
||||||
define('DEDUCTION_HOMEWORK_LATE', (int)($config['DEDUCTION_HOMEWORK_LATE'] ?? 1));
|
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_ABSENT', (int)($config['DEDUCTION_ATTENDANCE_ABSENT'] ?? 3));
|
||||||
define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 1));
|
define('DEDUCTION_ATTENDANCE_LATE', (int)($config['DEDUCTION_ATTENDANCE_LATE'] ?? 1));
|
||||||
define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 0));
|
define('DEDUCTION_ATTENDANCE_LEAVE', (int)($config['DEDUCTION_ATTENDANCE_LEAVE'] ?? 0));
|
||||||
|
|||||||
Reference in New Issue
Block a user