refactor(saas): 架构重构 + 性能优化 — 借鉴 loco-rs 模式
Phase 0: 知识库
- docs/knowledge-base/loco-rs-patterns.md — loco-rs 10 个可借鉴模式研究
Phase 1: 数据层重构
- crates/zclaw-saas/src/models/ — 15 个 FromRow 类型化模型
- Login 3 次查询合并为 1 次 AccountLoginRow 查询
- 所有 service 文件从元组解构迁移到 FromRow 结构体
Phase 2: Worker + Scheduler 系统
- crates/zclaw-saas/src/workers/ — Worker trait + 5 个具体实现
- crates/zclaw-saas/src/scheduler.rs — TOML 声明式调度器
- crates/zclaw-saas/src/tasks/ — CLI 任务系统
Phase 3: 性能修复
- Relay N+1 查询 → 精准 SQL (relay/handlers.rs)
- Config RwLock → AtomicU32 无锁 rate limit (state.rs, middleware.rs)
- SSE std::sync::Mutex → tokio::sync::Mutex (relay/service.rs)
- /auth/refresh 阻塞清理 → Scheduler 定期执行
Phase 4: 多环境配置
- config/saas-{development,production,test}.toml
- ZCLAW_ENV 环境选择 + ZCLAW_SAAS_CONFIG 精确覆盖
- scheduler 配置集成到 TOML
This commit is contained in:
101
crates/zclaw-saas/src/scheduler.rs
Normal file
101
crates/zclaw-saas/src/scheduler.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! 声明式 Scheduler — 借鉴 loco-rs 的定时任务模式
|
||||
//!
|
||||
//! 通过 TOML 配置定时任务,无需改代码调整调度时间。
|
||||
//! 配置格式在 config.rs 的 SchedulerConfig / JobConfig 中定义。
|
||||
|
||||
use std::time::Duration;
|
||||
use sqlx::PgPool;
|
||||
use crate::config::SchedulerConfig;
|
||||
use crate::workers::WorkerDispatcher;
|
||||
|
||||
/// 解析时间间隔字符串为 Duration
|
||||
pub fn parse_duration(s: &str) -> Result<Duration, String> {
|
||||
let s = s.trim().to_lowercase();
|
||||
let (num_part, multiplier) = if s.ends_with('s') {
|
||||
(&s[..s.len()-1], 1u64)
|
||||
} else if s.ends_with('m') {
|
||||
(&s[..s.len()-1], 60u64)
|
||||
} else if s.ends_with('h') {
|
||||
(&s[..s.len()-1], 3600u64)
|
||||
} else if s.ends_with('d') {
|
||||
(&s[..s.len()-1], 86400u64)
|
||||
} else {
|
||||
return Err(format!("Invalid interval format: '{}'. Use '30s', '5m', '1h', '1d'", s));
|
||||
};
|
||||
|
||||
let num: u64 = num_part.parse()
|
||||
.map_err(|_| format!("Invalid number in interval: '{}'", num_part))?;
|
||||
|
||||
Ok(Duration::from_secs(num * multiplier))
|
||||
}
|
||||
|
||||
/// 启动所有定时任务
|
||||
pub fn start_scheduler(config: &SchedulerConfig, db: PgPool, dispatcher: WorkerDispatcher) {
|
||||
for job in &config.jobs {
|
||||
let interval = match parse_duration(&job.interval) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
tracing::error!("Scheduler job '{}': {}", job.name, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let job_name = job.name.clone();
|
||||
let task_name = job.task.clone();
|
||||
let args_json = job.args.clone();
|
||||
let _db = db.clone();
|
||||
let dispatcher = dispatcher.clone_ref();
|
||||
let run_on_start = job.run_on_start;
|
||||
|
||||
tracing::info!(
|
||||
"Scheduler: registering job '{}' ({} interval, task={})",
|
||||
job_name, job.interval, task_name
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
if run_on_start {
|
||||
tracing::info!("Scheduler: running '{}' on start", job_name);
|
||||
if let Err(e) = dispatcher.dispatch_raw(&task_name, args_json.clone()).await {
|
||||
tracing::error!("Scheduler job '{}' on-start failed: {}", job_name, e);
|
||||
}
|
||||
}
|
||||
|
||||
let mut interval_timer = tokio::time::interval(interval);
|
||||
loop {
|
||||
interval_timer.tick().await;
|
||||
tracing::debug!("Scheduler: triggering job '{}'", job_name);
|
||||
if let Err(e) = dispatcher.dispatch_raw(&task_name, args_json.clone()).await {
|
||||
tracing::error!("Scheduler job '{}' failed: {}", job_name, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 内置的 DB 清理任务(不通过 Worker,直接执行 SQL)
|
||||
pub fn start_db_cleanup_tasks(db: PgPool) {
|
||||
// 每 24 小时清理不活跃设备
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(86400));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match sqlx::query(
|
||||
"DELETE FROM devices WHERE last_seen_at < $1"
|
||||
)
|
||||
.bind({
|
||||
let cutoff = (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339();
|
||||
cutoff
|
||||
})
|
||||
.execute(&db)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if result.rows_affected() > 0 {
|
||||
tracing::info!("Cleaned up {} inactive devices (90d)", result.rows_affected());
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::error!("Device cleanup failed: {}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user