feat(scheduler): 定时任务后端持久化 + Pipeline trigger 编译修复
S4/S8 定时任务后端: - 新增 scheduled_tasks 表 (migration v7) - 新增 scheduled_task CRUD 模块 (handlers/service/types) - 注册 /api/scheduler/tasks 路由 (GET/POST/PATCH/DELETE) - 新增 start_user_task_scheduler() 30秒轮询循环 - 支持 cron/interval/once 三种调度类型 - once 类型执行后自动禁用 修复: - pipeline_commands.rs: 修复 pipeline.trigger 字段不存在的编译错误 (Pipeline 结构体无 trigger 字段,改用 metadata.tags/description)
This commit is contained in:
195
crates/zclaw-saas/src/scheduled_task/service.rs
Normal file
195
crates/zclaw-saas/src/scheduled_task/service.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
//! 定时任务数据库服务层
|
||||
|
||||
use sqlx::{PgPool, FromRow};
|
||||
use crate::error::SaasResult;
|
||||
use super::types::*;
|
||||
|
||||
/// 数据库行结构
|
||||
#[derive(Debug, FromRow)]
|
||||
struct ScheduledTaskRow {
|
||||
id: String,
|
||||
account_id: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
schedule: String,
|
||||
schedule_type: String,
|
||||
target_type: String,
|
||||
target_id: String,
|
||||
enabled: bool,
|
||||
last_run_at: Option<String>,
|
||||
next_run_at: Option<String>,
|
||||
run_count: i32,
|
||||
last_error: Option<String>,
|
||||
input_payload: Option<serde_json::Value>,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
impl ScheduledTaskRow {
|
||||
fn to_response(&self) -> ScheduledTaskResponse {
|
||||
ScheduledTaskResponse {
|
||||
id: self.id.clone(),
|
||||
name: self.name.clone(),
|
||||
schedule: self.schedule.clone(),
|
||||
schedule_type: self.schedule_type.clone(),
|
||||
target: TaskTarget {
|
||||
target_type: self.target_type.clone(),
|
||||
id: self.target_id.clone(),
|
||||
},
|
||||
enabled: self.enabled,
|
||||
description: self.description.clone(),
|
||||
last_run: self.last_run_at.clone(),
|
||||
next_run: self.next_run_at.clone(),
|
||||
run_count: self.run_count,
|
||||
last_error: self.last_error.clone(),
|
||||
created_at: self.created_at.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建定时任务
|
||||
pub async fn create_task(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
req: &CreateScheduledTaskRequest,
|
||||
) -> SaasResult<ScheduledTaskResponse> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let input_json = req.input.as_ref().map(|v| v.to_string());
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO scheduled_tasks (id, account_id, name, description, schedule, schedule_type, target_type, target_id, enabled, input_payload, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11)"
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(account_id)
|
||||
.bind(&req.name)
|
||||
.bind(&req.description)
|
||||
.bind(&req.schedule)
|
||||
.bind(&req.schedule_type)
|
||||
.bind(&req.target.target_type)
|
||||
.bind(&req.target.id)
|
||||
.bind(req.enabled.unwrap_or(true))
|
||||
.bind(&input_json)
|
||||
.bind(&now)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
Ok(ScheduledTaskResponse {
|
||||
id,
|
||||
name: req.name.clone(),
|
||||
schedule: req.schedule.clone(),
|
||||
schedule_type: req.schedule_type.clone(),
|
||||
target: req.target.clone(),
|
||||
enabled: req.enabled.unwrap_or(true),
|
||||
description: req.description.clone(),
|
||||
last_run: None,
|
||||
next_run: None,
|
||||
run_count: 0,
|
||||
last_error: None,
|
||||
created_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
/// 列出用户的定时任务
|
||||
pub async fn list_tasks(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
) -> SaasResult<Vec<ScheduledTaskResponse>> {
|
||||
let rows: Vec<ScheduledTaskRow> = sqlx::query_as(
|
||||
"SELECT id, account_id, name, description, schedule, schedule_type,
|
||||
target_type, target_id, enabled, last_run_at, next_run_at,
|
||||
run_count, last_error, input_payload, created_at
|
||||
FROM scheduled_tasks WHERE account_id = $1 ORDER BY created_at DESC"
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
Ok(rows.iter().map(|r| r.to_response()).collect())
|
||||
}
|
||||
|
||||
/// 获取单个定时任务
|
||||
pub async fn get_task(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
task_id: &str,
|
||||
) -> SaasResult<ScheduledTaskResponse> {
|
||||
let row: Option<ScheduledTaskRow> = sqlx::query_as(
|
||||
"SELECT id, account_id, name, description, schedule, schedule_type,
|
||||
target_type, target_id, enabled, last_run_at, next_run_at,
|
||||
run_count, last_error, input_payload, created_at
|
||||
FROM scheduled_tasks WHERE id = $1 AND account_id = $2"
|
||||
)
|
||||
.bind(task_id)
|
||||
.bind(account_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
Ok(row
|
||||
.ok_or_else(|| crate::error::SaasError::NotFound("定时任务不存在".into()))?
|
||||
.to_response())
|
||||
}
|
||||
|
||||
/// 更新定时任务
|
||||
pub async fn update_task(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
task_id: &str,
|
||||
req: &UpdateScheduledTaskRequest,
|
||||
) -> SaasResult<ScheduledTaskResponse> {
|
||||
let existing = get_task(db, account_id, task_id).await?;
|
||||
|
||||
let name = req.name.as_deref().unwrap_or(&existing.name);
|
||||
let schedule = req.schedule.as_deref().unwrap_or(&existing.schedule);
|
||||
let schedule_type = req.schedule_type.as_deref().unwrap_or(&existing.schedule_type);
|
||||
let enabled = req.enabled.unwrap_or(existing.enabled);
|
||||
let description = req.description.as_deref().or(existing.description.as_deref());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let (target_type, target_id) = if let Some(ref target) = req.target {
|
||||
(target.target_type.as_str(), target.id.as_str())
|
||||
} else {
|
||||
(existing.target.target_type.as_str(), existing.target.id.as_str())
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE scheduled_tasks SET name = $1, schedule = $2, schedule_type = $3,
|
||||
target_type = $4, target_id = $5, enabled = $6, description = $7,
|
||||
updated_at = $8
|
||||
WHERE id = $9 AND account_id = $10"
|
||||
)
|
||||
.bind(name)
|
||||
.bind(schedule)
|
||||
.bind(schedule_type)
|
||||
.bind(target_type)
|
||||
.bind(target_id)
|
||||
.bind(enabled)
|
||||
.bind(description)
|
||||
.bind(&now)
|
||||
.bind(task_id)
|
||||
.bind(account_id)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
get_task(db, account_id, task_id).await
|
||||
}
|
||||
|
||||
/// 删除定时任务
|
||||
pub async fn delete_task(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
task_id: &str,
|
||||
) -> SaasResult<()> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM scheduled_tasks WHERE id = $1 AND account_id = $2"
|
||||
)
|
||||
.bind(task_id)
|
||||
.bind(account_id)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(crate::error::SaasError::NotFound("定时任务不存在".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user