//! 定时任务数据库服务层 use sqlx::{PgPool, FromRow}; use crate::error::SaasResult; use super::types::*; /// 数据库行结构 #[derive(Debug, FromRow)] #[allow(dead_code)] // @reserved: FromRow deserialization struct; fields accessed via destructuring struct ScheduledTaskRow { id: String, account_id: String, name: String, description: Option, schedule: String, schedule_type: String, target_type: String, target_id: String, enabled: bool, last_run_at: Option, next_run_at: Option, run_count: i32, last_result: Option, last_error: Option, last_duration_ms: Option, input_payload: Option, 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_result: self.last_result.clone(), last_error: self.last_error.clone(), last_duration_ms: self.last_duration_ms, created_at: self.created_at.clone(), } } } /// 创建定时任务 pub async fn create_task( db: &PgPool, account_id: &str, req: &CreateScheduledTaskRequest, ) -> SaasResult { let id = uuid::Uuid::new_v4().to_string(); let now = chrono::Utc::now(); let input_json: Option = 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::jsonb, $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_result: None, last_error: None, last_duration_ms: None, created_at: now.to_rfc3339(), }) } /// 列出用户的定时任务 pub async fn list_tasks( db: &PgPool, account_id: &str, ) -> SaasResult> { let rows: Vec = 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_result, last_error, last_duration_ms, input_payload, created_at::TEXT 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 { let row: Option = 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_result, last_error, last_duration_ms, input_payload, created_at::TEXT 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 { 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(); 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(()) }