diff --git a/backend/models/conduct.py b/backend/models/conduct.py index e8d8f6a..0d120eb 100644 --- a/backend/models/conduct.py +++ b/backend/models/conduct.py @@ -82,7 +82,8 @@ class ConductModel: offset: int = 0, start_date: str = None, end_date: str = None, - student_id: int = None + student_id: int = None, + include_revoked: bool = True ) -> List[Dict[str, Any]]: """获取所有记录(班主任/班长专用)""" # 空字符串转为None @@ -96,8 +97,10 @@ class ConductModel: FROM conduct_records cr JOIN students s ON cr.student_id = s.student_id JOIN users u ON cr.recorder_id = u.user_id - WHERE cr.is_revoked = 0 + WHERE 1=1 """ + if not include_revoked: + sql += " AND cr.is_revoked = 0" params = [] if student_id: @@ -210,6 +213,21 @@ class ConductModel: logger.error(f"撤销记录失败: {e}") return False + @staticmethod + async def restore_record(record_id: int, restorer_id: int) -> bool: + """反撤销(恢复)已撤销的记录""" + try: + sql = """ + UPDATE conduct_records + SET is_revoked = 0, revoked_by = NULL, revoked_at = NULL + WHERE record_id = %s AND is_revoked = 1 + """ + result = await execute_update(sql, (record_id,)) + return result > 0 + except Exception as e: + logger.error(f"恢复记录失败: {e}") + return False + @staticmethod async def batch_create_records(records_data: List[Dict]) -> List[Dict]: """批量创建操行分记录""" diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 22a7dd0..9fa538f 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -231,6 +231,33 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest): return error_response(message=result["message"]) +@router.post("/conduct/restore") +async def restore_conduct_record(request: Request, req: RevokeRequest): + """反撤销(恢复)已撤销的记录""" + user = await get_current_user(request) + result = await ConductService.restore_record( + record_id=req.record_id, + restorer_id=user["user_id"] + ) + if result["success"]: + record = result.get("record", {}) + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["username"], + operator_role="班主任", operation_type="restore_record", + target_type="conduct", target_id=req.record_id, + details=( + f"反撤销记录ID: {req.record_id}, " + f"原操作人: {record.get('recorder_name', '未知')}, " + f"原分值变动: {'+' if record.get('points_change', 0) > 0 else ''}{record.get('points_change', 0)}分, " + f"反撤销操作人: {user['username']}" + ), + ip=request.client.host + ) + return success_response(message="反撤销成功") + else: + return error_response(message=result["message"]) + + @router.get("/conduct/history") async def get_conduct_history( request: Request, diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index c6c0d43..facd2ee 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -152,6 +152,42 @@ class ConductService: else: return {"success": False, "message": "撤销失败"} + @staticmethod + async def restore_record(record_id: int, restorer_id: int) -> Dict[str, Any]: + """反撤销(恢复)已撤销的记录""" + # 检查权限:只有班主任可以反撤销 + role = await PermissionChecker.get_user_role(restorer_id) + if role != "班主任": + return {"success": False, "message": "仅班主任可反撤销记录"} + + # 获取原记录信息 + record = await ConductModel.get_record_by_id(record_id) + if not record: + return {"success": False, "message": "记录不存在"} + + if not record.get("is_revoked"): + return {"success": False, "message": "该记录未被撤销,无需恢复"} + + # 恢复记录 + result = await ConductModel.restore_record(record_id, restorer_id) + + if result: + # 恢复学生总分(重新加上原来的分数变动) + await StudentModel.update_total_points(record["student_id"], record["points_change"]) + logger.info(f"用户[{restorer_id}] 反撤销了记录[{record_id}]") + return { + "success": True, + "message": "反撤销成功", + "record": { + "student_id": record["student_id"], + "recorder_name": record.get("recorder_name", "未知"), + "points_change": record["points_change"], + "reason": record.get("reason", "") + } + } + else: + return {"success": False, "message": "反撤销失败"} + @staticmethod async def get_history( user_id: int, @@ -193,7 +229,7 @@ class ConductService: # 获取总数 from utils.database import execute_one - count_conditions = ["cr.is_revoked = 0"] + count_conditions = ["1=1"] count_params = [] if student_id: count_conditions.append("cr.student_id = %s") diff --git a/frontend/admin/history.php b/frontend/admin/history.php index 0e0264c..270a523 100644 --- a/frontend/admin/history.php +++ b/frontend/admin/history.php @@ -53,7 +53,6 @@ include __DIR__ . '/../includes/header.php'; 时间 学生 - 人数 分数变动 原因 操作人 @@ -92,8 +91,7 @@ async function loadHistory(page = 1) { const params = { page, page_size: 20, start_date: startDate, - end_date: endDate, - grouped: true + end_date: endDate }; if (studentId) params.student_id = studentId; @@ -103,21 +101,31 @@ async function loadHistory(page = 1) { let html = ''; res.data.records.forEach(record => { const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; - html += ` + const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : ''; + html += ` ${formatDateTime(record.created_at)} - ${escapeHtml(record.student_names)} - ${record.student_count || 1} + ${escapeHtml(record.student_name)} ${record.points_change > 0 ? '+' : ''}${record.points_change} ${escapeHtml(record.reason)} ${escapeHtml(record.recorder_name)}`; - - html += `-`; + + if (record.is_revoked == 1) { + html += `已撤销`; + } else { + html += ``; + } + + if (record.is_revoked == 1) { + html += `已撤销`; + } else { + html += ``; + } html += ``; }); if (res.data.records.length === 0) { - const colSpan = ; + const colSpan = ; html = `暂无记录`; } diff --git a/frontend/assets/js/admin.js b/frontend/assets/js/admin.js index 2bb3d37..9141aa3 100644 --- a/frontend/assets/js/admin.js +++ b/frontend/assets/js/admin.js @@ -251,7 +251,7 @@ async function submitAddSubject() { // 撤销扣分记录 async function revokeRecord(recordId) { - if (!confirm('确定要撤销这条扣分记录吗?')) return; + if (!confirm('确定要撤销这条记录吗?撤销后学生分数将恢复。')) return; const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId }); if (res && res.success) { @@ -262,6 +262,19 @@ async function revokeRecord(recordId) { } } +// 反撤销(恢复)记录 +async function restoreRecord(recordId) { + if (!confirm('确定要反撤销这条记录吗?分数变动将重新生效。')) return; + + const res = await apiPost('/api/admin/conduct/restore', { record_id: recordId }); + if (res && res.success) { + showToast('反撤销成功'); + loadHistory(currentHistoryPage); + } else { + showToast(res?.message || '反撤销失败', 'error'); + } +} + // 关闭模态框 function closeModal(modalId) { const modal = document.getElementById(modalId);