Files
zclaw_openfang/crates/zclaw-saas/src/scheduled_task/service.rs
iven d9b0b4f4f7
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
fix(audit): Batch 7-9 dead_code 标注 + TODO 清理 + 文档同步
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
2026-04-19 08:54:57 +08:00

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(())
}