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

@@ -2,33 +2,34 @@
use sqlx::PgPool;
use crate::error::{SaasError, SaasResult};
use crate::models::{RoleRow, PermissionTemplateRow};
use super::types::*;
pub async fn list_roles(db: &PgPool) -> SaasResult<Vec<RoleInfo>> {
let rows: Vec<(String, String, Option<String>, String, bool, String, String)> =
let rows: Vec<RoleRow> =
sqlx::query_as(
"SELECT id, name, description, permissions, is_system, created_at, updated_at
FROM roles ORDER BY
CASE id
WHEN 'super_admin' THEN 1
WHEN 'admin' THEN 2
WHEN 'user' THEN 3
ELSE 4
FROM roles ORDER BY
CASE id
WHEN 'super_admin' THEN 1
WHEN 'admin' THEN 2
WHEN 'user' THEN 3
ELSE 4
END"
)
.fetch_all(db)
.await?;
let roles = rows.into_iter().map(|(id, name, description, perms, is_system, created_at, updated_at)| {
let permissions: Vec<String> = serde_json::from_str(&perms).unwrap_or_default();
RoleInfo { id, name, description, permissions, is_system, created_at, updated_at }
let roles = rows.into_iter().map(|r| {
let permissions: Vec<String> = serde_json::from_str(&r.permissions).unwrap_or_default();
RoleInfo { id: r.id, name: r.name, description: r.description, permissions, is_system: r.is_system, created_at: r.created_at, updated_at: r.updated_at }
}).collect();
Ok(roles)
}
pub async fn get_role(db: &PgPool, role_id: &str) -> SaasResult<RoleInfo> {
let row: Option<(String, String, Option<String>, String, bool, String, String)> =
let row: Option<RoleRow> =
sqlx::query_as(
"SELECT id, name, description, permissions, is_system, created_at, updated_at
FROM roles WHERE id = $1"
@@ -37,11 +38,10 @@ pub async fn get_role(db: &PgPool, role_id: &str) -> SaasResult<RoleInfo> {
.fetch_optional(db)
.await?;
let (id, name, description, perms, is_system, created_at, updated_at) =
row.ok_or_else(|| SaasError::NotFound(format!("角色 {} 不存在", role_id)))?;
let r = row.ok_or_else(|| SaasError::NotFound(format!("角色 {} 不存在", role_id)))?;
let permissions: Vec<String> = serde_json::from_str(&perms).unwrap_or_default();
Ok(RoleInfo { id, name, description, permissions, is_system, created_at, updated_at })
let permissions: Vec<String> = serde_json::from_str(&r.permissions).unwrap_or_default();
Ok(RoleInfo { id: r.id, name: r.name, description: r.description, permissions, is_system: r.is_system, created_at: r.created_at, updated_at: r.updated_at })
}
pub async fn create_role(db: &PgPool, req: &CreateRoleRequest) -> SaasResult<RoleInfo> {
@@ -137,7 +137,7 @@ pub async fn delete_role(db: &PgPool, role_id: &str) -> SaasResult<()> {
}
pub async fn list_templates(db: &PgPool) -> SaasResult<Vec<PermissionTemplate>> {
let rows: Vec<(String, String, Option<String>, String, String, String)> =
let rows: Vec<PermissionTemplateRow> =
sqlx::query_as(
"SELECT id, name, description, permissions, created_at, updated_at
FROM permission_templates ORDER BY created_at DESC"
@@ -145,16 +145,16 @@ pub async fn list_templates(db: &PgPool) -> SaasResult<Vec<PermissionTemplate>>
.fetch_all(db)
.await?;
let templates = rows.into_iter().map(|(id, name, description, perms, created_at, updated_at)| {
let permissions: Vec<String> = serde_json::from_str(&perms).unwrap_or_default();
PermissionTemplate { id, name, description, permissions, created_at, updated_at }
let templates = rows.into_iter().map(|r| {
let permissions: Vec<String> = serde_json::from_str(&r.permissions).unwrap_or_default();
PermissionTemplate { id: r.id, name: r.name, description: r.description, permissions, created_at: r.created_at, updated_at: r.updated_at }
}).collect();
Ok(templates)
}
pub async fn get_template(db: &PgPool, template_id: &str) -> SaasResult<PermissionTemplate> {
let row: Option<(String, String, Option<String>, String, String, String)> =
let row: Option<PermissionTemplateRow> =
sqlx::query_as(
"SELECT id, name, description, permissions, created_at, updated_at
FROM permission_templates WHERE id = $1"
@@ -163,11 +163,10 @@ pub async fn get_template(db: &PgPool, template_id: &str) -> SaasResult<Permissi
.fetch_optional(db)
.await?;
let (id, name, description, perms, created_at, updated_at) =
row.ok_or_else(|| SaasError::NotFound(format!("权限模板 {} 不存在", template_id)))?;
let r = row.ok_or_else(|| SaasError::NotFound(format!("权限模板 {} 不存在", template_id)))?;
let permissions: Vec<String> = serde_json::from_str(&perms).unwrap_or_default();
Ok(PermissionTemplate { id, name, description, permissions, created_at, updated_at })
let permissions: Vec<String> = serde_json::from_str(&r.permissions).unwrap_or_default();
Ok(PermissionTemplate { id: r.id, name: r.name, description: r.description, permissions, created_at: r.created_at, updated_at: r.updated_at })
}
pub async fn create_template(db: &PgPool, req: &CreateTemplateRequest) -> SaasResult<PermissionTemplate> {