"""定时任务 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)