v0.6测试
This commit is contained in:
64
backend/models/log.py
Normal file
64
backend/models/log.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from utils.database import execute_insert
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LoginLogModel:
|
||||
"""登录日志数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def create(username: str, login_result: int, ip_address: str, user_agent: str = None, fail_reason: str = None) -> int:
|
||||
"""
|
||||
写入登录日志
|
||||
:param username: 用户名
|
||||
:param login_result: 登录结果 (1=成功, 0=失败)
|
||||
:param ip_address: IP地址
|
||||
:param user_agent: 浏览器UA
|
||||
:param fail_reason: 失败原因
|
||||
:return: log_id
|
||||
"""
|
||||
sql = """
|
||||
INSERT INTO login_logs (username, login_result, fail_reason, ip_address, user_agent)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (username, login_result, fail_reason, ip_address, user_agent))
|
||||
|
||||
|
||||
class OperationLogModel:
|
||||
"""操作日志数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def create(operator_id: int, operator_name: str, operator_role: str,
|
||||
operation_type: str, target_type: str = None, target_id: int = None,
|
||||
details: str = None, ip_address: str = None) -> int:
|
||||
"""
|
||||
写入操作日志
|
||||
:param operator_id: 操作者用户ID
|
||||
:param operator_name: 操作者用户名
|
||||
:param operator_role: 操作者角色
|
||||
:param operation_type: 操作类型
|
||||
:param target_type: 目标类型
|
||||
:param target_id: 目标ID
|
||||
:param details: 详细信息
|
||||
:param ip_address: IP地址
|
||||
:return: log_id
|
||||
"""
|
||||
sql = """
|
||||
INSERT INTO operation_logs (operator_id, operator_name, operator_role,
|
||||
operation_type, target_type, target_id, details, ip_address)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (operator_id, operator_name, operator_role,
|
||||
operation_type, target_type, target_id, details, ip_address))
|
||||
@@ -22,8 +22,10 @@ from services.admin_service import AdminService
|
||||
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 schemas.admin import (
|
||||
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
||||
AddStudentRequest,
|
||||
UpdateHomeworkStatusRequest, AddAttendanceRequest
|
||||
)
|
||||
from utils.response import success_response, error_response
|
||||
@@ -83,16 +85,18 @@ async def import_students(request: Request, file: UploadFile = File(...)):
|
||||
operator_id=user["user_id"],
|
||||
initial_points=60
|
||||
)
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="import_students",
|
||||
target_type="student",
|
||||
details=f"批量导入: 成功{result['success_count']}人, 失败{result['failed_count']}人",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result, message=f"导入完成: 成功{result['success_count']}人,失败{result['failed_count']}人")
|
||||
|
||||
|
||||
@router.post("/students")
|
||||
async def add_student(
|
||||
request: Request,
|
||||
student_no: str,
|
||||
name: str,
|
||||
parent_phone: Optional[str] = None
|
||||
):
|
||||
async def add_student(request: Request, req: AddStudentRequest):
|
||||
"""新增学生"""
|
||||
user = await get_current_user(request)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
@@ -100,13 +104,20 @@ async def add_student(
|
||||
return error_response(message="仅班主任可新增学生", code=403)
|
||||
|
||||
result = await AdminService.add_student(
|
||||
student_no=student_no,
|
||||
name=name,
|
||||
parent_phone=parent_phone,
|
||||
student_no=req.student_no,
|
||||
name=req.name,
|
||||
parent_phone=req.parent_phone,
|
||||
operator_id=user["user_id"],
|
||||
initial_points=60
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="add_student",
|
||||
target_type="student", target_id=result.get("student_id"),
|
||||
details=f"新增学生: {req.name}({req.student_no})",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result, message="学生添加成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -126,6 +137,14 @@ 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["username"],
|
||||
operator_role=role, operation_type="add_points",
|
||||
target_type="conduct",
|
||||
details=f"批量加减分: {req.points_change}分, 原因: {req.reason}, 对象: {req.student_ids}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result, message="操作成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -140,6 +159,13 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
|
||||
revoker_id=user["user_id"]
|
||||
)
|
||||
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_role=role, operation_type="revoke_record",
|
||||
target_type="conduct", target_id=req.record_id,
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message="撤销成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -215,6 +241,13 @@ async def create_assignment(
|
||||
created_by=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="create_assignment",
|
||||
target_type="homework",
|
||||
details=f"发布作业: {title}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result, message="作业发布成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -235,6 +268,13 @@ async def update_submission_status(request: Request, req: UpdateHomeworkStatusRe
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role=role, operation_type="update_submission",
|
||||
target_type="homework", target_id=req.submission_id,
|
||||
details=f"状态: {req.status}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message="状态更新成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -258,6 +298,13 @@ async def add_attendance(request: Request, req: AddAttendanceRequest):
|
||||
recorder_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role=role, operation_type="add_attendance",
|
||||
target_type="attendance",
|
||||
details=f"学生ID: {req.student_id}, 日期: {req.date}, 状态: {req.status}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message="考勤记录添加成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -298,6 +345,13 @@ async def add_admin(request: Request, req: AddAdminRequest):
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="add_admin",
|
||||
target_type="admin",
|
||||
details=f"新增管理员: {req.real_name}({req.username}), 角色: {req.role_type}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result, message="管理员添加成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
@@ -29,11 +29,13 @@ async def login(request: LoginRequest, http_request: Request):
|
||||
"""
|
||||
# 获取客户端IP
|
||||
client_ip = http_request.client.host
|
||||
user_agent = http_request.headers.get("user-agent", "")
|
||||
|
||||
result = await AuthService.login(
|
||||
username=request.username,
|
||||
password=request.password,
|
||||
ip=client_ip
|
||||
ip=client_ip,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
|
||||
@@ -71,6 +71,13 @@ class UpdateHomeworkStatusRequest(BaseModel):
|
||||
apply_deduction: bool = False
|
||||
|
||||
|
||||
class AddStudentRequest(BaseModel):
|
||||
"""新增学生请求"""
|
||||
student_no: str = Field(..., min_length=1, max_length=20, description="学号")
|
||||
name: str = Field(..., min_length=1, max_length=50, description="姓名")
|
||||
parent_phone: Optional[str] = Field(None, max_length=11, description="家长手机号")
|
||||
|
||||
|
||||
class AddAttendanceRequest(BaseModel):
|
||||
"""添加考勤请求"""
|
||||
student_id: int
|
||||
|
||||
@@ -15,6 +15,7 @@ from datetime import datetime
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.admin_role import AdminRoleModel
|
||||
from services.log_service import LogService
|
||||
from utils.security import security
|
||||
from utils.jwt_handler import jwt_handler
|
||||
from utils.redis_client import RedisClient
|
||||
@@ -28,13 +29,14 @@ class AuthService:
|
||||
"""认证服务"""
|
||||
|
||||
@staticmethod
|
||||
async def login(username: str, password: str, ip: str) -> Dict[str, Any]:
|
||||
async def login(username: str, password: str, ip: str, user_agent: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
用户登录
|
||||
"""
|
||||
# 检查登录失败次数
|
||||
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分钟后重试"}
|
||||
|
||||
# 获取用户信息
|
||||
@@ -42,15 +44,18 @@ class AuthService:
|
||||
|
||||
if not user:
|
||||
await RedisClient.set_login_attempts(username)
|
||||
await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误")
|
||||
return {"success": False, "message": "用户名或密码错误"}
|
||||
|
||||
# 验证密码
|
||||
if not security.verify_password(password, user["password_hash"]):
|
||||
await RedisClient.set_login_attempts(username)
|
||||
await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误")
|
||||
return {"success": False, "message": "用户名或密码错误"}
|
||||
|
||||
# 检查账号状态
|
||||
if user["status"] != 1:
|
||||
await LogService.write_login_log(username, 0, ip, user_agent, "账号已被禁用")
|
||||
return {"success": False, "message": "账号已被禁用"}
|
||||
|
||||
# 清除登录失败记录
|
||||
@@ -80,6 +85,8 @@ class AuthService:
|
||||
# 确定跳转路径
|
||||
redirect = AuthService._get_redirect_path(user["user_type"], role)
|
||||
|
||||
await LogService.write_login_log(username, 1, ip, user_agent)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"token": token,
|
||||
|
||||
57
backend/services/log_service.py
Normal file
57
backend/services/log_service.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from models.log import LoginLogModel, OperationLogModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LogService:
|
||||
"""日志服务"""
|
||||
|
||||
@staticmethod
|
||||
async def write_login_log(username: str, login_result: int, ip: str, user_agent: str = None, fail_reason: str = None):
|
||||
"""
|
||||
写入登录日志(异步,不阻塞主流程)
|
||||
"""
|
||||
try:
|
||||
await LoginLogModel.create(
|
||||
username=username,
|
||||
login_result=login_result,
|
||||
ip_address=ip,
|
||||
user_agent=user_agent,
|
||||
fail_reason=fail_reason
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"写入登录日志失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def write_operation_log(operator_id: int, operator_name: str, operator_role: str,
|
||||
operation_type: str, target_type: str = None,
|
||||
target_id: int = None, details: str = None, ip: str = None):
|
||||
"""
|
||||
写入操作日志(异步,不阻塞主流程)
|
||||
"""
|
||||
try:
|
||||
await OperationLogModel.create(
|
||||
operator_id=operator_id,
|
||||
operator_name=operator_name,
|
||||
operator_role=operator_role,
|
||||
operation_type=operation_type,
|
||||
target_type=target_type,
|
||||
target_id=target_id,
|
||||
details=details,
|
||||
ip_address=ip
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"写入操作日志失败: {e}")
|
||||
Reference in New Issue
Block a user