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.
216 lines
6.3 KiB
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)
|