"""定时任务管理路由""" 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 * * *" execution_type: str = "webhook" # webhook | script webhook_url: Optional[str] = None input_params: Optional[dict] = None script_content: Optional[str] = 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 execution_type: Optional[str] = None webhook_url: Optional[str] = None input_params: Optional[dict] = None script_content: Optional[str] = None class ScriptTestRequest(BaseModel): tenant_id: str script_content: str # 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模式需要提供表达式") # 验证执行配置 if data.execution_type == "webhook": if not data.webhook_url: raise HTTPException(status_code=400, detail="Webhook模式需要提供URL") elif data.execution_type == "script": if not data.script_content: raise HTTPException(status_code=400, detail="脚本模式需要提供脚本内容") # 插入数据库 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, execution_type, webhook_url, input_params, script_content, is_enabled) VALUES (:tenant_id, :task_name, :task_desc, :schedule_type, :time_points, :cron_expression, :execution_type, :webhook_url, :input_params, :script_content, :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, "execution_type": data.execution_type, "webhook_url": data.webhook_url, "input_params": input_params_json, "script_content": data.script_content, "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.execution_type is not None: updates.append("execution_type = :execution_type") params["execution_type"] = data.execution_type if data.webhook_url is not None: updates.append("webhook_url = :webhook_url") params["webhook_url"] = data.webhook_url if data.script_content is not None: updates.append("script_content = :script_content") params["script_content"] = data.script_content 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 } @router.post("/test-script") async def test_script( data: ScriptTestRequest, user: User = Depends(require_operator) ): """测试执行脚本(不记录日志)""" from ..services.script_executor import test_script as run_test if not data.script_content or not data.script_content.strip(): raise HTTPException(status_code=400, detail="脚本内容不能为空") result = await run_test( tenant_id=data.tenant_id, script_content=data.script_content ) return result.to_dict() @router.get("/sdk-docs") async def get_sdk_docs(): """获取 SDK 文档""" return { "description": "脚本执行 SDK 文档", "methods": [ { "name": "ai(prompt, system=None, model='gemini-2.5-flash')", "description": "调用大模型生成内容", "example": "result = ai('帮我写一段营销文案')" }, { "name": "dingtalk(webhook, content, msg_type='text', at_mobiles=None)", "description": "发送钉钉群消息", "example": "dingtalk('https://oapi.dingtalk.com/robot/send?access_token=xxx', '消息内容')" }, { "name": "wecom(webhook, content, msg_type='text')", "description": "发送企业微信群消息", "example": "wecom('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx', '消息内容')" }, { "name": "db(sql, params=None)", "description": "执行 SQL 查询(仅支持 SELECT)", "example": "rows = db('SELECT * FROM users WHERE id = :id', {'id': 1})" }, { "name": "http_get(url, headers=None, params=None)", "description": "发送 HTTP GET 请求", "example": "data = http_get('https://api.example.com/data')" }, { "name": "http_post(url, data=None, json_data=None, headers=None)", "description": "发送 HTTP POST 请求", "example": "data = http_post('https://api.example.com/submit', json_data={'key': 'value'})" }, { "name": "get_var(key, default=None)", "description": "获取存储的变量(跨执行持久化)", "example": "count = get_var('run_count', 0)" }, { "name": "set_var(key, value)", "description": "存储变量(跨执行持久化)", "example": "set_var('run_count', count + 1)" }, { "name": "log(message, level='INFO')", "description": "记录日志", "example": "log('任务执行完成')" } ], "example_script": '''# 示例:每日推送 AI 生成的内容到钉钉 import json # 获取历史数据 history = get_var('history', []) # 调用 AI 生成内容 prompt = f"根据以下信息生成今日营销文案:{json.dumps(history[-5:], ensure_ascii=False)}" content = ai(prompt, system="你是一个专业的营销文案专家") # 发送到钉钉 dingtalk( webhook="你的钉钉机器人Webhook", content=content ) # 记录日志 log(f"已发送: {content[:50]}...") ''' }