fix(handlers): add handlers
This commit is contained in:
222
src/services/auth.rs
Normal file
222
src/services/auth.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use crate::models::{CreateUserRequest, LoginRequest, LoginResponse, User};
|
||||
use crate::utils::{hash_password, sign, verify_password};
|
||||
use common_telemetry::AppError;
|
||||
use rand::RngCore;
|
||||
use sqlx::PgPool;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthService {
|
||||
pool: PgPool,
|
||||
// jwt_secret removed, using RS256 keys
|
||||
}
|
||||
|
||||
impl AuthService {
|
||||
/// 创建认证服务实例。
|
||||
///
|
||||
/// 说明:
|
||||
/// - 当前实现使用 RS256 密钥对进行 JWT 签发与校验,因此 `_jwt_secret` 参数仅为兼容保留。
|
||||
pub fn new(pool: PgPool, _jwt_secret: String) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
// 注册业务
|
||||
#[instrument(skip(self, req))]
|
||||
/// 在指定租户下注册新用户,并在首次注册时自动引导初始化租户管理员权限。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 用户必须绑定到 `tenant_id`,禁止跨租户注册写入。
|
||||
/// - 密码以安全哈希形式存储,不回传明文。
|
||||
/// - 若该租户用户数为 1(首个用户),自动创建/获取 `Admin` 系统角色并授予全量权限,同时绑定到该用户。
|
||||
///
|
||||
/// 输入:
|
||||
/// - `tenant_id`:目标租户
|
||||
/// - `req.email` / `req.password`:注册信息
|
||||
///
|
||||
/// 输出:
|
||||
/// - 返回创建后的 `User` 记录(包含 `id/tenant_id/email` 等字段)
|
||||
///
|
||||
/// 异常:
|
||||
/// - 数据库写入失败(如唯一约束冲突、连接错误等)
|
||||
/// - 密码哈希失败
|
||||
pub async fn register(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
req: CreateUserRequest,
|
||||
) -> Result<User, AppError> {
|
||||
let mut tx = self.pool.begin().await?;
|
||||
|
||||
// 1. 哈希密码
|
||||
let hashed =
|
||||
hash_password(&req.password).map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?;
|
||||
|
||||
// 2. 存入数据库 (带上 tenant_id)
|
||||
let query = r#"
|
||||
INSERT INTO users (tenant_id, email, password_hash)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, tenant_id, email, password_hash
|
||||
"#;
|
||||
let user = sqlx::query_as::<_, User>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(&req.email)
|
||||
.bind(&hashed)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let user_count: i64 = sqlx::query_scalar("SELECT COUNT(1) FROM users WHERE tenant_id = $1")
|
||||
.bind(tenant_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if user_count == 1 {
|
||||
self.bootstrap_tenant_admin(&mut tx, tenant_id, user.id)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
// 登录业务
|
||||
#[instrument(skip(self, req))]
|
||||
/// 在指定租户内完成用户认证并签发访问令牌与刷新令牌。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 仅在当前租户内按 `email` 查找用户,防止跨租户登录。
|
||||
/// - 密码校验失败返回 `InvalidCredentials`。
|
||||
/// - 登录成功后生成:
|
||||
/// - `access_token`:JWT(包含租户、用户、角色与权限)
|
||||
/// - `refresh_token`:随机生成并哈希后入库(默认 30 天过期)
|
||||
///
|
||||
/// 输出:
|
||||
/// - `LoginResponse`(token_type 固定为 `Bearer`,`expires_in` 当前为 15 分钟)
|
||||
///
|
||||
/// 异常:
|
||||
/// - 用户不存在(404)
|
||||
/// - 密码错误(401)
|
||||
/// - Token 签发失败或数据库写入失败
|
||||
pub async fn login(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
req: LoginRequest,
|
||||
) -> Result<LoginResponse, AppError> {
|
||||
// 1. 查找用户 (带 tenant_id 防止跨租户登录)
|
||||
let query = "SELECT * FROM users WHERE tenant_id = $1 AND email = $2";
|
||||
let user = sqlx::query_as::<_, User>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(&req.email)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("User not found".into()))?;
|
||||
|
||||
// 2. 验证密码
|
||||
if !verify_password(&req.password, &user.password_hash) {
|
||||
return Err(AppError::InvalidCredentials);
|
||||
}
|
||||
|
||||
let roles = sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT r.name
|
||||
FROM roles r
|
||||
JOIN user_roles ur ON ur.role_id = r.id
|
||||
WHERE r.tenant_id = $1 AND ur.user_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user.tenant_id)
|
||||
.bind(user.id)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
let permissions = sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT DISTINCT p.code
|
||||
FROM permissions p
|
||||
JOIN role_permissions rp ON rp.permission_id = p.id
|
||||
JOIN user_roles ur ON ur.role_id = rp.role_id
|
||||
JOIN roles r ON r.id = ur.role_id
|
||||
WHERE r.tenant_id = $1 AND ur.user_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user.tenant_id)
|
||||
.bind(user.id)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
// 3. 签发 Access Token
|
||||
let access_token = sign(user.id, user.tenant_id, roles, permissions)?;
|
||||
|
||||
// 4. 生成 Refresh Token
|
||||
let mut refresh_bytes = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut refresh_bytes);
|
||||
let refresh_token = hex::encode(refresh_bytes);
|
||||
|
||||
// Hash refresh token for storage
|
||||
let refresh_token_hash =
|
||||
hash_password(&refresh_token).map_err(|e| AppError::AnyhowError(anyhow::anyhow!(e)))?;
|
||||
|
||||
// 5. 存储 Refresh Token (30天过期)
|
||||
let expires_at = chrono::Utc::now() + chrono::Duration::days(30);
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)",
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(refresh_token_hash)
|
||||
.bind(expires_at)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(LoginResponse {
|
||||
access_token,
|
||||
refresh_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: 15 * 60, // 15 mins
|
||||
})
|
||||
}
|
||||
|
||||
async fn bootstrap_tenant_admin(
|
||||
&self,
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let role_id: Uuid = sqlx::query_scalar(
|
||||
r#"
|
||||
INSERT INTO roles (tenant_id, name, description, is_system)
|
||||
VALUES ($1, 'Admin', 'Tenant administrator', TRUE)
|
||||
ON CONFLICT (tenant_id, name)
|
||||
DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(tenant_id)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT $1, p.id FROM permissions p
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(role_id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_roles (user_id, role_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(role_id)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
79
src/services/authorization.rs
Normal file
79
src/services/authorization.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use common_telemetry::AppError;
|
||||
use sqlx::PgPool;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthorizationService {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl AuthorizationService {
|
||||
/// 创建权限服务实例。
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 获取用户在指定租户下的权限编码集合(去重)。
|
||||
///
|
||||
/// 说明:
|
||||
/// - 权限来源于用户所属角色(user_roles → roles)及角色绑定权限(role_permissions → permissions)。
|
||||
///
|
||||
/// 输入:
|
||||
/// - `tenant_id`:租户 ID
|
||||
/// - `user_id`:用户 ID
|
||||
///
|
||||
/// 输出:
|
||||
/// - 权限编码数组(如 `tenant:read` / `user:write`)
|
||||
///
|
||||
/// 异常:
|
||||
/// - 数据库查询失败
|
||||
pub async fn list_permissions_for_user(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
let query = r#"
|
||||
SELECT DISTINCT p.code
|
||||
FROM permissions p
|
||||
JOIN role_permissions rp ON rp.permission_id = p.id
|
||||
JOIN user_roles ur ON ur.role_id = rp.role_id
|
||||
JOIN roles r ON r.id = ur.role_id
|
||||
WHERE r.tenant_id = $1 AND ur.user_id = $2
|
||||
"#;
|
||||
let rows = sqlx::query_scalar::<_, String>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(user_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 校验用户是否具备指定权限,不满足则直接返回权限拒绝错误。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 若用户权限集合中不包含 `permission_code`,返回 `PermissionDenied(permission_code)`。
|
||||
///
|
||||
/// 输入:
|
||||
/// - `tenant_id`:租户 ID
|
||||
/// - `user_id`:用户 ID
|
||||
/// - `permission_code`:权限编码
|
||||
///
|
||||
/// 输出:
|
||||
/// - 成功返回 `()`;失败返回权限拒绝错误
|
||||
pub async fn require_permission(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
permission_code: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let permissions = self.list_permissions_for_user(tenant_id, user_id).await?;
|
||||
if permissions.iter().any(|p| p == permission_code) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AppError::PermissionDenied(permission_code.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,11 @@
|
||||
use crate::models::{CreateUserRequest, User}; // 假设你在 models 定义了这些
|
||||
use crate::utils::{create_jwt, hash_password, verify_password};
|
||||
use axum::Json;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
pub mod auth;
|
||||
pub mod authorization;
|
||||
pub mod role;
|
||||
pub mod tenant;
|
||||
pub mod user;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthService {
|
||||
pool: PgPool,
|
||||
jwt_secret: String,
|
||||
}
|
||||
|
||||
impl AuthService {
|
||||
pub fn new(pool: PgPool, jwt_secret: String) -> Self {
|
||||
Self { pool, jwt_secret }
|
||||
}
|
||||
|
||||
// 注册业务
|
||||
pub async fn register(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
req: CreateUserRequest,
|
||||
) -> Result<Json<User>, String> {
|
||||
// 1. 哈希密码
|
||||
let hashed = hash_password(&req.password)?;
|
||||
|
||||
// 2. 存入数据库 (带上 tenant_id)
|
||||
let query = r#"
|
||||
INSERT INTO users (tenant_id, email, password_hash)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, tenant_id, email, password_hash, created_at
|
||||
"#;
|
||||
let user = sqlx::query_as::<_, User>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(&req.email)
|
||||
.bind(&hashed)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(Json(user))
|
||||
}
|
||||
|
||||
// 登录业务
|
||||
pub async fn login(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
email: &str,
|
||||
password: &str,
|
||||
) -> Result<String, String> {
|
||||
// 1. 查找用户 (带 tenant_id 防止跨租户登录)
|
||||
let query = "SELECT * FROM users WHERE tenant_id = $1 AND email = $2";
|
||||
let user = sqlx::query_as::<_, User>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(email)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("User not found")?;
|
||||
|
||||
// 2. 验证密码
|
||||
if !verify_password(password, &user.password_hash) {
|
||||
return Err("Invalid password".to_string());
|
||||
}
|
||||
|
||||
// 3. 签发 Token
|
||||
create_jwt(user.id, user.tenant_id, &self.jwt_secret)
|
||||
}
|
||||
}
|
||||
pub use auth::AuthService;
|
||||
pub use authorization::AuthorizationService;
|
||||
pub use role::RoleService;
|
||||
pub use tenant::TenantService;
|
||||
pub use user::UserService;
|
||||
|
||||
59
src/services/role.rs
Normal file
59
src/services/role.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::models::{CreateRoleRequest, Role};
|
||||
use common_telemetry::AppError;
|
||||
use sqlx::PgPool;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RoleService {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl RoleService {
|
||||
/// 创建角色服务实例。
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 在指定租户下创建角色记录。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 角色与租户强绑定(写入时携带 `tenant_id`)。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 数据库写入失败(如约束冲突、连接错误等)
|
||||
pub async fn create_role(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
req: CreateRoleRequest,
|
||||
) -> Result<Role, AppError> {
|
||||
let query = r#"
|
||||
INSERT INTO roles (tenant_id, name, description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, tenant_id, name, description
|
||||
"#;
|
||||
// Note: 'roles' table needs to be created in DB
|
||||
sqlx::query_as::<_, Role>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(req.name)
|
||||
.bind(req.description)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| AppError::DbError(e))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 查询指定租户下的角色列表。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 数据库查询失败
|
||||
pub async fn list_roles(&self, tenant_id: Uuid) -> Result<Vec<Role>, AppError> {
|
||||
let query = "SELECT * FROM roles WHERE tenant_id = $1";
|
||||
sqlx::query_as::<_, Role>(query)
|
||||
.bind(tenant_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| AppError::DbError(e))
|
||||
}
|
||||
}
|
||||
134
src/services/tenant.rs
Normal file
134
src/services/tenant.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use crate::models::{
|
||||
CreateTenantRequest, Tenant, UpdateTenantRequest, UpdateTenantStatusRequest,
|
||||
};
|
||||
use common_telemetry::AppError;
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TenantService {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl TenantService {
|
||||
/// 创建租户服务实例。
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
#[instrument(skip(self, req))]
|
||||
/// 创建新租户并初始化默认状态与配置。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 默认 `status=active`。
|
||||
/// - `config` 未提供时默认 `{}`。
|
||||
///
|
||||
/// 输出:
|
||||
/// - 返回新建租户记录(含 `id`)
|
||||
///
|
||||
/// 异常:
|
||||
/// - 数据库写入失败(如连接异常、约束失败等)
|
||||
pub async fn create_tenant(&self, req: CreateTenantRequest) -> Result<Tenant, AppError> {
|
||||
let config = req.config.unwrap_or_else(|| Value::Object(Default::default()));
|
||||
let query = r#"
|
||||
INSERT INTO tenants (name, status, config)
|
||||
VALUES ($1, 'active', $2)
|
||||
RETURNING id, name, status, config
|
||||
"#;
|
||||
let tenant = sqlx::query_as::<_, Tenant>(query)
|
||||
.bind(req.name)
|
||||
.bind(config)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
Ok(tenant)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 根据租户 ID 查询租户信息。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
|
||||
pub async fn get_tenant(&self, tenant_id: Uuid) -> Result<Tenant, AppError> {
|
||||
let query = "SELECT id, name, status, config FROM tenants WHERE id = $1";
|
||||
sqlx::query_as::<_, Tenant>(query)
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Tenant not found".into()))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, req))]
|
||||
/// 更新租户基础信息(名称 / 配置)。
|
||||
///
|
||||
/// 说明:
|
||||
/// - 仅更新 `UpdateTenantRequest` 中提供的字段,未提供字段保持不变。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
|
||||
pub async fn update_tenant(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
req: UpdateTenantRequest,
|
||||
) -> Result<Tenant, AppError> {
|
||||
let query = r#"
|
||||
UPDATE tenants
|
||||
SET
|
||||
name = COALESCE($1, name),
|
||||
config = COALESCE($2, config),
|
||||
updated_at = NOW()
|
||||
WHERE id = $3
|
||||
RETURNING id, name, status, config
|
||||
"#;
|
||||
sqlx::query_as::<_, Tenant>(query)
|
||||
.bind(req.name)
|
||||
.bind(req.config)
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Tenant not found".into()))
|
||||
}
|
||||
|
||||
#[instrument(skip(self, req))]
|
||||
/// 更新租户状态字段(如 active / disabled)。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
|
||||
pub async fn update_tenant_status(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
req: UpdateTenantStatusRequest,
|
||||
) -> Result<Tenant, AppError> {
|
||||
let query = r#"
|
||||
UPDATE tenants
|
||||
SET
|
||||
status = $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING id, name, status, config
|
||||
"#;
|
||||
sqlx::query_as::<_, Tenant>(query)
|
||||
.bind(req.status)
|
||||
.bind(tenant_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Tenant not found".into()))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 删除指定租户。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 若租户不存在,返回 `NotFound("Tenant not found")`。
|
||||
pub async fn delete_tenant(&self, tenant_id: Uuid) -> Result<(), AppError> {
|
||||
let result = sqlx::query("DELETE FROM tenants WHERE id = $1")
|
||||
.bind(tenant_id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("Tenant not found".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
109
src/services/user.rs
Normal file
109
src/services/user.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::models::{UpdateUserRequest, User};
|
||||
use common_telemetry::AppError;
|
||||
use sqlx::PgPool;
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserService {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl UserService {
|
||||
/// 创建用户服务实例。
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 根据用户 ID 查询用户记录(限定在指定租户内)。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 查询条件同时包含 `tenant_id` 与 `user_id`,避免跨租户读取。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 用户不存在返回 `NotFound("User not found")`
|
||||
pub async fn get_user_by_id(&self, tenant_id: Uuid, user_id: Uuid) -> Result<User, AppError> {
|
||||
let query = "SELECT * FROM users WHERE tenant_id = $1 AND id = $2";
|
||||
sqlx::query_as::<_, User>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(|e| AppError::DbError(e))?
|
||||
.ok_or_else(|| AppError::NotFound("User not found".into()))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 分页查询租户下用户列表。
|
||||
///
|
||||
/// 说明:
|
||||
/// - `offset = (page - 1) * page_size`,由上层负责保证 `page>=1`。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 数据库查询失败
|
||||
pub async fn list_users(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
) -> Result<Vec<User>, AppError> {
|
||||
let offset = (page - 1) * page_size;
|
||||
let query = "SELECT * FROM users WHERE tenant_id = $1 LIMIT $2 OFFSET $3";
|
||||
sqlx::query_as::<_, User>(query)
|
||||
.bind(tenant_id)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| AppError::DbError(e))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 更新指定用户信息(目前仅支持邮箱字段)。
|
||||
///
|
||||
/// 业务规则:
|
||||
/// - 查询条件同时包含 `tenant_id` 与 `user_id`,避免跨租户更新。
|
||||
/// - `UpdateUserRequest` 中未提供字段保持不变。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 用户不存在返回 `NotFound("User not found")`
|
||||
pub async fn update_user(
|
||||
&self,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
req: UpdateUserRequest,
|
||||
) -> Result<User, AppError> {
|
||||
// Simple update implementation
|
||||
// In a real app, you'd build the query dynamically based on Option fields
|
||||
let query = "UPDATE users SET email = COALESCE($1, email) WHERE tenant_id = $2 AND id = $3 RETURNING *";
|
||||
sqlx::query_as::<_, User>(query)
|
||||
.bind(req.email)
|
||||
.bind(tenant_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(|e| AppError::DbError(e))?
|
||||
.ok_or_else(|| AppError::NotFound("User not found".into()))
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
/// 删除指定用户(限定在指定租户内)。
|
||||
///
|
||||
/// 异常:
|
||||
/// - 用户不存在返回 `NotFound("User not found")`
|
||||
pub async fn delete_user(&self, tenant_id: Uuid, user_id: Uuid) -> Result<(), AppError> {
|
||||
let query = "DELETE FROM users WHERE tenant_id = $1 AND id = $2";
|
||||
let result = sqlx::query(query)
|
||||
.bind(tenant_id)
|
||||
.bind(user_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| AppError::DbError(e))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("User not found".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user