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
238 lines
7.4 KiB
Rust
238 lines
7.4 KiB
Rust
//! 角色管理业务逻辑
|
|
|
|
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<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
|
|
END"
|
|
)
|
|
.fetch_all(db)
|
|
.await?;
|
|
|
|
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<RoleRow> =
|
|
sqlx::query_as(
|
|
"SELECT id, name, description, permissions, is_system, created_at, updated_at
|
|
FROM roles WHERE id = $1"
|
|
)
|
|
.bind(role_id)
|
|
.fetch_optional(db)
|
|
.await?;
|
|
|
|
let r = row.ok_or_else(|| SaasError::NotFound(format!("角色 {} 不存在", role_id)))?;
|
|
|
|
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> {
|
|
let existing: Option<(String,)> = sqlx::query_as(
|
|
"SELECT id FROM roles WHERE id = $1"
|
|
)
|
|
.bind(&req.id)
|
|
.fetch_optional(db)
|
|
.await?;
|
|
|
|
if existing.is_some() {
|
|
return Err(SaasError::AlreadyExists(format!("角色 {} 已存在", req.id)));
|
|
}
|
|
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
let permissions = serde_json::to_string(&req.permissions)?;
|
|
|
|
sqlx::query(
|
|
"INSERT INTO roles (id, name, description, permissions, is_system, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, false, $5, $5)"
|
|
)
|
|
.bind(&req.id)
|
|
.bind(&req.name)
|
|
.bind(&req.description)
|
|
.bind(&permissions)
|
|
.bind(&now)
|
|
.execute(db)
|
|
.await?;
|
|
|
|
Ok(RoleInfo {
|
|
id: req.id.clone(),
|
|
name: req.name.clone(),
|
|
description: req.description.clone(),
|
|
permissions: req.permissions.clone(),
|
|
is_system: false,
|
|
created_at: now.clone(),
|
|
updated_at: now,
|
|
})
|
|
}
|
|
|
|
pub async fn update_role(db: &PgPool, role_id: &str, req: &UpdateRoleRequest) -> SaasResult<RoleInfo> {
|
|
let existing = get_role(db, role_id).await?;
|
|
|
|
if existing.is_system {
|
|
return Err(SaasError::Forbidden("系统角色不可修改".into()));
|
|
}
|
|
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
let name = req.name.as_ref().unwrap_or(&existing.name);
|
|
let description = req.description.as_ref().or(existing.description.as_ref());
|
|
let permissions = req.permissions.as_ref().unwrap_or(&existing.permissions);
|
|
let permissions_json = serde_json::to_string(permissions)?;
|
|
|
|
sqlx::query(
|
|
"UPDATE roles SET name = $1, description = $2, permissions = $3, updated_at = $4 WHERE id = $5"
|
|
)
|
|
.bind(name)
|
|
.bind(description)
|
|
.bind(&permissions_json)
|
|
.bind(&now)
|
|
.bind(role_id)
|
|
.execute(db)
|
|
.await?;
|
|
|
|
Ok(RoleInfo {
|
|
id: role_id.to_string(),
|
|
name: name.clone(),
|
|
description: description.cloned(),
|
|
permissions: permissions.clone(),
|
|
is_system: false,
|
|
created_at: existing.created_at,
|
|
updated_at: now,
|
|
})
|
|
}
|
|
|
|
pub async fn delete_role(db: &PgPool, role_id: &str) -> SaasResult<()> {
|
|
let existing = get_role(db, role_id).await?;
|
|
|
|
if existing.is_system {
|
|
return Err(SaasError::Forbidden("系统角色不可删除".into()));
|
|
}
|
|
|
|
let result = sqlx::query("DELETE FROM roles WHERE id = $1 AND is_system = false")
|
|
.bind(role_id)
|
|
.execute(db)
|
|
.await?;
|
|
|
|
if result.rows_affected() == 0 {
|
|
return Err(SaasError::NotFound(format!("角色 {} 不存在", role_id)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn list_templates(db: &PgPool) -> SaasResult<Vec<PermissionTemplate>> {
|
|
let rows: Vec<PermissionTemplateRow> =
|
|
sqlx::query_as(
|
|
"SELECT id, name, description, permissions, created_at, updated_at
|
|
FROM permission_templates ORDER BY created_at DESC"
|
|
)
|
|
.fetch_all(db)
|
|
.await?;
|
|
|
|
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<PermissionTemplateRow> =
|
|
sqlx::query_as(
|
|
"SELECT id, name, description, permissions, created_at, updated_at
|
|
FROM permission_templates WHERE id = $1"
|
|
)
|
|
.bind(template_id)
|
|
.fetch_optional(db)
|
|
.await?;
|
|
|
|
let r = row.ok_or_else(|| SaasError::NotFound(format!("权限模板 {} 不存在", template_id)))?;
|
|
|
|
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> {
|
|
let id = uuid::Uuid::new_v4().to_string();
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
let permissions = serde_json::to_string(&req.permissions)?;
|
|
|
|
sqlx::query(
|
|
"INSERT INTO permission_templates (id, name, description, permissions, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $5)"
|
|
)
|
|
.bind(&id)
|
|
.bind(&req.name)
|
|
.bind(&req.description)
|
|
.bind(&permissions)
|
|
.bind(&now)
|
|
.execute(db)
|
|
.await?;
|
|
|
|
Ok(PermissionTemplate {
|
|
id,
|
|
name: req.name.clone(),
|
|
description: req.description.clone(),
|
|
permissions: req.permissions.clone(),
|
|
created_at: now.clone(),
|
|
updated_at: now,
|
|
})
|
|
}
|
|
|
|
pub async fn delete_template(db: &PgPool, template_id: &str) -> SaasResult<()> {
|
|
let result = sqlx::query("DELETE FROM permission_templates WHERE id = $1")
|
|
.bind(template_id)
|
|
.execute(db)
|
|
.await?;
|
|
|
|
if result.rows_affected() == 0 {
|
|
return Err(SaasError::NotFound(format!("权限模板 {} 不存在", template_id)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn apply_template_to_accounts(
|
|
db: &PgPool,
|
|
template_id: &str,
|
|
account_ids: &[String],
|
|
) -> SaasResult<usize> {
|
|
let template = get_template(db, template_id).await?;
|
|
let now = chrono::Utc::now().to_rfc3339();
|
|
|
|
let mut success_count = 0;
|
|
for account_id in account_ids {
|
|
let result = sqlx::query(
|
|
"UPDATE accounts SET role = $1, updated_at = $2 WHERE id = $3"
|
|
)
|
|
.bind(&template.id)
|
|
.bind(&now)
|
|
.bind(account_id)
|
|
.execute(db)
|
|
.await?;
|
|
|
|
if result.rows_affected() > 0 {
|
|
success_count += 1;
|
|
}
|
|
}
|
|
|
|
Ok(success_count)
|
|
}
|