107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""定时任务执行器。
|
|
|
|
仅支持内部方法执行类型,供 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")
|
|
|
|
|
|
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_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)
|
|
_update_task_status(task_id, "success")
|
|
logger.info("定时任务执行成功: %s -> %s", task_id, exec_result)
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
_update_task_status(task_id, "error", error_msg)
|
|
logger.exception("定时任务执行失败: %s", task_id)
|
|
|
|
run_async(_run())
|