You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/buffer_platform_v2/app/api/tasks.py

216 lines
6.3 KiB

"""定时任务 API 路由 - 创建/启动/停止/删除/列表"""
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.market import ScheduledTask
from app.schemas import CreateTaskRequest, TaskInfo, TaskListResponse
from app.services.cache import (
create_task,
delete_task,
disable_task,
enable_task,
get_task,
)
from app.services.scheduler import (
add_job,
get_all_jobs,
is_job_running,
remove_job,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tasks", tags=["定时任务"])
def _to_task_info(task: ScheduledTask, job_info: dict | None = None) -> TaskInfo:
"""ORM -> Pydantic TaskInfo。"""
next_run = None
if job_info and job_info.get("next_run_time"):
next_run = job_info["next_run_time"]
return TaskInfo(
id=task.id,
symbol=task.symbol,
data_type=task.data_type,
periods=task.periods.split(",") if task.periods else [],
interval_seconds=task.interval_seconds,
task_type=task.task_type if hasattr(task, "task_type") else "interval",
enabled=task.enabled,
running=False, # 由调用方异步更新
last_run=task.last_run.isoformat() if task.last_run else None,
last_status=task.last_status,
next_run=next_run,
created_at=task.created_at.isoformat(),
updated_at=task.updated_at.isoformat(),
)
@router.post("", response_model=TaskInfo)
async def create_new_task(
req: CreateTaskRequest,
db: AsyncSession = Depends(get_db),
):
"""创建并启动一个定时采集任务。"""
periods_str = ",".join(req.periods) if isinstance(req.periods, list) else req.periods
task = await create_task(
db=db,
symbol=req.symbol,
data_type=req.data_type,
periods=periods_str,
interval_seconds=req.interval_seconds,
task_type=req.task_type,
run_time=req.run_time,
)
job_id = await add_job(task.id, task.interval_seconds, task.task_type, task.run_time)
task.job_id = job_id
await db.flush()
return _to_task_info(task)
@router.get("", response_model=TaskListResponse)
async def list_all_tasks(
db: AsyncSession = Depends(get_db),
):
"""列出所有定时任务(未完成的)。"""
stmt = (
select(ScheduledTask)
.where(ScheduledTask.is_finished == False) # noqa: E712
.order_by(ScheduledTask.created_at.desc())
)
result = await db.execute(stmt)
tasks = list(result.scalars().all())
job_status = await get_all_jobs()
task_infos = []
for t in tasks:
job_id = f"task_{t.id}"
job_info = job_status.get(job_id)
info = _to_task_info(t, job_info)
info.running = await is_job_running(t.id)
task_infos.append(info)
return TaskListResponse(tasks=task_infos, total=len(task_infos))
@router.get("/history", response_model=TaskListResponse)
async def list_finished_tasks(
db: AsyncSession = Depends(get_db),
):
"""列出已完成的历史任务。"""
stmt = (
select(ScheduledTask)
.where(ScheduledTask.is_finished == True) # noqa: E712
.order_by(ScheduledTask.updated_at.desc())
)
result = await db.execute(stmt)
tasks = list(result.scalars().all())
task_infos = [_to_task_info(t) for t in tasks]
return TaskListResponse(tasks=task_infos, total=len(task_infos))
@router.post("/{task_id}/rerun", response_model=TaskInfo)
async def rerun_task(
task_id: int,
db: AsyncSession = Depends(get_db),
):
"""重新执行已完成的任务。"""
task = await get_task(db, task_id)
if not task:
raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在")
if not task.is_finished:
raise HTTPException(status_code=400, detail=f"任务 {task_id} 尚未完成,无法重新执行")
task.is_finished = False
task.enabled = True
task.last_run = None
task.last_status = None
task.updated_at = datetime.now()
await db.flush()
job_id = await add_job(task.id, task.interval_seconds, task.task_type, task.run_time)
task.job_id = job_id
await db.flush()
return _to_task_info(task)
@router.post("/{task_id}/stop", response_model=TaskInfo)
async def stop_task(
task_id: int,
db: AsyncSession = Depends(get_db),
):
"""停止定时任务(从调度器移除,保留配置)。"""
task = await get_task(db, task_id)
if not task:
raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在")
await remove_job(task_id)
task = await disable_task(db, task_id)
return _to_task_info(task)
@router.post("/{task_id}/start", response_model=TaskInfo)
async def start_task(
task_id: int,
db: AsyncSession = Depends(get_db),
):
"""重新启动已停止的定时任务。"""
task = await get_task(db, task_id)
if not task:
raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在")
await enable_task(db, task_id)
await add_job(task.id, task.interval_seconds, task.task_type, task.run_time)
return _to_task_info(task)
@router.delete("/{task_id}")
async def delete_existing_task(
task_id: int,
db: AsyncSession = Depends(get_db),
):
"""删除定时任务(同时从调度器移除)。"""
task = await get_task(db, task_id)
if not task:
raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在")
await remove_job(task_id)
await delete_task(db, task_id)
return {"message": f"任务 {task_id} 已删除"}
@router.post("/{task_id}/update-interval", response_model=TaskInfo)
async def update_interval(
task_id: int,
interval_seconds: int,
db: AsyncSession = Depends(get_db),
):
"""更新任务的轮询间隔。"""
task = await get_task(db, task_id)
if not task:
raise HTTPException(status_code=404, detail=f"任务 {task_id} 不存在")
task.interval_seconds = interval_seconds
task.updated_at = datetime.now()
await db.flush()
if task.enabled and await is_job_running(task_id):
await remove_job(task_id)
await add_job(task.id, task.interval_seconds, task.task_type, task.run_time)
return _to_task_info(task)