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:
@@ -1,8 +1,15 @@
|
||||
//! ZCLAW SaaS 服务入口
|
||||
|
||||
use axum::extract::State;
|
||||
use tower_http::timeout::TimeoutLayer;
|
||||
use tracing::info;
|
||||
use zclaw_saas::{config::SaaSConfig, db::init_db, state::AppState};
|
||||
use zclaw_saas::workers::WorkerDispatcher;
|
||||
use zclaw_saas::workers::log_operation::LogOperationWorker;
|
||||
use zclaw_saas::workers::cleanup_refresh_tokens::CleanupRefreshTokensWorker;
|
||||
use zclaw_saas::workers::cleanup_rate_limit::CleanupRateLimitWorker;
|
||||
use zclaw_saas::workers::record_usage::RecordUsageWorker;
|
||||
use zclaw_saas::workers::update_last_used::UpdateLastUsedWorker;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
@@ -19,10 +26,34 @@ async fn main() -> anyhow::Result<()> {
|
||||
let db = init_db(&config.database.url).await?;
|
||||
info!("Database initialized");
|
||||
|
||||
let state = AppState::new(db, config.clone())?;
|
||||
// 初始化 Worker 调度器 + 注册所有 Worker
|
||||
let mut dispatcher = WorkerDispatcher::new(db.clone());
|
||||
dispatcher.register(LogOperationWorker);
|
||||
dispatcher.register(CleanupRefreshTokensWorker);
|
||||
dispatcher.register(CleanupRateLimitWorker);
|
||||
dispatcher.register(RecordUsageWorker);
|
||||
dispatcher.register(UpdateLastUsedWorker);
|
||||
info!("Worker dispatcher initialized (5 workers registered)");
|
||||
|
||||
// 后台定时任务
|
||||
spawn_background_tasks(state.clone());
|
||||
let state = AppState::new(db.clone(), config.clone(), dispatcher)?;
|
||||
|
||||
// 启动声明式 Scheduler(从 TOML 配置读取定时任务)
|
||||
let scheduler_config = &config.scheduler;
|
||||
zclaw_saas::scheduler::start_scheduler(scheduler_config, db.clone(), state.worker_dispatcher.clone_ref());
|
||||
info!("Scheduler started with {} jobs", scheduler_config.jobs.len());
|
||||
|
||||
// 启动内置 DB 清理任务(设备清理等不通过 Worker 的任务)
|
||||
zclaw_saas::scheduler::start_db_cleanup_tasks(db.clone());
|
||||
|
||||
// 启动内存中的 rate limit 条目清理
|
||||
let rate_limit_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
rate_limit_state.cleanup_rate_limit_entries();
|
||||
}
|
||||
});
|
||||
|
||||
let app = build_router(state).await;
|
||||
|
||||
@@ -51,43 +82,6 @@ async fn health_handler(State(state): State<AppState>) -> axum::Json<serde_json:
|
||||
}))
|
||||
}
|
||||
|
||||
/// 启动后台定时任务
|
||||
fn spawn_background_tasks(state: AppState) {
|
||||
// 每 5 分钟清理过期的限流条目
|
||||
let rate_limit_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
rate_limit_state.cleanup_rate_limit_entries();
|
||||
}
|
||||
});
|
||||
|
||||
// 每 24 小时清理 90 天未活跃的设备
|
||||
// 注意: last_seen_at 为 TEXT 类型,使用 rfc3339 字符串比较(字典序等价于时间序)
|
||||
let cleanup_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(86400));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let cutoff = (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339();
|
||||
match sqlx::query("DELETE FROM devices WHERE last_seen_at < $1")
|
||||
.bind(&cutoff)
|
||||
.execute(&cleanup_state.db)
|
||||
.await
|
||||
{
|
||||
Ok(result) if result.rows_affected() > 0 => {
|
||||
info!("Cleaned up {} stale devices", result.rows_affected());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to cleanup stale devices: {}", e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn build_router(state: AppState) -> axum::Router {
|
||||
use axum::middleware;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
@@ -163,6 +157,7 @@ async fn build_router(state: AppState) -> axum::Router {
|
||||
axum::Router::new()
|
||||
.merge(public_routes)
|
||||
.merge(protected_routes)
|
||||
.layer(TimeoutLayer::new(std::time::Duration::from_secs(30)))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(cors)
|
||||
.with_state(state)
|
||||
|
||||
Reference in New Issue
Block a user