重试队列增强 + 审计分页

- 后端: 添加 POST /api/retries/{id}/retry (手动重试) + DELETE /api/retries/{id}
  + 审计日志分页(offset/limit/count) + PushRetryJobRepo新增get_all/get_by_id/delete
- 前端retries: 手动重试按钮、删除按钮、状态过滤、操作人显示、toast反馈
- 前端audit: 分页控件(上一页/下一页)、每页50条、新增retry_job/delete_retry过滤
- 修复index.html: 适配审计API新返回格式(data.items)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-22 09:41:33 +08:00
parent 120ae186ef
commit 249a1aaed9
6 changed files with 199 additions and 34 deletions
+73 -8
View File
@@ -239,20 +239,27 @@ async def delete_preset(
audit_router = APIRouter(prefix="/api/audit", tags=["audit"])
@audit_router.get("/", response_model=list)
@audit_router.get("/", response_model=dict)
async def list_audit_logs(
action: Optional[str] = None,
limit: int = 200,
limit: int = 50,
offset: int = 0,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List recent audit logs, optionally filtered by action"""
"""List audit logs with pagination"""
repo = AuditLogRepositoryImpl(db)
total = await repo.count(action)
if action:
logs = await repo.get_by_action(action, limit)
logs = await repo.get_by_action(action, limit, offset)
else:
logs = await repo.get_recent(limit)
return [_audit_to_dict(log) for log in logs]
logs = await repo.get_recent(limit, offset)
return {
"total": total,
"limit": limit,
"offset": offset,
"items": [_audit_to_dict(log) for log in logs],
}
# ── Retry Jobs ──
@@ -266,12 +273,70 @@ async def list_retry_jobs(
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List pending retry jobs"""
"""List all retry jobs (not just pending)"""
repo = PushRetryJobRepositoryImpl(db)
jobs = await repo.get_pending(limit)
jobs = await repo.get_all(limit)
return [_retry_to_dict(j) for j in jobs]
@retry_router.post("/{id}/retry", response_model=dict)
async def retry_job(
id: int,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Manually retry a failed/pending job"""
repo = PushRetryJobRepositoryImpl(db)
job = await repo.get_by_id(id)
if not job:
raise HTTPException(status_code=404, detail="Retry job not found")
# Reset the job to pending state for immediate retry
job = await repo.update_status(
id, "pending",
retry_count=0,
next_retry_at=None,
last_error=None,
)
# Audit log
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username,
action="retry_job",
target_type="retry",
target_id=id,
detail=f"Manual retry triggered for job #{id}",
))
return _retry_to_dict(job)
@retry_router.delete("/{id}", status_code=204)
async def delete_retry_job(
id: int,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Delete a retry job"""
repo = PushRetryJobRepositoryImpl(db)
job = await repo.get_by_id(id)
if not job:
raise HTTPException(status_code=404, detail="Retry job not found")
# Audit log
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username,
action="delete_retry",
target_type="retry",
target_id=id,
detail=f"Retry job #{id} deleted",
))
await repo.delete(id)
# ── Helper functions ──
def _schedule_to_dict(schedule: PushSchedule) -> dict: