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:
iven
2026-03-29 19:21:48 +08:00
parent 5fdf96c3f5
commit 8b9d506893
64 changed files with 3348 additions and 520 deletions

View File

@@ -0,0 +1,88 @@
//! CLI Task 系统 — 借鉴 loco-rs 的 Task trait 模式
//!
//! 提供可手动执行的运维命令:
//! - seed_admin — 创建管理员账号
//! - cleanup_devices — 清理不活跃设备
//! - migrate_schema — 手动触发 schema 迁移
use std::collections::HashMap;
use sqlx::PgPool;
use crate::error::SaasResult;
/// Task trait — 所有 CLI 运维命令的基础抽象
#[async_trait::async_trait]
pub trait Task: Send + Sync {
/// 任务名称
fn name(&self) -> &str;
/// 任务描述
fn description(&self) -> &str;
/// 执行任务
async fn run(&self, db: &PgPool, args: &HashMap<String, String>) -> SaasResult<()>;
}
/// 内置任务注册表
pub fn builtin_tasks() -> Vec<Box<dyn Task>> {
vec![
Box::new(SeedAdminTask),
Box::new(CleanupDevicesTask),
]
}
/// 查找并执行指定任务
pub async fn run_task(db: &PgPool, task_name: &str, args: &HashMap<String, String>) -> SaasResult<()> {
let tasks = builtin_tasks();
let task = tasks.into_iter()
.find(|t| t.name() == task_name)
.ok_or_else(|| crate::error::SaasError::NotFound(format!("Task '{}' not found", task_name)))?;
tracing::info!("Running task: {} — {}", task.name(), task.description());
task.run(db, args).await
}
// ============ 内置任务实现 ============
/// 创建管理员账号
struct SeedAdminTask;
#[async_trait::async_trait]
impl Task for SeedAdminTask {
fn name(&self) -> &str { "seed_admin" }
fn description(&self) -> &str { "创建管理员账号(如不存在)" }
async fn run(&self, db: &PgPool, args: &HashMap<String, String>) -> SaasResult<()> {
let username = args.get("username").map(|s| s.as_str()).unwrap_or("admin");
let password = args.get("password")
.ok_or_else(|| crate::error::SaasError::InvalidInput("Missing 'password' argument".into()))?;
// 临时设置环境变量让 db::seed_admin_account 使用
std::env::set_var("ZCLAW_ADMIN_USERNAME", username);
std::env::set_var("ZCLAW_ADMIN_PASSWORD", password);
crate::db::seed_admin_account(db).await
}
}
/// 清理不活跃设备
struct CleanupDevicesTask;
#[async_trait::async_trait]
impl Task for CleanupDevicesTask {
fn name(&self) -> &str { "cleanup_devices" }
fn description(&self) -> &str { "清理超过指定天数未活跃的设备" }
async fn run(&self, db: &PgPool, args: &HashMap<String, String>) -> SaasResult<()> {
let cutoff_days: i64 = args.get("cutoff_days")
.and_then(|v| v.parse().ok())
.unwrap_or(90);
let cutoff = (chrono::Utc::now() - chrono::Duration::days(cutoff_days)).to_rfc3339();
let result = sqlx::query("DELETE FROM devices WHERE last_seen_at < $1")
.bind(&cutoff)
.execute(db)
.await?;
tracing::info!("Cleaned up {} inactive devices (>={} days)", result.rows_affected(), cutoff_days);
Ok(())
}
}