Files
zclaw_openfang/crates/zclaw-saas/src/error.rs
iven 5c48d62f7e
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
fix(saas): harden model group failover + relay reliability
- 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>
2026-04-04 12:26:55 +08:00

148 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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>;