//! SaaS 错误类型 use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde_json::json; /// SaaS 服务错误类型 #[derive(Debug, thiserror::Error)] pub enum SaasError { #[error("未找到: {0}")] NotFound(String), #[error("权限不足: {0}")] Forbidden(String), #[error("未认证")] Unauthorized, #[error("无效输入: {0}")] InvalidInput(String), #[error("认证失败: {0}")] AuthError(String), #[error("用户已存在: {0}")] AlreadyExists(String), #[error("序列化错误: {0}")] Serialization(#[from] serde_json::Error), #[error("IO 错误: {0}")] Io(#[from] std::io::Error), #[error("数据库错误: {0}")] Database(#[from] sqlx::Error), #[error("配置错误: {0}")] Config(#[from] toml::de::Error), #[error("JWT 错误: {0}")] Jwt(#[from] jsonwebtoken::errors::Error), #[error("密码哈希错误: {0}")] PasswordHash(String), #[error("TOTP 错误: {0}")] Totp(String), #[error("加密错误: {0}")] Encryption(String), #[error("中转错误: {0}")] Relay(String), #[error("通用错误: {0}")] General(#[from] anyhow::Error), #[error("速率限制: {0}")] RateLimited(String), #[error("内部错误: {0}")] Internal(String), } impl SaasError { /// 将 sqlx::Error 中的 unique violation 映射为 AlreadyExists (409), /// 其他 DB 错误保持为 Database (500)。 pub fn from_sqlx_unique(e: sqlx::Error, context: &str) -> Self { if let sqlx::Error::Database(ref db_err) = e { // PostgreSQL unique_violation = "23505" if db_err.code().map(|c| c == "23505").unwrap_or(false) { return Self::AlreadyExists(format!("{}已存在", context)); } } Self::Database(e) } /// 获取 HTTP 状态码 pub fn status_code(&self) -> StatusCode { match self { Self::NotFound(_) => StatusCode::NOT_FOUND, Self::Forbidden(_) => StatusCode::FORBIDDEN, Self::Unauthorized => StatusCode::UNAUTHORIZED, Self::InvalidInput(_) => StatusCode::BAD_REQUEST, Self::AlreadyExists(_) => StatusCode::CONFLICT, Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS, Self::Database(_) | Self::Internal(_) | Self::Io(_) | Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::AuthError(_) => StatusCode::UNAUTHORIZED, Self::Jwt(_) | Self::PasswordHash(_) | Self::Encryption(_) => { StatusCode::INTERNAL_SERVER_ERROR } Self::Totp(_) => StatusCode::BAD_REQUEST, Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::Relay(_) => StatusCode::BAD_GATEWAY, Self::General(_) => StatusCode::INTERNAL_SERVER_ERROR, } } /// 获取错误代码 pub fn error_code(&self) -> &str { match self { Self::NotFound(_) => "NOT_FOUND", Self::Forbidden(_) => "FORBIDDEN", Self::Unauthorized => "UNAUTHORIZED", Self::InvalidInput(_) => "INVALID_INPUT", Self::AlreadyExists(_) => "ALREADY_EXISTS", Self::RateLimited(_) => "RATE_LIMITED", Self::Database(_) => "DATABASE_ERROR", Self::Io(_) => "IO_ERROR", Self::Serialization(_) => "SERIALIZATION_ERROR", Self::Internal(_) => "INTERNAL_ERROR", Self::AuthError(_) => "AUTH_ERROR", Self::Jwt(_) => "JWT_ERROR", Self::PasswordHash(_) => "PASSWORD_HASH_ERROR", Self::Totp(_) => "TOTP_ERROR", Self::Encryption(_) => "ENCRYPTION_ERROR", Self::Config(_) => "CONFIG_ERROR", Self::Relay(_) => "RELAY_ERROR", Self::General(_) => "GENERAL_ERROR", } } } /// 实现 Axum 响应 impl IntoResponse for SaasError { fn into_response(self) -> Response { let status = self.status_code(); let (error_code, message) = match &self { // 500 错误不泄露内部细节给客户端 (开发模式除外) Self::Database(_) | Self::Internal(_) | Self::Io(_) | Self::Jwt(_) | Self::Config(_) => { tracing::error!("内部错误 [{}]: {}", self.error_code(), self); if std::env::var("ZCLAW_SAAS_DEV").as_deref() == Ok("true") { (self.error_code().to_string(), format!("[DEV] {}", self)) } else { (self.error_code().to_string(), "服务内部错误".to_string()) } } _ => (self.error_code().to_string(), self.to_string()), }; let body = json!({ "error": error_code, "message": message, }); (status, axum::Json(body)).into_response() } } /// Result 类型别名 pub type SaasResult = std::result::Result;