Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- cache: insert-then-retain pattern avoids empty-window race during refresh - relay: manage_task_status flag for proper failover state transitions - relay: retry_task re-resolves model groups instead of blind provider reuse - relay: filter empty-member groups from available models list - relay: quota cache stale entry cleanup (TTL 5x expiry) - error: from_sqlx_unique helper for 409 vs 500 distinction - model_config: unique constraint handling, duplicate member check - model_config: failover_strategy whitelist, model_id vs group name conflict check - model_config: group-scoped member removal with group_id validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
4.7 KiB
Rust
148 lines
4.7 KiB
Rust
//! 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);
|
||
(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<T> = std::result::Result<T, SaasError>;
|