v0.6测试

This commit is contained in:
2026-04-14 20:42:18 +08:00
parent a60ba8352f
commit d17a63d4cd
13 changed files with 680 additions and 473 deletions

View File

@@ -304,3 +304,38 @@
- dashboard.php 第61行: 快捷操作条件添加劳动委员 - dashboard.php 第61行: 快捷操作条件添加劳动委员
- attendance.php 第27行: `/student/conduct.php``/student/conduct_history.php` - attendance.php 第27行: `/student/conduct.php``/student/conduct_history.php`
- password.php 第26行: `/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备案号
【修改方式】
- 移除自定义的 `<!DOCTYPE html>`, `<head>`, `<body>` 和自定义 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 bodyFastAPI从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_logsimport_students、add_student、add_points、revoke_record、create_assignment、update_submission、add_attendance、add_admin

64
backend/models/log.py Normal file
View 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))

View File

@@ -22,8 +22,10 @@ from services.admin_service import AdminService
from services.conduct_service import ConductService 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 schemas.admin import ( from schemas.admin import (
AddPointsRequest, RevokeRequest, AddAdminRequest, AddPointsRequest, RevokeRequest, AddAdminRequest,
AddStudentRequest,
UpdateHomeworkStatusRequest, AddAttendanceRequest UpdateHomeworkStatusRequest, AddAttendanceRequest
) )
from utils.response import success_response, error_response 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"], operator_id=user["user_id"],
initial_points=60 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']}") return success_response(data=result, message=f"导入完成: 成功{result['success_count']}人,失败{result['failed_count']}")
@router.post("/students") @router.post("/students")
async def add_student( async def add_student(request: Request, req: AddStudentRequest):
request: Request,
student_no: str,
name: str,
parent_phone: Optional[str] = None
):
"""新增学生""" """新增学生"""
user = await get_current_user(request) user = await get_current_user(request)
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
@@ -100,13 +104,20 @@ async def add_student(
return error_response(message="仅班主任可新增学生", code=403) return error_response(message="仅班主任可新增学生", code=403)
result = await AdminService.add_student( result = await AdminService.add_student(
student_no=student_no, student_no=req.student_no,
name=name, name=req.name,
parent_phone=parent_phone, parent_phone=req.parent_phone,
operator_id=user["user_id"], operator_id=user["user_id"],
initial_points=60 initial_points=60
) )
if result["success"]: 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="学生添加成功") return success_response(data=result, message="学生添加成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])
@@ -126,6 +137,14 @@ 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"])
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="操作成功") return success_response(data=result, message="操作成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])
@@ -140,6 +159,13 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
revoker_id=user["user_id"] revoker_id=user["user_id"]
) )
if result["success"]: 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="撤销成功") return success_response(message="撤销成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])
@@ -215,6 +241,13 @@ async def create_assignment(
created_by=user["user_id"] created_by=user["user_id"]
) )
if result["success"]: 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="作业发布成功") return success_response(data=result, message="作业发布成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])
@@ -235,6 +268,13 @@ async def update_submission_status(request: Request, req: UpdateHomeworkStatusRe
operator_id=user["user_id"] operator_id=user["user_id"]
) )
if result["success"]: 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="状态更新成功") return success_response(message="状态更新成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])
@@ -258,6 +298,13 @@ async def add_attendance(request: Request, req: AddAttendanceRequest):
recorder_id=user["user_id"] recorder_id=user["user_id"]
) )
if result["success"]: 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="考勤记录添加成功") return success_response(message="考勤记录添加成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])
@@ -298,6 +345,13 @@ async def add_admin(request: Request, req: AddAdminRequest):
operator_id=user["user_id"] operator_id=user["user_id"]
) )
if result["success"]: 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="管理员添加成功") return success_response(data=result, message="管理员添加成功")
else: else:
return error_response(message=result["message"]) return error_response(message=result["message"])

View File

@@ -29,11 +29,13 @@ async def login(request: LoginRequest, http_request: Request):
""" """
# 获取客户端IP # 获取客户端IP
client_ip = http_request.client.host client_ip = http_request.client.host
user_agent = http_request.headers.get("user-agent", "")
result = await AuthService.login( result = await AuthService.login(
username=request.username, username=request.username,
password=request.password, password=request.password,
ip=client_ip ip=client_ip,
user_agent=user_agent
) )
if result["success"]: if result["success"]:

View File

@@ -71,6 +71,13 @@ class UpdateHomeworkStatusRequest(BaseModel):
apply_deduction: bool = False 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): class AddAttendanceRequest(BaseModel):
"""添加考勤请求""" """添加考勤请求"""
student_id: int student_id: int

View File

@@ -15,6 +15,7 @@ from datetime import datetime
from models.user import UserModel from models.user import UserModel
from models.student import StudentModel from models.student import StudentModel
from models.admin_role import AdminRoleModel from models.admin_role import AdminRoleModel
from services.log_service import LogService
from utils.security import security from utils.security import security
from utils.jwt_handler import jwt_handler from utils.jwt_handler import jwt_handler
from utils.redis_client import RedisClient from utils.redis_client import RedisClient
@@ -28,13 +29,14 @@ class AuthService:
"""认证服务""" """认证服务"""
@staticmethod @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}") 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, "登录失败次数过多")
return {"success": False, "message": "登录失败次数过多请15分钟后重试"} return {"success": False, "message": "登录失败次数过多请15分钟后重试"}
# 获取用户信息 # 获取用户信息
@@ -42,15 +44,18 @@ class AuthService:
if not user: if not user:
await RedisClient.set_login_attempts(username) await RedisClient.set_login_attempts(username)
await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误")
return {"success": False, "message": "用户名或密码错误"} return {"success": False, "message": "用户名或密码错误"}
# 验证密码 # 验证密码
if not security.verify_password(password, user["password_hash"]): if not security.verify_password(password, user["password_hash"]):
await RedisClient.set_login_attempts(username) await RedisClient.set_login_attempts(username)
await LogService.write_login_log(username, 0, ip, user_agent, "用户名或密码错误")
return {"success": False, "message": "用户名或密码错误"} return {"success": False, "message": "用户名或密码错误"}
# 检查账号状态 # 检查账号状态
if user["status"] != 1: if user["status"] != 1:
await LogService.write_login_log(username, 0, ip, user_agent, "账号已被禁用")
return {"success": False, "message": "账号已被禁用"} return {"success": False, "message": "账号已被禁用"}
# 清除登录失败记录 # 清除登录失败记录
@@ -80,6 +85,8 @@ class AuthService:
# 确定跳转路径 # 确定跳转路径
redirect = AuthService._get_redirect_path(user["user_type"], role) redirect = AuthService._get_redirect_path(user["user_type"], role)
await LogService.write_login_log(username, 1, ip, user_agent)
return { return {
"success": True, "success": True,
"token": token, "token": token,

View 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}")

View File

@@ -97,7 +97,7 @@ function selectStatus(btn) {
// 加载学生列表 // 加载学生列表
async function loadStudents() { async function loadStudents() {
const res = await apiGet('/api/admin/students'); const res = await apiGet('/api/admin/students', {page_size: 1000});
if (res && res.success) { if (res && res.success) {
studentsData = res.data.students; studentsData = res.data.students;
renderStudentGrid(); renderStudentGrid();

View File

@@ -59,7 +59,7 @@ include __DIR__ . '/../includes/header.php';
var selectedStudentIds = []; var selectedStudentIds = [];
async function loadStudents() { async function loadStudents() {
const res = await apiGet('/api/admin/students'); const res = await apiGet('/api/admin/students', {page_size: 1000});
if (res && res.success) { if (res && res.success) {
let html = ''; let html = '';
res.data.students.forEach(student => { res.data.students.forEach(student => {

View File

@@ -108,7 +108,7 @@ document.getElementById('pointsChange').setAttribute('min', -hwMaxPoints);
document.getElementById('pointsChange').setAttribute('max', hwMaxPoints); document.getElementById('pointsChange').setAttribute('max', hwMaxPoints);
async function loadStudents() { async function loadStudents() {
const res = await apiGet('/api/admin/students'); const res = await apiGet('/api/admin/students', {page_size: 1000});
if (res && res.success) { if (res && res.success) {
let html = ''; let html = '';
res.data.students.forEach(student => { res.data.students.forEach(student => {

View File

@@ -51,7 +51,7 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) {
</form> </form>
<div class="login-footer"> <div class="login-footer">
<p>© Sea Network Technology Studio</p> <p>&copy; <?php echo date('Y'); ?> Sea Network Technology Studio<?php if (defined('ICP_ENABLED') && ICP_ENABLED && defined('ICP_NUMBER') && ICP_NUMBER): ?> | <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer"><?php echo htmlspecialchars(ICP_NUMBER); ?></a><?php endif; ?></p>
</div> </div>
</div> </div>

View File

@@ -18,15 +18,11 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
exit(); exit();
} }
$page_title = '学生端';
$student_id = $_SESSION['student_id']; $student_id = $_SESSION['student_id'];
include __DIR__ . '/../includes/header.php';
?> ?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - 学生端</title>
<link rel="stylesheet" href="/assets/css/style.css">
<style> <style>
.conduct-score { .conduct-score {
text-align: center; text-align: center;
@@ -75,15 +71,6 @@ $student_id = $_SESSION['student_id'];
text-decoration: none; text-decoration: none;
} }
</style> </style>
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?> - 学生端</h1>
<div class="header-info">
<span class="user-name" id="userName"></span>
<button class="btn-logout" id="logoutBtn">退出登录</button>
</div>
</div>
<div class="nav"> <div class="nav">
<button class="nav-item active" data-page="dashboard">首页</button> <button class="nav-item active" data-page="dashboard">首页</button>
@@ -115,7 +102,7 @@ $student_id = $_SESSION['student_id'];
<div class="card-title">最新操行分记录</div> <div class="card-title">最新操行分记录</div>
<div id="recentRecords"></div> <div id="recentRecords"></div>
<div class="view-more"> <div class="view-more">
<a href="#" onclick="showPage('conduct'); return false;">查看更多 &gt;</a> <a href="#" onclick="showPage('conduct'); return false;">查看更多 ></a>
</div> </div>
</div> </div>
</div> </div>
@@ -240,12 +227,6 @@ $student_id = $_SESSION['student_id'];
</div> </div>
</div> </div>
<script>
window.API_BASE_URL = '<?php echo API_BASE_URL; ?>';
window.JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
window.USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
</script>
<script src="/assets/js/common.js"></script>
<script> <script>
const STUDENT_ID = <?php echo $student_id; ?>; const STUDENT_ID = <?php echo $student_id; ?>;
@@ -513,5 +494,5 @@ $student_id = $_SESSION['student_id'];
loadDashboard(); loadDashboard();
checkForceChangePassword(); checkForceChangePassword();
</script> </script>
</body>
</html> <?php include __DIR__ . '/../includes/footer.php'; ?>