Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Batch 7: dead_code 标注统一 (16 处) - crates/ 9 处: growth, kernel, pipeline, runtime, saas, skills - src-tauri/ 7 处: classroom, intelligence, browser, mcp - 统一格式: #[allow(dead_code)] // @reserved: <原因> Batch 7+: EvolutionEngine L2/L3 10 个未使用 pub 函数 - 全部标注 @reserved: EvolutionEngine L2/L3, post-release integration Batch 9: TODO → FUTURE 标记 (4 处) - html.rs: template-based export - nl_schedule.rs: LLM-assisted parsing - knowledge/handlers.rs: category_id from upload - personality_detector.rs: VikingStorage persistence Batch 5+: Cargo.lock 更新 (serde_yaml_bw 迁移) 全量测试通过: 719 passed, 0 failed
203 lines
6.1 KiB
Rust
203 lines
6.1 KiB
Rust
//! 定时任务数据库服务层
|
|
|
|
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<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_result: Option<String>,
|
|
last_error: Option<String>,
|
|
last_duration_ms: Option<i64>,
|
|
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_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<ScheduledTaskResponse> {
|
|
let id = uuid::Uuid::new_v4().to_string();
|
|
let now = chrono::Utc::now();
|
|
let input_json: Option<String> = 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<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_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<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_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<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();
|
|
|
|
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(())
|
|
}
|