feat(调度任务): 实现调度任务管理功能

新增调度任务模块,支持任务的增删改查、启停及手动执行
- 后端添加 schedule_job 模型、服务、路由及调度器工具类
- 前端新增调度任务管理页面
- 修改 flow 相关接口将 id 类型从 String 改为 i64
- 添加 tokio-cron-scheduler 依赖实现定时任务调度
- 初始化时加载已启用任务并注册到调度器
This commit is contained in:
2025-09-24 00:21:30 +08:00
parent cadd336dee
commit 8c06849254
29 changed files with 1253 additions and 103 deletions

View File

@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use once_cell::sync::OnceCell;
use tokio::sync::Mutex;
use tokio_cron_scheduler::{JobScheduler, Job};
use tracing::{error, info};
use uuid::Uuid;
use crate::models::schedule_job;
static SCHEDULER: OnceCell<Mutex<JobScheduler>> = OnceCell::new();
static JOB_GUIDS: OnceCell<Mutex<HashMap<String, Uuid>>> = OnceCell::new();
pub type JobExecutor = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
fn scheduler() -> &'static Mutex<JobScheduler> {
SCHEDULER
.get()
.expect("Scheduler not initialized. Call init_and_start() early in main.")
}
fn job_guids() -> &'static Mutex<HashMap<String, Uuid>> {
JOB_GUIDS
.get()
.expect("JOB_GUIDS not initialized. Call init_and_start() early in main.")
}
pub async fn init_and_start() -> anyhow::Result<()> {
if SCHEDULER.get().is_some() {
return Ok(());
}
let js = JobScheduler::new().await?;
SCHEDULER.set(Mutex::new(js)).ok().expect("set scheduler once");
JOB_GUIDS.set(Mutex::new(HashMap::new())).ok().expect("set job_guids once");
// 仅启动调度器,不进行任何数据库加载,数据库加载应由 service 层负责
let lock = scheduler().lock().await;
lock.start().await?;
drop(lock);
info!(target = "udmin", "scheduler started");
Ok(())
}
/// 新增或更新一个任务(根据 model.id 作为唯一标识)。
/// - 如果已存在,则先移除旧任务再按最新 cron 重新创建;
/// - 当 enabled=false 时,仅执行移除逻辑,不会重新添加。
pub async fn add_or_update_job_by_model(m: &schedule_job::Model, executor: JobExecutor) -> anyhow::Result<()> {
// 如果已有旧的 job先移除
if let Some(old) = { job_guids().lock().await.get(&m.id).cloned() } {
let js = scheduler().lock().await;
if let Err(e) = js.remove(&old).await {
error!(target = "udmin", id = %m.id, guid = %old, error = %e, "scheduler.remove old failed");
}
job_guids().lock().await.remove(&m.id);
}
if !m.enabled {
return Ok(());
}
// 构造异步 Job仅负责调度触发不做数据库操作
let cron_expr = m.cron_expr.clone();
let name = m.name.clone();
let job_id = m.id.clone();
let exec_arc = executor.clone();
let job = Job::new_async(cron_expr.as_str(), move |_uuid, _l| {
let job_name = name.clone();
let job_id = job_id.clone();
let exec = exec_arc.clone();
Box::pin(async move {
info!(target = "udmin", job = %job_name, id = %job_id, "scheduler.tick: start");
exec().await;
})
})?;
let guid = job.guid();
{
let js = scheduler().lock().await;
js.add(job).await?;
}
job_guids().lock().await.insert(m.id.clone(), guid);
info!(target = "udmin", id = %m.id, guid = %guid, "scheduler.add: ok");
Ok(())
}
pub async fn remove_job_by_id(id: &str) -> anyhow::Result<()> {
if let Some(g) = { job_guids().lock().await.get(id).cloned() } {
let js = scheduler().lock().await;
if let Err(e) = js.remove(&g).await {
error!(target = "udmin", id = %id, guid = %g, error = %e, "scheduler.remove: failed");
}
job_guids().lock().await.remove(id);
info!(target = "udmin", id = %id, guid = %g, "scheduler.remove: ok");
}
Ok(())
}