修复考勤管理bug并加强了信息保护
This commit is contained in:
@@ -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"))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -126,6 +126,7 @@ class PermissionChecker:
|
||||
检查是否可以撤销扣分记录
|
||||
班主任:可以撤销任何记录
|
||||
班长:可以撤销任何记录
|
||||
考勤委员:可以撤销自己的记录
|
||||
其他:只能撤销自己的记录
|
||||
"""
|
||||
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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']})",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": "考勤记录添加成功"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user