From 112dc94f7ce06769a204d3cf2233b1b1f5d56615 Mon Sep 17 00:00:00 2001 From: canglan Date: Thu, 16 Apr 2026 10:36:34 +0800 Subject: [PATCH] =?UTF-8?q?v0.8.5=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/models/user.py | 9 ++- backend/routes/admin.py | 65 +++++++++++++++- backend/schemas/admin.py | 14 +++- backend/services/attendance_service.py | 6 -- backend/services/conduct_service.py | 12 --- backend/services/homework_service.py | 6 -- frontend/admin/admins.php | 100 ++++++++++++++++++++++++- frontend/admin/attendance.php | 23 +++--- frontend/admin/conduct.php | 96 +++++++++++++----------- frontend/admin/dashboard.php | 2 +- frontend/admin/history.php | 52 +++++++++++++ frontend/student/dashboard.php | 2 +- 12 files changed, 297 insertions(+), 90 deletions(-) diff --git a/backend/models/user.py b/backend/models/user.py index 6aa03a8..2f7fe28 100644 --- a/backend/models/user.py +++ b/backend/models/user.py @@ -98,4 +98,11 @@ class UserModel: """检查用户名是否存在""" sql = "SELECT 1 FROM users WHERE username = %s" result = await execute_one(sql, (username,)) - return result is not None \ No newline at end of file + return result is not None + + @staticmethod + async def update_status(user_id: int, status: int) -> bool: + """更新用户状态(0=禁用,1=启用)""" + sql = "UPDATE users SET status = %s WHERE user_id = %s" + result = await execute_update(sql, (status, user_id)) + return result > 0 \ No newline at end of file diff --git a/backend/routes/admin.py b/backend/routes/admin.py index d805cb8..2320634 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -26,7 +26,8 @@ from services.log_service import LogService from schemas.admin import ( AddPointsRequest, RevokeRequest, AddAdminRequest, AddStudentRequest, - UpdateHomeworkStatusRequest, AddAttendanceRequest + UpdateHomeworkStatusRequest, AddAttendanceRequest, + UpdateAdminRequest, DeleteAdminRequest ) from utils.response import success_response, error_response from utils.logger import get_logger @@ -374,4 +375,64 @@ async def get_admins(request: Request): return success_response(data=result) except Exception as e: logger.error(f"获取管理员列表失败: {e}", exc_info=True) - return error_response(message=f"获取管理员列表失败: {str(e)}") \ No newline at end of file + return error_response(message=f"获取管理员列表失败: {str(e)}") + + +@router.put("/update/{user_id}") +async def update_admin(request: Request, user_id: int, req: UpdateAdminRequest): + """更新管理员信息(班主任)""" + user = await get_current_user(request) + is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) + if not is_teacher: + return error_response(message="仅班主任可更新管理员", code=403) + if req.role_type not in ["班长", "学习委员", "考勤委员", "劳动委员", "志愿委员"]: + return error_response(message="无效的角色类型", code=400) + + from models.admin_role import AdminRoleModel + result = await AdminRoleModel.update_role( + user_id=user_id, + role_type=req.role_type + ) + if result: + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["username"], + operator_role="班主任", operation_type="update_admin", + target_type="admin", target_id=user_id, + details=f"更新管理员角色为: {req.role_type}", + ip=request.client.host + ) + return success_response(message="管理员更新成功") + else: + return error_response(message="更新失败或管理员不存在") + + +@router.delete("/delete/{user_id}") +async def delete_admin(request: Request, user_id: int): + """删除管理员(班主任)""" + user = await get_current_user(request) + is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) + if not is_teacher: + return error_response(message="仅班主任可删除管理员", code=403) + + # 防止删除自己 + if user_id == user["user_id"]: + return error_response(message="不能删除当前登录的管理员", code=400) + + from models.admin_role import AdminRoleModel + from models.user import UserModel + + # 先删除角色记录 + role_deleted = await AdminRoleModel.delete(user_id) + if role_deleted: + # 再删除用户账号(软删除,将状态设为禁用) + await UserModel.update_status(user_id, 0) + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["username"], + operator_role="班主任", operation_type="delete_admin", + target_type="admin", target_id=user_id, + details=f"删除管理员: ID={user_id}", + ip=request.client.host + ) + return success_response(message="管理员删除成功") + else: + return error_response(message="删除失败或管理员不存在") \ No newline at end of file diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index 99a2122..3505f6e 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -85,4 +85,16 @@ class AddAttendanceRequest(BaseModel): status: str reason: Optional[str] = None apply_deduction: bool = True - custom_deduction: Optional[int] = Field(default=None, gt=0, description="自定义扣分值") \ No newline at end of file + custom_deduction: Optional[int] = Field(default=None, gt=0, description="自定义扣分值") + + +class UpdateAdminRequest(BaseModel): + """更新管理员请求""" + user_id: int = Field(..., description="用户ID") + real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名") + role_type: str = Field(..., description="角色类型") + + +class DeleteAdminRequest(BaseModel): + """删除管理员请求""" + user_id: int = Field(..., description="用户ID") \ No newline at end of file diff --git a/backend/services/attendance_service.py b/backend/services/attendance_service.py index a983438..24ab0f0 100644 --- a/backend/services/attendance_service.py +++ b/backend/services/attendance_service.py @@ -78,12 +78,6 @@ class AttendanceService: # 创建扣分记录 student = await StudentModel.get_by_id(student_id) if student: - # 检查分数是否会超出范围(防止溢出) - current_points = student.get("total_points", 0) - new_points = current_points + points_change - if new_points < 0: - return {"success": False, "message": f"分数不能为负(当前{current_points},扣{abs(points_change)})"} - # 获取操作人姓名 user = await UserModel.get_by_user_id(recorder_id) recorder_name = user.get("real_name", "班主任") if user else "班主任" diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index 2201d24..b2cf3b9 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -80,18 +80,6 @@ class ConductService: fail_count += 1 continue - # 检查分数是否会超出范围(防止溢出) - current_points = student.get("total_points", 0) - new_points = current_points + points_change - if new_points < 0: - details.append({"student_id": student_id, "error": f"分数不能为负(当前{current_points},操作{points_change})"}) - fail_count += 1 - continue - if new_points > 100: - details.append({"student_id": student_id, "error": f"分数不能超过100(当前{current_points},操作{points_change})"}) - fail_count += 1 - continue - # 创建记录 record_id = await ConductModel.create_record( student_id=student_id, diff --git a/backend/services/homework_service.py b/backend/services/homework_service.py index 125893e..788b825 100644 --- a/backend/services/homework_service.py +++ b/backend/services/homework_service.py @@ -109,12 +109,6 @@ class HomeworkService: # 创建扣分记录 student = await StudentModel.get_by_id(submission["student_id"]) if student: - # 检查分数是否会超出范围(防止溢出) - current_points = student.get("total_points", 0) - new_points = current_points + points_change - if new_points < 0: - return {"success": False, "message": f"分数不能为负(当前{current_points},扣{abs(points_change)})"} - # 获取操作人姓名 from models.user import UserModel user = await UserModel.get_by_user_id(operator_id) diff --git a/frontend/admin/admins.php b/frontend/admin/admins.php index 80b154c..9a9b90d 100644 --- a/frontend/admin/admins.php +++ b/frontend/admin/admins.php @@ -37,7 +37,7 @@ include __DIR__ . '/../includes/header.php';
- +
用户名姓名角色
用户名姓名角色操作
@@ -85,7 +85,45 @@ include __DIR__ . '/../includes/header.php';
+ + + - \ No newline at end of file + diff --git a/frontend/admin/attendance.php b/frontend/admin/attendance.php index b98ab66..e542ee6 100644 --- a/frontend/admin/attendance.php +++ b/frontend/admin/attendance.php @@ -38,6 +38,14 @@ include __DIR__ . '/../includes/header.php'; +
+ + +
@@ -166,20 +174,7 @@ async function submitAttendance() { const customDeduction = document.getElementById('customDeduction').value; const customDeductionValue = customDeduction ? parseInt(customDeduction) : null; - // 检查是否有已存在记录的学生 - const hasRecordStudents = []; - selectedCells.forEach(cell => { - if (cell.classList.contains('has-record')) { - hasRecordStudents.push(cell.dataset.name); - } - }); - - if (hasRecordStudents.length > 0) { - const confirmed = confirm(`以下学生已有考勤记录:${hasRecordStudents.join('、')},是否继续提交?`); - if (!confirmed) return; - } - - // 批量提交 + // 批量提交(不再检查已有记录,允许同一学生同一天多次考勤) const promises = []; selectedCells.forEach(cell => { const studentId = parseInt(cell.dataset.id); diff --git a/frontend/admin/conduct.php b/frontend/admin/conduct.php index 8c9ed29..667a63a 100644 --- a/frontend/admin/conduct.php +++ b/frontend/admin/conduct.php @@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
- +
@@ -96,50 +96,62 @@ function showSinglePointsModal(studentId, studentName) { document.getElementById('batchPointsModal').style.display = 'flex'; } -// 导出操行分记录 -async function exportConductRecords() { - const startDate = prompt('请输入开始日期(格式:YYYY-MM-DD,留空则不限):', ''); - if (startDate === null) return; - const endDate = prompt('请输入结束日期(格式:YYYY-MM-DD,留空则不限):', ''); - if (endDate === null) return; - - showToast('正在导出...', 'info'); +// 导出德育分记录(格式:学号 姓名 分数 加分历史 减分记录) +async function exportMoralityRecords() { + showToast('正在导出德育分记录...', 'info'); try { - const params = { page: 1, page_size: 1000 }; - if (startDate) params.start_date = startDate; - if (endDate) params.end_date = endDate; - - const res = await apiGet('/api/admin/conduct/history', params); - if (res && res.success && res.data.records) { - const records = res.data.records; - if (records.length === 0) { - showToast('没有找到记录', 'warning'); - return; - } - - // 构建CSV内容 - let csv = '\uFEFF'; // BOM for UTF-8 - csv += '时间,学号,姓名,分数变动,原因,操作人\n'; - records.forEach(r => { - csv += `${r.created_at || ''},${r.student_no || ''},${r.student_name || ''},${r.points_change > 0 ? '+' : ''}${r.points_change},${(r.reason || '').replace(/,/g, ';')},${r.recorder_name || ''}\n`; - }); - - // 下载文件 - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `操行分记录_${new Date().toISOString().slice(0,10)}.csv`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - showToast(`导出成功,共${records.length}条记录`); - } else { - showToast('导出失败:' + (res?.message || '未知错误'), 'error'); + // 获取所有学生 + const studentsRes = await apiGet('/api/admin/students', { page_size: 1000 }); + if (!studentsRes || !studentsRes.success) { + showToast('获取学生列表失败', 'error'); + return; } + + const students = studentsRes.data.students; + if (students.length === 0) { + showToast('没有找到学生', 'warning'); + return; + } + + // 获取每个学生的历史记录 + const studentRecords = []; + for (const student of students) { + const historyRes = await apiGet(`/api/student/conduct/${student.student_id}`, { limit: 1000 }); + if (historyRes && historyRes.success) { + const records = historyRes.data.records || []; + const positiveRecords = records.filter(r => r.points_change > 0).map(r => `${r.reason}(${r.points_change > 0 ? '+' : ''}${r.points_change})`); + const negativeRecords = records.filter(r => r.points_change < 0).map(r => `${r.reason}(${r.points_change})`); + + studentRecords.push({ + student_no: student.student_no, + name: student.name, + total_points: historyRes.data.total_points || 0, + positive_history: positiveRecords.join(', '), + negative_history: negativeRecords.join(', ') + }); + } + } + + // 构建CSV内容 + let csv = '\uFEFF'; // BOM for UTF-8 + csv += '学号,姓名,分数,加分历史,减分记录\n'; + studentRecords.forEach(s => { + csv += `${s.student_no},${s.name},${s.total_points},"${(s.positive_history || '').replace(/"/g, '""')}","${(s.negative_history || '').replace(/"/g, '""')}"\n`; + }); + + // 下载文件 + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `德育分记录_${new Date().toISOString().slice(0,10)}.csv`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + showToast(`导出成功,共${studentRecords.length}名学生`); } catch (err) { showToast('导出失败:' + err.message, 'error'); } diff --git a/frontend/admin/dashboard.php b/frontend/admin/dashboard.php index ac62e7e..910d0a6 100644 --- a/frontend/admin/dashboard.php +++ b/frontend/admin/dashboard.php @@ -63,7 +63,7 @@ async function loadDashboard() { } if ('' === '班主任') { quickActions += ''; - quickActions += ''; + quickActions += ''; } document.getElementById('quickActions').innerHTML = quickActions || '

暂无快捷操作

'; diff --git a/frontend/admin/history.php b/frontend/admin/history.php index 1820718..7d6bf40 100644 --- a/frontend/admin/history.php +++ b/frontend/admin/history.php @@ -42,6 +42,9 @@ include __DIR__ . '/../includes/header.php'; + + +
@@ -142,6 +145,55 @@ function renderHistoryPagination() { container.innerHTML = html; } +// 导出历史记录 +async function exportHistoryRecords() { + const startDate = document.getElementById('historyStartDate').value; + const endDate = document.getElementById('historyEndDate').value; + const studentId = document.getElementById('historyStudentId').value; + + showToast('正在导出历史记录...', 'info'); + + try { + const params = { page: 1, page_size: 1000 }; + if (startDate) params.start_date = startDate; + if (endDate) params.end_date = endDate; + if (studentId) params.student_id = studentId; + + const res = await apiGet('/api/admin/conduct/history', params); + if (res && res.success && res.data.records) { + const records = res.data.records; + if (records.length === 0) { + showToast('没有找到记录', 'warning'); + return; + } + + // 构建CSV内容 + let csv = '\uFEFF'; // BOM for UTF-8 + csv += '时间,学号,姓名,分数变动,原因,操作人\n'; + records.forEach(r => { + csv += `${r.created_at || ''},${r.student_no || ''},${r.student_name || ''},${r.points_change > 0 ? '+' : ''}${r.points_change},${(r.reason || '').replace(/,/g, ';')},${r.recorder_name || ''}\n`; + }); + + // 下载文件 + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `历史记录_${new Date().toISOString().slice(0,10)}.csv`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + + showToast(`导出成功,共${records.length}条记录`); + } else { + showToast('导出失败:' + (res?.message || '未知错误'), 'error'); + } + } catch (err) { + showToast('导出失败:' + err.message, 'error'); + } +} + loadStudentsForSelect(); loadHistory(); diff --git a/frontend/student/dashboard.php b/frontend/student/dashboard.php index 67e6d39..e446a85 100644 --- a/frontend/student/dashboard.php +++ b/frontend/student/dashboard.php @@ -301,7 +301,7 @@ include __DIR__ . '/../includes/header.php'; const ranking = rankingRes.data.ranking || []; const rank = ranking.find(s => s.student_id === parseInt(STUDENT_ID)); if (rank) { - document.getElementById('studentRank').textContent = `第${rank.rank}名 / ${ranking.length}人`; + document.getElementById('studentRank').textContent = rank.rank; } else { document.getElementById('studentRank').textContent = '--'; }