feat(conduct): add restore revoked record functionality and update history view
This commit is contained in:
@@ -82,7 +82,8 @@ class ConductModel:
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
start_date: str = None,
|
start_date: str = None,
|
||||||
end_date: str = None,
|
end_date: str = None,
|
||||||
student_id: int = None
|
student_id: int = None,
|
||||||
|
include_revoked: bool = True
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""获取所有记录(班主任/班长专用)"""
|
"""获取所有记录(班主任/班长专用)"""
|
||||||
# 空字符串转为None
|
# 空字符串转为None
|
||||||
@@ -96,8 +97,10 @@ class ConductModel:
|
|||||||
FROM conduct_records cr
|
FROM conduct_records cr
|
||||||
JOIN students s ON cr.student_id = s.student_id
|
JOIN students s ON cr.student_id = s.student_id
|
||||||
JOIN users u ON cr.recorder_id = u.user_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 = []
|
params = []
|
||||||
|
|
||||||
if student_id:
|
if student_id:
|
||||||
@@ -210,6 +213,21 @@ class ConductModel:
|
|||||||
logger.error(f"撤销记录失败: {e}")
|
logger.error(f"撤销记录失败: {e}")
|
||||||
return False
|
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
|
@staticmethod
|
||||||
async def batch_create_records(records_data: List[Dict]) -> List[Dict]:
|
async def batch_create_records(records_data: List[Dict]) -> List[Dict]:
|
||||||
"""批量创建操行分记录"""
|
"""批量创建操行分记录"""
|
||||||
|
|||||||
@@ -231,6 +231,33 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
|
|||||||
return error_response(message=result["message"])
|
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")
|
@router.get("/conduct/history")
|
||||||
async def get_conduct_history(
|
async def get_conduct_history(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -152,6 +152,42 @@ class ConductService:
|
|||||||
else:
|
else:
|
||||||
return {"success": False, "message": "撤销失败"}
|
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
|
@staticmethod
|
||||||
async def get_history(
|
async def get_history(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
@@ -193,7 +229,7 @@ class ConductService:
|
|||||||
|
|
||||||
# 获取总数
|
# 获取总数
|
||||||
from utils.database import execute_one
|
from utils.database import execute_one
|
||||||
count_conditions = ["cr.is_revoked = 0"]
|
count_conditions = ["1=1"]
|
||||||
count_params = []
|
count_params = []
|
||||||
if student_id:
|
if student_id:
|
||||||
count_conditions.append("cr.student_id = %s")
|
count_conditions.append("cr.student_id = %s")
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<tr>
|
<tr>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th>学生</th>
|
<th>学生</th>
|
||||||
<th>人数</th>
|
|
||||||
<th>分数变动</th>
|
<th>分数变动</th>
|
||||||
<th>原因</th>
|
<th>原因</th>
|
||||||
<th>操作人</th>
|
<th>操作人</th>
|
||||||
@@ -92,8 +91,7 @@ async function loadHistory(page = 1) {
|
|||||||
const params = {
|
const params = {
|
||||||
page, page_size: 20,
|
page, page_size: 20,
|
||||||
start_date: startDate,
|
start_date: startDate,
|
||||||
end_date: endDate,
|
end_date: endDate
|
||||||
grouped: true
|
|
||||||
};
|
};
|
||||||
if (studentId) params.student_id = studentId;
|
if (studentId) params.student_id = studentId;
|
||||||
|
|
||||||
@@ -103,21 +101,31 @@ async function loadHistory(page = 1) {
|
|||||||
let html = '';
|
let html = '';
|
||||||
res.data.records.forEach(record => {
|
res.data.records.forEach(record => {
|
||||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
html += `<tr>
|
const revokedStyle = record.is_revoked == 1 ? ' style="opacity:0.5; text-decoration:line-through;"' : '';
|
||||||
|
html += `<tr${revokedStyle}>
|
||||||
<td>${formatDateTime(record.created_at)}</td>
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
<td>${escapeHtml(record.student_names)}</td>
|
<td>${escapeHtml(record.student_name)}</td>
|
||||||
<td>${record.student_count || 1}</td>
|
|
||||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${escapeHtml(record.reason)}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
<?php if ($role === '班主任'): ?>
|
||||||
html += `<td>-</td>`;
|
if (record.is_revoked == 1) {
|
||||||
|
html += `<td><span class="text-muted" style="margin-right:4px;">已撤销</span><button class="btn btn-sm btn-secondary" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
|
||||||
|
} else {
|
||||||
|
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||||
|
}
|
||||||
|
<?php elseif ($role === '班长'): ?>
|
||||||
|
if (record.is_revoked == 1) {
|
||||||
|
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||||
|
} else {
|
||||||
|
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||||
|
}
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
html += `</tr>`;
|
html += `</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.records.length === 0) {
|
if (res.data.records.length === 0) {
|
||||||
const colSpan = <?php echo ($role === '班主任' || $role === '班长') ? '7' : '6'; ?>;
|
const colSpan = <?php echo ($role === '班主任' || $role === '班长') ? '6' : '5'; ?>;
|
||||||
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ async function submitAddSubject() {
|
|||||||
|
|
||||||
// 撤销扣分记录
|
// 撤销扣分记录
|
||||||
async function revokeRecord(recordId) {
|
async function revokeRecord(recordId) {
|
||||||
if (!confirm('确定要撤销这条扣分记录吗?')) return;
|
if (!confirm('确定要撤销这条记录吗?撤销后学生分数将恢复。')) return;
|
||||||
|
|
||||||
const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId });
|
const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId });
|
||||||
if (res && res.success) {
|
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) {
|
function closeModal(modalId) {
|
||||||
const modal = document.getElementById(modalId);
|
const modal = document.getElementById(modalId);
|
||||||
|
|||||||
Reference in New Issue
Block a user