chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
//! 账号管理业务逻辑
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::PgPool;
|
||||
use crate::error::{SaasError, SaasResult};
|
||||
use crate::common::{PaginatedResponse, normalize_pagination};
|
||||
use super::types::*;
|
||||
|
||||
pub async fn list_accounts(
|
||||
db: &SqlitePool,
|
||||
db: &PgPool,
|
||||
query: &ListAccountsQuery,
|
||||
) -> SaasResult<PaginatedResponse<serde_json::Value>> {
|
||||
let page = query.page.unwrap_or(1).max(1);
|
||||
@@ -14,17 +15,21 @@ pub async fn list_accounts(
|
||||
|
||||
let mut where_clauses = Vec::new();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
let mut param_idx = 1usize;
|
||||
|
||||
if let Some(role) = &query.role {
|
||||
where_clauses.push("role = ?".to_string());
|
||||
where_clauses.push(format!("role = ${}", param_idx));
|
||||
param_idx += 1;
|
||||
params.push(role.clone());
|
||||
}
|
||||
if let Some(status) = &query.status {
|
||||
where_clauses.push("status = ?".to_string());
|
||||
where_clauses.push(format!("status = ${}", param_idx));
|
||||
param_idx += 1;
|
||||
params.push(status.clone());
|
||||
}
|
||||
if let Some(search) = &query.search {
|
||||
where_clauses.push("(username LIKE ? OR email LIKE ? OR display_name LIKE ?)".to_string());
|
||||
where_clauses.push(format!("(username LIKE ${} OR email LIKE ${} OR display_name LIKE ${})", param_idx, param_idx + 1, param_idx + 2));
|
||||
param_idx += 3;
|
||||
let pattern = format!("%{}%", search);
|
||||
params.push(pattern.clone());
|
||||
params.push(pattern.clone());
|
||||
@@ -44,10 +49,12 @@ pub async fn list_accounts(
|
||||
}
|
||||
let total: i64 = count_query.fetch_one(db).await?;
|
||||
|
||||
let limit_idx = param_idx;
|
||||
let offset_idx = param_idx + 1;
|
||||
let data_sql = format!(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts {} ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||
where_sql
|
||||
FROM accounts {} ORDER BY created_at DESC LIMIT ${} OFFSET ${}",
|
||||
where_sql, limit_idx, offset_idx
|
||||
);
|
||||
let mut data_query = sqlx::query_as::<_, (String, String, String, String, String, String, bool, Option<String>, String)>(&data_sql);
|
||||
for p in ¶ms {
|
||||
@@ -69,11 +76,11 @@ pub async fn list_accounts(
|
||||
Ok(PaginatedResponse { items, total, page, page_size })
|
||||
}
|
||||
|
||||
pub async fn get_account(db: &SqlitePool, account_id: &str) -> SaasResult<serde_json::Value> {
|
||||
pub async fn get_account(db: &PgPool, account_id: &str) -> SaasResult<serde_json::Value> {
|
||||
let row: Option<(String, String, String, String, String, String, bool, Option<String>, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, username, email, display_name, role, status, totp_enabled, last_login_at, created_at
|
||||
FROM accounts WHERE id = ?1"
|
||||
FROM accounts WHERE id = $1"
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_optional(db)
|
||||
@@ -90,28 +97,30 @@ pub async fn get_account(db: &SqlitePool, account_id: &str) -> SaasResult<serde_
|
||||
}
|
||||
|
||||
pub async fn update_account(
|
||||
db: &SqlitePool,
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
req: &UpdateAccountRequest,
|
||||
) -> SaasResult<serde_json::Value> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let mut updates = Vec::new();
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
let mut param_idx = 1usize;
|
||||
|
||||
if let Some(ref v) = req.display_name { updates.push("display_name = ?"); params.push(v.clone()); }
|
||||
if let Some(ref v) = req.email { updates.push("email = ?"); params.push(v.clone()); }
|
||||
if let Some(ref v) = req.role { updates.push("role = ?"); params.push(v.clone()); }
|
||||
if let Some(ref v) = req.avatar_url { updates.push("avatar_url = ?"); params.push(v.clone()); }
|
||||
if let Some(ref v) = req.display_name { updates.push(format!("display_name = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
if let Some(ref v) = req.email { updates.push(format!("email = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
if let Some(ref v) = req.role { updates.push(format!("role = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
if let Some(ref v) = req.avatar_url { updates.push(format!("avatar_url = ${}", param_idx)); param_idx += 1; params.push(v.clone()); }
|
||||
|
||||
if updates.is_empty() {
|
||||
return get_account(db, account_id).await;
|
||||
}
|
||||
|
||||
updates.push("updated_at = ?");
|
||||
updates.push(format!("updated_at = ${}", param_idx));
|
||||
param_idx += 1;
|
||||
params.push(now.clone());
|
||||
params.push(account_id.to_string());
|
||||
|
||||
let sql = format!("UPDATE accounts SET {} WHERE id = ?", updates.join(", "));
|
||||
let sql = format!("UPDATE accounts SET {} WHERE id = ${}", updates.join(", "), param_idx);
|
||||
let mut query = sqlx::query(&sql);
|
||||
for p in ¶ms {
|
||||
query = query.bind(p);
|
||||
@@ -121,7 +130,7 @@ pub async fn update_account(
|
||||
}
|
||||
|
||||
pub async fn update_account_status(
|
||||
db: &SqlitePool,
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
status: &str,
|
||||
) -> SaasResult<()> {
|
||||
@@ -130,7 +139,7 @@ pub async fn update_account_status(
|
||||
return Err(SaasError::InvalidInput(format!("无效状态: {},有效值: {:?}", status, valid)));
|
||||
}
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let result = sqlx::query("UPDATE accounts SET status = ?1, updated_at = ?2 WHERE id = ?3")
|
||||
let result = sqlx::query("UPDATE accounts SET status = $1, updated_at = $2 WHERE id = $3")
|
||||
.bind(status).bind(&now).bind(account_id)
|
||||
.execute(db).await?;
|
||||
|
||||
@@ -141,7 +150,7 @@ pub async fn update_account_status(
|
||||
}
|
||||
|
||||
pub async fn create_api_token(
|
||||
db: &SqlitePool,
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
req: &CreateTokenRequest,
|
||||
) -> SaasResult<TokenInfo> {
|
||||
@@ -163,7 +172,7 @@ pub async fn create_api_token(
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO api_tokens (id, account_id, name, token_hash, token_prefix, permissions, created_at, expires_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"
|
||||
)
|
||||
.bind(&token_id)
|
||||
.bind(account_id)
|
||||
@@ -189,28 +198,80 @@ pub async fn create_api_token(
|
||||
}
|
||||
|
||||
pub async fn list_api_tokens(
|
||||
db: &SqlitePool,
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
) -> SaasResult<Vec<TokenInfo>> {
|
||||
page: Option<u32>,
|
||||
page_size: Option<u32>,
|
||||
) -> SaasResult<PaginatedResponse<TokenInfo>> {
|
||||
let (p, ps, offset) = normalize_pagination(page, page_size);
|
||||
|
||||
let total: (i64,) = sqlx::query_as(
|
||||
"SELECT COUNT(*) FROM api_tokens WHERE account_id = $1 AND revoked_at IS NULL"
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
let rows: Vec<(String, String, String, String, Option<String>, Option<String>, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, name, token_prefix, permissions, last_used_at, expires_at, created_at
|
||||
FROM api_tokens WHERE account_id = ?1 AND revoked_at IS NULL ORDER BY created_at DESC"
|
||||
FROM api_tokens WHERE account_id = $1 AND revoked_at IS NULL ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||
)
|
||||
.bind(account_id)
|
||||
.bind(ps as i64)
|
||||
.bind(offset)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(|(id, name, token_prefix, perms, last_used, expires, created)| {
|
||||
let items = rows.into_iter().map(|(id, name, token_prefix, perms, last_used, expires, created)| {
|
||||
let permissions: Vec<String> = serde_json::from_str(&perms).unwrap_or_default();
|
||||
TokenInfo { id, name, token_prefix, permissions, last_used_at: last_used, expires_at: expires, created_at: created, token: None, }
|
||||
}).collect())
|
||||
}).collect();
|
||||
|
||||
Ok(PaginatedResponse { items, total: total.0, page: p, page_size: ps })
|
||||
}
|
||||
|
||||
pub async fn revoke_api_token(db: &SqlitePool, token_id: &str, account_id: &str) -> SaasResult<()> {
|
||||
pub async fn list_devices(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
page: Option<u32>,
|
||||
page_size: Option<u32>,
|
||||
) -> SaasResult<PaginatedResponse<serde_json::Value>> {
|
||||
let (p, ps, offset) = normalize_pagination(page, page_size);
|
||||
|
||||
let total: (i64,) = sqlx::query_as(
|
||||
"SELECT COUNT(*) FROM devices WHERE account_id = $1"
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
let rows: Vec<(String, String, Option<String>, Option<String>, Option<String>, String, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, device_id, device_name, platform, app_version, last_seen_at, created_at
|
||||
FROM devices WHERE account_id = $1 ORDER BY last_seen_at DESC LIMIT $2 OFFSET $3"
|
||||
)
|
||||
.bind(account_id)
|
||||
.bind(ps as i64)
|
||||
.bind(offset)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
let items: Vec<serde_json::Value> = rows.into_iter().map(|r| {
|
||||
serde_json::json!({
|
||||
"id": r.0, "device_id": r.1,
|
||||
"device_name": r.2, "platform": r.3, "app_version": r.4,
|
||||
"last_seen_at": r.5, "created_at": r.6,
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Ok(PaginatedResponse { items, total: total.0, page: p, page_size: ps })
|
||||
}
|
||||
|
||||
pub async fn revoke_api_token(db: &PgPool, token_id: &str, account_id: &str) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let result = sqlx::query(
|
||||
"UPDATE api_tokens SET revoked_at = ?1 WHERE id = ?2 AND account_id = ?3 AND revoked_at IS NULL"
|
||||
"UPDATE api_tokens SET revoked_at = $1 WHERE id = $2 AND account_id = $3 AND revoked_at IS NULL"
|
||||
)
|
||||
.bind(&now).bind(token_id).bind(account_id)
|
||||
.execute(db).await?;
|
||||
|
||||
Reference in New Issue
Block a user