From d17a63d4cdc64fc06a5b3dd0b804ff5514748a54 Mon Sep 17 00:00:00 2001 From: canglan Date: Tue, 14 Apr 2026 20:42:18 +0800 Subject: [PATCH] =?UTF-8?q?v0.6=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../changes/fix-admin-multi-issues/task.md | 35 + backend/models/log.py | 64 ++ backend/routes/admin.py | 72 +- backend/routes/auth.py | 4 +- backend/schemas/admin.py | 7 + backend/services/auth_service.py | 9 +- backend/services/log_service.py | 57 ++ docs/admin.md | 2 +- frontend/admin/attendance.php | 2 +- frontend/admin/conduct.php | 2 +- frontend/admin/homework.php | 2 +- frontend/index.php | 2 +- frontend/student/dashboard.php | 895 +++++++++--------- 13 files changed, 680 insertions(+), 473 deletions(-) create mode 100644 backend/models/log.py create mode 100644 backend/services/log_service.py diff --git a/.cospec/plan/changes/fix-admin-multi-issues/task.md b/.cospec/plan/changes/fix-admin-multi-issues/task.md index f00e9a8..b7bcf93 100644 --- a/.cospec/plan/changes/fix-admin-multi-issues/task.md +++ b/.cospec/plan/changes/fix-admin-multi-issues/task.md @@ -304,3 +304,38 @@ - dashboard.php 第61行: 快捷操作条件添加劳动委员 - attendance.php 第27行: `/student/conduct.php` → `/student/conduct_history.php` - password.php 第26行: `/student/conduct.php` → `/student/conduct_history.php` + +- [x] 12.7 同步学生端dashboard.php页眉(使用header.php/footer.php) + 【目标对象】`frontend/student/dashboard.php` + 【修改目的】学生端dashboard.php使用自定义HTML结构,与管理端不统一,缺少共享页眉和ICP备案号 + 【修改方式】 + - 移除自定义的 ``, ``, `` 和自定义 header div + - 添加 `$page_title = '学生端'; include __DIR__ . '/../includes/header.php';` + - 移除重复的 `window.API_BASE_URL` 等变量设置(header.php已处理) + - 添加 `include __DIR__ . '/../includes/footer.php';` 以显示ICP备案号 + +- [x] 12.8 登录页面补充备案号悬挂 + 【目标对象】`frontend/index.php` + 【修改目的】登录页面的页脚缺少ICP备案号 + 【修改方式】 + - 在登录页 footer 的版权信息后,添加ICP备案号链接(使用 `ICP_ENABLED` 和 `ICP_NUMBER` 常量) + - 链接指向 https://beian.miit.gov.cn/,target="_blank" + +- [x] 12.9 修复用户报告的4个运行时Bug + 【目标对象】`backend/schemas/admin.py`、`frontend/admin/conduct.php`、`frontend/admin/homework.php`、`frontend/admin/attendance.php` + 【修改目的】用户报告:1) 历史记录在加减分后无内容; 2) 管理员无法添加学生; 3) 加减分页面仅显示21人; 4) 管理端修改密码失败 + 【修改方式】 + - 12.9a 历史记录问题:已在12.3中通过添加 `StudentModel.update_total_points()` 调用修复 + - 12.9b 添加学生422错误:`add_student` 路由使用裸函数参数而非Pydantic body,FastAPI从query string读取导致422。创建 `AddStudentRequest` schema,路由改用Pydantic body + - 12.9c 仅显示21人:`loadStudents()` 未传 `page_size`,后端默认20。前端3个文件添加 `{page_size: 1000}` + - 12.9d 修改密码:已确认代码正确(`sha1_md5_password` + `need_change_password=0`) + +- [x] 12.10 修复登录及管理员日志未写入数据库 + 【目标对象】`backend/models/log.py`(新建)、`backend/services/log_service.py`(新建)、`backend/services/auth_service.py`、`backend/routes/auth.py`、`backend/routes/admin.py` + 【修改目的】数据库中 `login_logs` 和 `operation_logs` 表已存在,但没有任何后端代码写入数据。README中规划了 `models/operation_log.py` 和 `services/log_service.py`,但文件不存在 + 【修改方式】 + - 新建 `backend/models/log.py`:包含 `LoginLogModel` 和 `OperationLogModel`,分别向 login_logs 和 operation_logs 表写入记录 + - 新建 `backend/services/log_service.py`:`LogService` 提供 `write_login_log` 和 `write_operation_log` 方法,用 try-except 包裹确保日志写入失败不影响主业务 + - 修改 `auth_service.py`:login 方法增加 `user_agent` 参数,在5个退出点(失败次数过多、用户不存在、密码错误、账号禁用、登录成功)均写入 login_logs + - 修改 `auth.py`:从 HTTP 请求头获取 user-agent 并传递给 AuthService.login() + - 修改 `admin.py`:8个管理操作成功后写入 operation_logs(import_students、add_student、add_points、revoke_record、create_assignment、update_submission、add_attendance、add_admin) diff --git a/backend/models/log.py b/backend/models/log.py new file mode 100644 index 0000000..064428d --- /dev/null +++ b/backend/models/log.py @@ -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)) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index c9c2e20..9e5a291 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -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"]) diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 5aadd69..fd39d65 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -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"]: diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index 280071b..5b8ebee 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -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 diff --git a/backend/services/auth_service.py b/backend/services/auth_service.py index bcef2f7..5a08578 100644 --- a/backend/services/auth_service.py +++ b/backend/services/auth_service.py @@ -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, diff --git a/backend/services/log_service.py b/backend/services/log_service.py new file mode 100644 index 0000000..ae59876 --- /dev/null +++ b/backend/services/log_service.py @@ -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}") diff --git a/docs/admin.md b/docs/admin.md index 64cdc19..12095ed 100644 --- a/docs/admin.md +++ b/docs/admin.md @@ -1 +1 @@ -# 管理端使用文档 +# 管理端使用文档 \ No newline at end of file diff --git a/frontend/admin/attendance.php b/frontend/admin/attendance.php index 0b223fd..2757740 100644 --- a/frontend/admin/attendance.php +++ b/frontend/admin/attendance.php @@ -97,7 +97,7 @@ function selectStatus(btn) { // 加载学生列表 async function loadStudents() { - const res = await apiGet('/api/admin/students'); + const res = await apiGet('/api/admin/students', {page_size: 1000}); if (res && res.success) { studentsData = res.data.students; renderStudentGrid(); diff --git a/frontend/admin/conduct.php b/frontend/admin/conduct.php index e498f7f..126e5d4 100644 --- a/frontend/admin/conduct.php +++ b/frontend/admin/conduct.php @@ -59,7 +59,7 @@ include __DIR__ . '/../includes/header.php'; var selectedStudentIds = []; async function loadStudents() { - const res = await apiGet('/api/admin/students'); + const res = await apiGet('/api/admin/students', {page_size: 1000}); if (res && res.success) { let html = ''; res.data.students.forEach(student => { diff --git a/frontend/admin/homework.php b/frontend/admin/homework.php index 205a998..8f3ad43 100644 --- a/frontend/admin/homework.php +++ b/frontend/admin/homework.php @@ -108,7 +108,7 @@ document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints); document.getElementById('pointsChange').setAttribute('max', hwMaxPoints); async function loadStudents() { - const res = await apiGet('/api/admin/students'); + const res = await apiGet('/api/admin/students', {page_size: 1000}); if (res && res.success) { let html = ''; res.data.students.forEach(student => { diff --git a/frontend/index.php b/frontend/index.php index 49b0e50..496920c 100644 --- a/frontend/index.php +++ b/frontend/index.php @@ -51,7 +51,7 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) { diff --git a/frontend/student/dashboard.php b/frontend/student/dashboard.php index be14ade..d9b5f59 100644 --- a/frontend/student/dashboard.php +++ b/frontend/student/dashboard.php @@ -18,500 +18,481 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') { exit(); } +$page_title = '学生端'; $student_id = $_SESSION['student_id']; +include __DIR__ . '/../includes/header.php'; ?> - - - - - - <?php echo SITE_NAME; ?> - 学生端 - - - - -
-

- 学生端

-
- - -
-
- + -
- -
-
-
-
当前操行分
-
--
-
-
-
作业完成率
-
--%
-
-
-
本月出勤率
-
--%
-
+ + +
+ +
+
+
+
当前操行分
+
--
- -
-
最新操行分记录
-
- +
+
作业完成率
+
--%
+
+
+
本月出勤率
+
--%
- - - - - - - - - - -