chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -0,0 +1,238 @@
//! 角色管理业务逻辑
use sqlx::PgPool;
use crate::error::{SaasError, SaasResult};
use super::types::*;
pub async fn list_roles(db: &PgPool) -> SaasResult<Vec<RoleInfo>> {
let rows: Vec<(String, String, Option<String>, String, bool, String, String)> =
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(|(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 }
}).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)> =
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 (id, name, description, perms, is_system, created_at, updated_at) =
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 })
}
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<(String, String, Option<String>, String, String, String)> =
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(|(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 }
}).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)> =
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 (id, name, description, perms, created_at, updated_at) =
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 })
}
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)
}