- 新增 platform_scheduled_tasks 和 platform_task_logs 数据表 - 实现 APScheduler 调度器服务(支持简单模式和CRON表达式) - 添加定时任务 CRUD API - 支持手动触发执行和查看执行日志 - 前端任务管理页面
This commit is contained in:
356
backend/app/routers/tasks.py
Normal file
356
backend/app/routers/tasks.py
Normal file
@@ -0,0 +1,356 @@
|
||||
"""定时任务管理路由"""
|
||||
import asyncio
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
|
||||
from ..database import get_db
|
||||
from .auth import get_current_user, require_operator
|
||||
from ..models.user import User
|
||||
from ..services.scheduler import (
|
||||
add_task_to_scheduler,
|
||||
remove_task_from_scheduler,
|
||||
reload_task,
|
||||
execute_task
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/scheduled-tasks", tags=["定时任务"])
|
||||
|
||||
|
||||
# Schemas
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
tenant_id: str
|
||||
task_name: str
|
||||
task_desc: Optional[str] = None
|
||||
schedule_type: str = "simple" # simple | cron
|
||||
time_points: Optional[List[str]] = None # ["09:00", "14:00"]
|
||||
cron_expression: Optional[str] = None # "0 9,14 * * *"
|
||||
webhook_url: str
|
||||
input_params: Optional[dict] = None
|
||||
is_enabled: bool = True
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
task_name: Optional[str] = None
|
||||
task_desc: Optional[str] = None
|
||||
schedule_type: Optional[str] = None
|
||||
time_points: Optional[List[str]] = None
|
||||
cron_expression: Optional[str] = None
|
||||
webhook_url: Optional[str] = None
|
||||
input_params: Optional[dict] = None
|
||||
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@router.get("")
|
||||
async def list_tasks(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
tenant_id: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取定时任务列表"""
|
||||
# 构建查询
|
||||
where_clauses = []
|
||||
params = {}
|
||||
|
||||
if tenant_id:
|
||||
where_clauses.append("tenant_id = :tenant_id")
|
||||
params["tenant_id"] = tenant_id
|
||||
|
||||
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
|
||||
|
||||
# 查询总数
|
||||
count_result = db.execute(
|
||||
text(f"SELECT COUNT(*) FROM platform_scheduled_tasks WHERE {where_sql}"),
|
||||
params
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询列表
|
||||
params["offset"] = (page - 1) * size
|
||||
params["limit"] = size
|
||||
result = db.execute(
|
||||
text(f"""
|
||||
SELECT t.*, tn.name as tenant_name
|
||||
FROM platform_scheduled_tasks t
|
||||
LEFT JOIN platform_tenants tn ON t.tenant_id = tn.code
|
||||
WHERE {where_sql}
|
||||
ORDER BY t.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""),
|
||||
params
|
||||
)
|
||||
tasks = [dict(row) for row in result.mappings().all()]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": tasks
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_task(
|
||||
data: TaskCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建定时任务"""
|
||||
# 验证调度配置
|
||||
if data.schedule_type == "simple":
|
||||
if not data.time_points or len(data.time_points) == 0:
|
||||
raise HTTPException(status_code=400, detail="简单模式需要至少一个时间点")
|
||||
elif data.schedule_type == "cron":
|
||||
if not data.cron_expression:
|
||||
raise HTTPException(status_code=400, detail="CRON模式需要提供表达式")
|
||||
|
||||
# 插入数据库
|
||||
import json
|
||||
time_points_json = json.dumps(data.time_points) if data.time_points else None
|
||||
input_params_json = json.dumps(data.input_params) if data.input_params else None
|
||||
|
||||
db.execute(
|
||||
text("""
|
||||
INSERT INTO platform_scheduled_tasks
|
||||
(tenant_id, task_name, task_desc, schedule_type, time_points,
|
||||
cron_expression, webhook_url, input_params, is_enabled)
|
||||
VALUES (:tenant_id, :task_name, :task_desc, :schedule_type, :time_points,
|
||||
:cron_expression, :webhook_url, :input_params, :is_enabled)
|
||||
"""),
|
||||
{
|
||||
"tenant_id": data.tenant_id,
|
||||
"task_name": data.task_name,
|
||||
"task_desc": data.task_desc,
|
||||
"schedule_type": data.schedule_type,
|
||||
"time_points": time_points_json,
|
||||
"cron_expression": data.cron_expression,
|
||||
"webhook_url": data.webhook_url,
|
||||
"input_params": input_params_json,
|
||||
"is_enabled": 1 if data.is_enabled else 0
|
||||
}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# 获取新插入的ID
|
||||
result = db.execute(text("SELECT LAST_INSERT_ID() as id"))
|
||||
task_id = result.scalar()
|
||||
|
||||
# 如果启用,添加到调度器
|
||||
if data.is_enabled:
|
||||
reload_task(task_id)
|
||||
|
||||
return {"id": task_id, "message": "创建成功"}
|
||||
|
||||
|
||||
@router.get("/{task_id}")
|
||||
async def get_task(
|
||||
task_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取任务详情"""
|
||||
result = db.execute(
|
||||
text("""
|
||||
SELECT t.*, tn.name as tenant_name
|
||||
FROM platform_scheduled_tasks t
|
||||
LEFT JOIN platform_tenants tn ON t.tenant_id = tn.code
|
||||
WHERE t.id = :id
|
||||
"""),
|
||||
{"id": task_id}
|
||||
)
|
||||
task = result.mappings().first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
return dict(task)
|
||||
|
||||
|
||||
@router.put("/{task_id}")
|
||||
async def update_task(
|
||||
task_id: int,
|
||||
data: TaskUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新定时任务"""
|
||||
# 检查任务是否存在
|
||||
result = db.execute(
|
||||
text("SELECT * FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
task = result.mappings().first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 构建更新语句
|
||||
import json
|
||||
updates = []
|
||||
params = {"id": task_id}
|
||||
|
||||
if data.task_name is not None:
|
||||
updates.append("task_name = :task_name")
|
||||
params["task_name"] = data.task_name
|
||||
if data.task_desc is not None:
|
||||
updates.append("task_desc = :task_desc")
|
||||
params["task_desc"] = data.task_desc
|
||||
if data.schedule_type is not None:
|
||||
updates.append("schedule_type = :schedule_type")
|
||||
params["schedule_type"] = data.schedule_type
|
||||
if data.time_points is not None:
|
||||
updates.append("time_points = :time_points")
|
||||
params["time_points"] = json.dumps(data.time_points)
|
||||
if data.cron_expression is not None:
|
||||
updates.append("cron_expression = :cron_expression")
|
||||
params["cron_expression"] = data.cron_expression
|
||||
if data.webhook_url is not None:
|
||||
updates.append("webhook_url = :webhook_url")
|
||||
params["webhook_url"] = data.webhook_url
|
||||
if data.input_params is not None:
|
||||
updates.append("input_params = :input_params")
|
||||
params["input_params"] = json.dumps(data.input_params)
|
||||
|
||||
if updates:
|
||||
db.execute(
|
||||
text(f"UPDATE platform_scheduled_tasks SET {', '.join(updates)} WHERE id = :id"),
|
||||
params
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# 重新加载调度器中的任务
|
||||
reload_task(task_id)
|
||||
|
||||
return {"message": "更新成功"}
|
||||
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
async def delete_task(
|
||||
task_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除定时任务"""
|
||||
# 检查任务是否存在
|
||||
result = db.execute(
|
||||
text("SELECT id FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
if not result.scalar():
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 从调度器移除
|
||||
remove_task_from_scheduler(task_id)
|
||||
|
||||
# 删除日志
|
||||
db.execute(
|
||||
text("DELETE FROM platform_task_logs WHERE task_id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
|
||||
# 删除任务
|
||||
db.execute(
|
||||
text("DELETE FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.post("/{task_id}/toggle")
|
||||
async def toggle_task(
|
||||
task_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""启用/禁用任务"""
|
||||
# 获取当前状态
|
||||
result = db.execute(
|
||||
text("SELECT is_enabled FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
row = result.first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
current_enabled = row[0]
|
||||
new_enabled = 0 if current_enabled else 1
|
||||
|
||||
# 更新状态
|
||||
db.execute(
|
||||
text("UPDATE platform_scheduled_tasks SET is_enabled = :enabled WHERE id = :id"),
|
||||
{"id": task_id, "enabled": new_enabled}
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# 更新调度器
|
||||
reload_task(task_id)
|
||||
|
||||
return {
|
||||
"is_enabled": bool(new_enabled),
|
||||
"message": "已启用" if new_enabled else "已禁用"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{task_id}/run")
|
||||
async def run_task_now(
|
||||
task_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""手动执行任务"""
|
||||
# 检查任务是否存在
|
||||
result = db.execute(
|
||||
text("SELECT id FROM platform_scheduled_tasks WHERE id = :id"),
|
||||
{"id": task_id}
|
||||
)
|
||||
if not result.scalar():
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 在后台执行任务
|
||||
background_tasks.add_task(asyncio.create_task, execute_task(task_id))
|
||||
|
||||
return {"message": "任务已触发执行"}
|
||||
|
||||
|
||||
@router.get("/{task_id}/logs")
|
||||
async def get_task_logs(
|
||||
task_id: int,
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取任务执行日志"""
|
||||
# 查询总数
|
||||
count_result = db.execute(
|
||||
text("SELECT COUNT(*) FROM platform_task_logs WHERE task_id = :task_id"),
|
||||
{"task_id": task_id}
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询日志
|
||||
result = db.execute(
|
||||
text("""
|
||||
SELECT * FROM platform_task_logs
|
||||
WHERE task_id = :task_id
|
||||
ORDER BY id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""),
|
||||
{"task_id": task_id, "limit": size, "offset": (page - 1) * size}
|
||||
)
|
||||
logs = [dict(row) for row in result.mappings().all()]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": logs
|
||||
}
|
||||
Reference in New Issue
Block a user