"""定时任务执行器。 支持两种任务类型: - external_api: 调用外部 HTTP 接口 - internal_method: 动态导入并执行内部函数 任务在 Celery Worker 中执行,通过 run_async 桥接异步操作。 """ import json import logging import time from datetime import datetime, timezone import httpx from sqlalchemy import select from app.models.base import async_session from app.models.scheduled_task import ScheduledTask from app.tasks.async_runner import run_async from app.tasks.celery_app import celery_app logger = logging.getLogger("video_gen") def _update_task_status(task_id: str, status: str, error_msg: str | None = None) -> None: """更新任务最后执行状态。""" async def _do(): async with async_session() as db: result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id)) task = result.scalar_one_or_none() if task is None: return task.last_run_at = datetime.now(timezone.utc).isoformat() task.last_status = status task.last_error = error_msg await db.commit() run_async(_do()) def _execute_external_api(config: str | None) -> dict: """执行外部 API 调用。""" cfg = json.loads(config or "{}") url = cfg.get("url", "").strip() method = cfg.get("method", "GET").upper() headers = cfg.get("headers") or {} payload = cfg.get("payload") timeout_val = float(cfg.get("timeout", 30)) if not url: raise ValueError("外部接口地址 (url) 未配置") start = time.monotonic() try: with httpx.Client(timeout=httpx.Timeout(timeout_val, connect=10.0)) as client: if method == "GET": resp = client.get(url, headers=headers, params=payload) elif method == "DELETE": resp = client.delete(url, headers=headers, params=payload) else: resp = client.request(method, url, headers=headers, json=payload) duration_ms = int((time.monotonic() - start) * 1000) resp.raise_for_status() return { "status_code": resp.status_code, "duration_ms": duration_ms, "body": resp.text[:2000], } except httpx.HTTPError as e: duration_ms = int((time.monotonic() - start) * 1000) raise RuntimeError(f"外部接口请求失败: {e} (耗时 {duration_ms}ms)") from e def _execute_internal_method(config: str | None) -> dict: """执行内部方法调用。""" cfg = json.loads(config or "{}") module_path = cfg.get("module", "").strip() function_name = cfg.get("function", "").strip() args = cfg.get("args") or [] if not module_path or not function_name: raise ValueError("内部方法需要指定 module 和 function") import importlib module = importlib.import_module(module_path) func = getattr(module, function_name, None) if func is None or not callable(func): raise ValueError(f"模块 {module_path} 中不存在可调用函数 {function_name}") start = time.monotonic() result = func(*args) duration_ms = int((time.monotonic() - start) * 1000) return { "duration_ms": duration_ms, "result": str(result)[:1000] if result is not None else None, } @celery_app.task(name="execute_scheduled_task", bind=True, ignore_result=True) # type: ignore[call-arg] def execute_scheduled_task(self, task_id: str): """执行定时任务(Celery 任务入口)。 通过 run_async 桥接到异步上下文读取任务配置并执行。 """ async def _run(): async with async_session() as db: result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id)) task = result.scalar_one_or_none() if task is None: logger.warning("定时任务不存在: %s", task_id) return if not task.is_active: logger.info("定时任务已禁用,跳过执行: %s", task_id) return task_config = task.config task_type = task.task_type try: if task_type == "external_api": exec_result = _execute_external_api(task_config) elif task_type == "internal_method": exec_result = _execute_internal_method(task_config) else: raise ValueError(f"未知的任务类型: {task_type}") _update_task_status(task_id, "success") logger.info("定时任务执行成功: %s (%s) -> %s", task_id, task_type, exec_result) except Exception as e: error_msg = str(e) _update_task_status(task_id, "error", error_msg) logger.exception("定时任务执行失败: %s (%s)", task_id, task_type) run_async(_run())