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,124 @@
//! 模块定时任务服务Service Layer
//! 职责:
//! 1) 负责定时任务schedule_jobs的数据库增删改查
//! 2) 在创建/更新/删除后与调度器同步;
//! 3) 服务启动时加载已启用任务并注册。
use std::{future::Future, pin::Pin, sync::Arc};
use chrono::{DateTime, FixedOffset, Utc};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
use tokio_cron_scheduler::Job;
use tracing::{error, info};
use crate::{db::Db, error::AppError, models::schedule_job, utils};
/// 通用分页响应体
#[derive(serde::Serialize)]
pub struct PageResp<T> {
pub items: Vec<T>,
pub total: u64,
pub page: u64,
pub page_size: u64,
}
/// 任务文档(对外返回 DTO
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
pub struct ScheduleJobDoc {
pub id: i64,
pub name: String,
pub cron_expr: String,
pub enabled: bool,
pub flow_code: String,
pub created_at: DateTime<FixedOffset>,
pub updated_at: DateTime<FixedOffset>,
}
impl From<schedule_job::Model> for ScheduleJobDoc {
fn from(m: schedule_job::Model) -> Self {
Self {
id: m.id,
name: m.name,
cron_expr: m.cron_expr,
enabled: m.enabled,
flow_code: m.flow_code,
created_at: m.created_at,
updated_at: m.updated_at,
}
}
}
/// 创建任务请求体
#[derive(serde::Deserialize)]
pub struct CreateReq {
pub name: String,
pub cron_expr: String,
pub enabled: bool,
pub flow_code: String,
}
/// 获取当前 UTC 时间并转为固定偏移
fn now_fixed_offset() -> DateTime<FixedOffset> {
Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap())
}
/// 创建任务
pub async fn create(db: &Db, req: CreateReq) -> Result<ScheduleJobDoc, AppError> {
// 1) 校验 cron 表达式
Job::new_async(&req.cron_expr, |_id, _l| Box::pin(async {}))
.map_err(|e| AppError::BadRequest(format!("无效的 cron 表达式: {e}")))?;
// 2) 入库
let am = schedule_job::ActiveModel {
id: Set(crate::utils::generate_id()),
name: Set(req.name),
cron_expr: Set(req.cron_expr),
enabled: Set(req.enabled),
flow_code: Set(req.flow_code),
created_at: Set(now_fixed_offset()),
updated_at: Set(now_fixed_offset()),
};
let m = am.insert(db).await?;
// 3) 同步调度器
let executor = build_executor_for_job(db, &m);
utils::add_or_update_job_by_model(&m, executor).await.map_err(AppError::Anyhow)?;
Ok(m.into())
}
/// 构建任务执行闭包JobExecutor
fn build_executor_for_job(db: &Db, m: &schedule_job::Model) -> utils::JobExecutor {
let db = db.clone();
let job_id = m.id;
let job_name = m.name.clone();
Arc::new(move || {
let db = db.clone();
let job_id = job_id;
let job_name = job_name.clone();
Box::pin(async move {
match schedule_job::Entity::find_by_id(job_id).one(&db).await {
Ok(Some(model)) if !model.enabled => {
info!(target = "udmin", job = %job_name, id = %job_id, "scheduler.tick.skip");
return;
}
Ok(None) => {
info!(target = "udmin", job = %job_name, id = %job_id, "scheduler.tick.deleted");
if let Err(e) = utils::remove_job_by_id(&job_id).await {
error!(target = "udmin", id = %job_id, error = %e, "scheduler.self_remove.failed");
}
return;
}
Err(e) => {
error!(target = "udmin", job = %job_name, id = %job_id, error = %e, "scheduler.tick.error");
return;
}
_ => {}
}
info!(target = "udmin", job = %job_name, "scheduler.tick.start");
}) as Pin<Box<dyn Future<Output = ()> + Send>>
})
}

View File

@ -0,0 +1,152 @@
# Rust 代码风格规范(用于 AI 生成代码规则)
## 1. 基础风格
* 使用 **Rust 2021 edition**
* 缩进统一 **4 空格**,不使用 Tab。
* 每行代码长度建议不超过 **100 字符**
* **花括号风格**
```rust
fn example() {
// good
}
```
* 表达式尽量简洁,必要时换行,参数链式调用时 **缩进对齐**。
---
## 2. 模块与导入
* 模块顶部导入,按以下顺序分组,组间空一行:
1. **标准库 (`std::..`)**
2. **第三方库 (`chrono`, `sea_orm`, `tokio` 等)**
3. **本地 crate (`crate::..`)**
* 统一使用 **显式导入**,禁止 `use super::*` 或 `use crate::*`。
* 相同模块导入合并:
```rust
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait};
```
---
## 3. 命名规则
* **模块 / 文件名**`snake_case`
* **函数 / 变量名**`snake_case`
* **结构体 / 枚举名**`PascalCase`
* **常量**`UPPER_CASE`
* **DTO/请求体/响应体**后缀:`Doc` / `Req` / `Resp`
示例:
```rust
pub struct ScheduleJobDoc { .. }
pub struct CreateReq { .. }
pub struct PageResp<T> { .. }
```
---
## 4. 文档与注释
* 每个 **模块** 顶部使用 `//!` 写模块职责说明。
* 每个 **公开函数** 必须有 `///` 注释,简述用途与主要逻辑。
* 内部复杂逻辑使用 `//` 单行注释解释。
* 中文注释优先,避免英文缩写晦涩难懂。
示例:
```rust
/// 创建任务:
/// - 校验 cron 表达式
/// - 校验唯一性
/// - 入库后注册调度器
pub async fn create(..) -> Result<..> { .. }
```
---
## 5. 错误处理
* 错误类型统一用 **自定义错误枚举**(如 `AppError`)。
* 不直接 `unwrap()` / `expect()`,统一返回 `Result<T, AppError>`。
* 错误信息应清晰且面向用户,内部日志保留技术细节。
---
## 6. 日志规范
* 使用 `tracing` 库,必须带 `target`。
* 统一格式:`模块.操作.状态`
* 日志字段使用 `key = %value` 或 `key = ?value`,避免拼接字符串。
示例:
```rust
info!(target = "udmin", id = %job_id, enabled = %enabled, "schedule_jobs.update.persisted");
error!(target = "udmin", id = %job_id, error = %e, "schedule_jobs.run.failed");
```
---
## 7. 异步与数据库
* 使用 `async fn`,返回 `Result<T, AppError>`。
* SeaORM 查询使用链式写法,**按字段过滤**时一行一个 filter。
* 分页/排序明确写出,不隐式。
示例:
```rust
let jobs = schedule_job::Entity::find()
.filter(schedule_job::Column::Enabled.eq(true))
.order_by_desc(schedule_job::Column::UpdatedAt)
.paginate(db, page_size);
```
---
## 8. 结构体组织
* DTO / 请求体 / 响应体 放在模块前部。
* Service 函数按生命周期顺序:`list → create → update → remove → init`。
* 工具函数(如 `build_executor_for_job`、`now_fixed_offset`)放在模块最后。
---
## 9. 闭包与异步执行器
* 使用 `Arc::new(move || { Box::pin(async move { .. }) })` 形式。
* 避免重复 clone大对象提前 clone 一次再 move 进闭包。
* 返回 `Pin<Box<dyn Future<Output = ()> + Send>>`。
---
## 10. 时间与 ID
* 时间统一用 `chrono::Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap())`,可封装成 `now_fixed_offset()`。
* ID 统一使用 `id: Set(crate::utils::generate_id())`。
---
## 11. 统一返回体
* 分页接口统一返回 `PageResp<T>`。
* 单条数据返回 DTO如 `ScheduleJobDoc`)。
* 删除接口返回 `Result<(), AppError>`。
---
## 12. 代码整洁性
* 避免嵌套过深,必要时提前 `return`。
* 冗余 clone 使用 `.clone()` 仅在必须时。
* 枚举 / match 分支完整,必要时加 `_ => {}` 显式忽略。
---
⚡ 总结:
生成的代码必须 **简洁、清晰、分组有序、日志一致、错误优雅**,看起来像经验丰富的 Rust 高手写的生产级代码。