修复考勤管理bug并加强了信息保护

This commit is contained in:
2026-04-27 01:15:03 +08:00
parent bf0314f098
commit 439c074534
16 changed files with 176 additions and 49 deletions

View File

@@ -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"))

View File

@@ -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

View File

@@ -126,6 +126,7 @@ class PermissionChecker:
检查是否可以撤销扣分记录
班主任:可以撤销任何记录
班长:可以撤销任何记录
考勤委员:可以撤销自己的记录
其他:只能撤销自己的记录
"""
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"

View File

@@ -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

View File

@@ -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

View File

@@ -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']})",

View File

@@ -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,

View File

@@ -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": "考勤记录添加成功"}

View File

@@ -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

View File

@@ -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

View File

@@ -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