Files
FastapiAdmin/backend/app/api/v1/controllers/monitor/task_controller.py
T
zhangtao b05c9b035f feat(monitor): 添加任务管理模块
- 在 README 中添加了 Celery 任务管理的说明
- 在系统菜单中添加了任务管理相关菜单项
- 添加了任务管理的路由和权限
- 引入了 croniter 库用于处理 cron 表达式
2025-02-24 03:50:47 +08:00

128 lines
5.5 KiB
Python

# -*- coding: utf-8 -*-
from datetime import datetime, timezone
import json
from typing import Optional
import croniter
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, WebSocket, Request
from fastapi.responses import StreamingResponse
from celery.result import AsyncResult
from pydantic import BaseModel
from redis import Redis
from celery_app import celery_app
from app.core.dependencies import AuthPermission
from app.core.tasks import task
from app.common.response import SuccessResponse
from app.core.logger import logger
router = APIRouter()
@router.get("/list", summary="任务列表", dependencies=[Depends(AuthPermission(permissions=["monitor:task:query"]))])
async def get_tasks(request: Request):
redis: Redis = request.app.state.redis
task_keys = await redis.keys("celery-task-meta-*") # 假设使用异步 Redis 客户端
tasks = []
if not task_keys:
return SuccessResponse(msg="获取任务列表成功", data=[])
for key in task_keys:
if isinstance(key, bytes): # 兼容字节类型
task_id = key.decode().split("celery-task-meta-")[-1]
else: # 字符串类型
task_id = key.split("celery-task-meta-")[-1]
task_data = await redis.get(key) # 假设使用异步 Redis 客户端
if task_data:
try:
task_data = json.loads(task_data.decode()) if isinstance(task_data, bytes) else json.loads(task_data)
except json.JSONDecodeError:
task_data = task_data.decode() if isinstance(task_data, bytes) else task_data
tasks.append({"task_id": task_id, "result": task_data})
return SuccessResponse(msg="获取任务列表成功", data=tasks)
@router.get("/detail/{task_id}", summary="任务详情", dependencies=[Depends(AuthPermission(permissions=["monitor:task:query"]))])
async def check_task(task_id: str):
result = AsyncResult(id=task_id, app=celery_app)
if result.state == 'PENDING': # 如果任务不存在或未启动
raise HTTPException(status_code=404, detail="任务不存在")
status_map = {
'SUCCESS': '成功',
'FAILURE': '失败',
'PENDING': '等待中',
'RETRY': '重试中',
'STARTED': '运行中',
}
status = status_map.get(result.state, result.state)
data = {
"task_id": task_id,
"status": status,
"result": str(result.info) if result.failed() else result.info, # 失败时转换为字符串
"date_done": result.date_done.isoformat() if result.date_done else None, # 转换为字符串并处理 None
}
return SuccessResponse(msg="获取任务详情成功", data=data)
class CreateTaskSchema(BaseModel):
res: str
is_cron: bool
cron_expression: Optional[str] = None
@router.post("/create", summary="任务创建", dependencies=[Depends(AuthPermission(permissions=["monitor:task:create"]))])
async def start_task(data: CreateTaskSchema):
try:
if data.is_cron:
if not data.cron_expression:
raise HTTPException(status_code=400, detail="定时任务必须提供 cron 表达式")
# 校验 cron 表达式
try:
now = datetime.now(timezone.utc)
cron = croniter(data.cron_expression, now)
next_run_time = cron.get_next(datetime)
except Exception as e:
logger.error(f"无效的 cron 表达式: {data.cron_expression}, 错误: {e}")
raise HTTPException(status_code=400, detail="无效的 cron 表达式")
result = task.apply_async(args=[data.res], eta=next_run_time)
else:
result = task.delay(data.res)
logger.info(f"任务创建成功, 任务ID: {result.id}")
return SuccessResponse(msg="创建任务成功", data=result.id)
except Exception as e:
logger.error(f"任务创建失败: {e}")
raise HTTPException(status_code=500, detail="任务创建失败")
@router.put("/cancel/{task_id}", summary="任务取消(默认:强制终止)", dependencies=[Depends(AuthPermission(permissions=["monitor:task:update"]))])
async def cancel_task(task_id: str):
result = AsyncResult(id=task_id, app=celery_app)
if result.state in ('SUCCESS', 'FAILURE'):
raise HTTPException(status_code=400, detail="任务已完成,无法取消")
result.revoke(terminate=True)
return SuccessResponse(msg=f"任务{task_id}已取消", data=task_id)
@router.delete("/delete/{task_id}", summary="任务删除", dependencies=[Depends(AuthPermission(permissions=["monitor:task:delete"]))])
async def delete_task(task_id: str):
result = AsyncResult(id=task_id, app=celery_app)
if result.state == 'STARTED':
raise HTTPException(status_code=400, detail="任务正在运行,无法删除")
result.forget()
return SuccessResponse(msg=f"任务{task_id}已删除", data=task_id)
@router.post("/background_tasks", summary="模拟fastapi自带后台任务-模拟流式响应")
async def stream_response(background_tasks: BackgroundTasks, *args, **kwargs):
background_tasks.add_task(task, *args, **kwargs)
return StreamingResponse(
data=task(*args, **kwargs),
headers={"X-Custom-Header": "Streaming-Response"},
media_type="text/plain",
)
@router.websocket("/ws", name="websocket")
async def websocket_endpoint(websocket: WebSocket):
# ws://127.0.0.1:8000/ws
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")