"""定时任务执行器。 仅支持内部方法执行类型,供 Celery Beat 定时调用。 配置 JSON 格式(示例): { "module": "app.services.bank.sync_service", "function": "sync_bank_transactions", "url": "http://...", "api_key": "...", "acct_no": "...", "start_date": "2026-08-01", "end_date": "2026-08-14" } 其中 module 和 function 指定要调用的函数,其余字段作为 kwargs 传给该函数。 """ import json import logging import time from datetime import datetime, timezone 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") async def _update_task_status(task_id: str, status: str, error_msg: str | None = None) -> None: """更新任务最后执行状态。""" 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() def _execute_internal_method(config: str | None) -> dict: """执行内部方法调用。 配置 JSON 中 module 和 function 指定要调用的函数,其余字段作为 kwargs 传入。 """ cfg = json.loads(config or "{}") module_path = cfg.pop("module", "").strip() function_name = cfg.pop("function", "").strip() 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}") # 剩余字段作为 kwargs 传给函数 start = time.monotonic() result = func(**cfg) 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 任务入口)。""" 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 try: exec_result = _execute_internal_method(task_config) await _update_task_status(task_id, "success") logger.info("定时任务执行成功: %s -> %s", task_id, exec_result) except Exception as e: error_msg = str(e) await _update_task_status(task_id, "error", error_msg) logger.exception("定时任务执行失败: %s", task_id) run_async(_run())