fix(error): perf error
This commit is contained in:
224
src/error.rs
224
src/error.rs
@@ -14,45 +14,111 @@ pub enum BizCode {
|
||||
Success = 0,
|
||||
|
||||
// 10xxx: 服务端通用错误
|
||||
ServerError = 10000,
|
||||
BadRequest = 10001,
|
||||
ServerError = 10000, // 未知/兜底错误
|
||||
DbError = 10001, // 数据库操作失败
|
||||
CacheError = 10002, // Redis/缓存失败
|
||||
SerializationError = 10003, // JSON 解析/序列化失败
|
||||
ExternalServiceError = 10004, // 调用第三方/下游服务失败
|
||||
ConfigError = 10005, // 配置加载失败
|
||||
|
||||
// 20xxx: 认证授权相关 (双 Token 核心逻辑)
|
||||
Unauthorized = 20000, // 通用未授权/签名错误
|
||||
AccessTokenExpired = 20001, // 前端捕获 -> 用 RefreshToken 换新 AccessToken (静默)
|
||||
RefreshTokenExpired = 20002, // 前端捕获 -> 强制退出到登录页
|
||||
PermissionDenied = 20003,
|
||||
Unauthorized = 20000, // 未认证 (通用)
|
||||
AccessTokenExpired = 20001, // [前端触发] 静默刷新
|
||||
RefreshTokenExpired = 20002, // [前端触发] 强制登出
|
||||
PermissionDenied = 20003, // 无权限 (403)
|
||||
AccountDisabled = 20004, // 账号被禁用/锁定
|
||||
InvalidCredentials = 20005, // 账号或密码错误
|
||||
MissingHeader = 20006, // 缺少必要的 Header (如 x-trace-id, Authorization)
|
||||
|
||||
// === 30xxx: 客户端输入错误 ===
|
||||
BadRequest = 30000, // 通用请求参数错误
|
||||
ValidationError = 30001, // 字段校验不通过 (Validation)
|
||||
ResourceNotFound = 30002, // 资源不存在 (404)
|
||||
ResourceAlreadyExists = 30003, // 资源冲突/重复 (409)
|
||||
MethodNotAllowed = 30004, // HTTP 方法不支持
|
||||
|
||||
// === 40xxx: 业务流控/状态 ===
|
||||
RateLimitExceeded = 40000, // 请求过于频繁 (429)
|
||||
PreconditionFailed = 40001, // 业务前置条件不满足 (如:余额不足无法支付)
|
||||
}
|
||||
|
||||
/// 全局应用错误枚举
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
// --- 基础设施错误 ---
|
||||
#[cfg(feature = "with-sqlx")]
|
||||
#[error("Database error: {0}")]
|
||||
DbError(String), // 实际建议: DbError(#[from] sqlx::Error)
|
||||
DbError(sqlx::Error),
|
||||
|
||||
#[cfg(feature = "with-redis")]
|
||||
#[error("Cache error: {0}")]
|
||||
CacheError(#[from] redis::RedisError),
|
||||
|
||||
// 外部服务调用错误 (如调用其他微服务失败)
|
||||
#[error("External service error: {0}")]
|
||||
ExternalReqError(String), // 对应 reqwest::Error
|
||||
|
||||
// 消息队列错误
|
||||
#[error("MQ error: {0}")]
|
||||
MqError(String),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
// --- 业务逻辑错误 ---
|
||||
#[error("Resource not found: {0}")]
|
||||
NotFound(String),
|
||||
// 序列化/反序列化错误
|
||||
#[error("Serialization error: {0}")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Invalid parameters: {0}")]
|
||||
InvalidParam(String),
|
||||
// 任意未预期错误 (兜底)
|
||||
#[cfg(feature = "with-anyhow")]
|
||||
#[error("Unexpected error: {0}")]
|
||||
AnyhowError(#[from] anyhow::Error),
|
||||
|
||||
// --- 认证相关 ---
|
||||
#[error("Authentication failed")]
|
||||
AuthError,
|
||||
// ======================================================
|
||||
// 2. 认证与授权层 (Auth) - 双 Token 核心
|
||||
// ======================================================
|
||||
#[error("Authentication failed: {0}")]
|
||||
AuthError(String), // 通用认证失败 (签名错误、格式错误)
|
||||
|
||||
#[error("Access token expired")]
|
||||
AccessTokenExpired,
|
||||
#[error("Missing authorization header")]
|
||||
MissingAuthHeader,
|
||||
|
||||
#[error("Refresh token expired")]
|
||||
RefreshTokenExpired,
|
||||
#[error("Access token has expired")]
|
||||
AccessTokenExpired, // 触发 refresh
|
||||
|
||||
#[error("Refresh token has expired")]
|
||||
RefreshTokenExpired, // 触发 logout
|
||||
|
||||
#[error("Permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
PermissionDenied(String), // 403
|
||||
|
||||
#[error("Account is disabled or locked")]
|
||||
AccountLocked,
|
||||
|
||||
// ======================================================
|
||||
// 3. 客户端输入层 (Client Side)
|
||||
// ======================================================
|
||||
#[error("Resource not found: {0}")]
|
||||
NotFound(String), // 404
|
||||
|
||||
#[error("Resource already exists: {0}")]
|
||||
AlreadyExists(String), // 409 Conflict
|
||||
|
||||
#[error("Invalid request parameters: {0}")]
|
||||
BadRequest(String), // 400
|
||||
|
||||
#[cfg(feature = "with-validator")]
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
// ======================================================
|
||||
// 4. 业务逻辑层 (Business Logic)
|
||||
// ======================================================
|
||||
#[error("Rate limit exceeded, please try again later")]
|
||||
RateLimitExceeded, // 429
|
||||
|
||||
#[error("Business precondition failed: {0}")]
|
||||
BusinessLogicError(String), // 业务状态冲突 (例如:订单已支付不能取消)
|
||||
}
|
||||
|
||||
/// 响应给前端的 JSON 结构
|
||||
@@ -64,29 +130,93 @@ pub struct ErrorResponse {
|
||||
pub trace_id: Option<String>, // 可选:返回 trace_id 方便排查
|
||||
}
|
||||
|
||||
// 1. 实现 sqlx::Error -> AppError
|
||||
#[cfg(feature = "with-sqlx")]
|
||||
impl From<sqlx::Error> for AppError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
match e {
|
||||
// 将 "查不到数据" 转换为 "NotFound" 业务错误
|
||||
sqlx::Error::RowNotFound => AppError::NotFound("Database row not found".into()),
|
||||
// 其他错误保持为 DbError
|
||||
other => AppError::DbError(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 实现 validator::ValidationErrors -> AppError
|
||||
#[cfg(feature = "with-validator")]
|
||||
impl From<validator::ValidationErrors> for AppError {
|
||||
fn from(e: validator::ValidationErrors) -> Self {
|
||||
// 将复杂的校验错误结构体转为字符串供前端显示
|
||||
// 你也可以选择在这里只取第一个错误,或者转为 JSON 字符串
|
||||
AppError::ValidationError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
// 映射 HTTP 状态码 (给网关/浏览器看)
|
||||
fn http_status(&self) -> StatusCode {
|
||||
match self {
|
||||
AppError::DbError(_) | AppError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
AppError::InvalidParam(_) => StatusCode::BAD_REQUEST,
|
||||
AppError::AuthError | AppError::AccessTokenExpired | AppError::RefreshTokenExpired => {
|
||||
StatusCode::UNAUTHORIZED
|
||||
}
|
||||
AppError::DbError(_)
|
||||
| AppError::CacheError(_)
|
||||
| AppError::ExternalReqError(_)
|
||||
| AppError::MqError(_)
|
||||
| AppError::IoError(_)
|
||||
| AppError::SerdeError(_)
|
||||
| AppError::AnyhowError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
// 401 Unauthorized
|
||||
AppError::AuthError(_)
|
||||
| AppError::MissingAuthHeader
|
||||
| AppError::AccessTokenExpired
|
||||
| AppError::RefreshTokenExpired
|
||||
| AppError::AccountLocked => StatusCode::UNAUTHORIZED,
|
||||
|
||||
// 403 Forbidden
|
||||
AppError::PermissionDenied(_) => StatusCode::FORBIDDEN,
|
||||
|
||||
// 404 Not Found
|
||||
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
|
||||
// 409 Conflict
|
||||
AppError::AlreadyExists(_) => StatusCode::CONFLICT,
|
||||
|
||||
// 429 Too Many Requests
|
||||
AppError::RateLimitExceeded => StatusCode::TOO_MANY_REQUESTS,
|
||||
|
||||
// 400 Bad Request (默认)
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
// 映射业务状态码 (给前端代码看)
|
||||
fn biz_code(&self) -> BizCode {
|
||||
match self {
|
||||
AppError::DbError(_) | AppError::IoError(_) => BizCode::ServerError,
|
||||
AppError::NotFound(_) | AppError::InvalidParam(_) => BizCode::BadRequest,
|
||||
AppError::AuthError => BizCode::Unauthorized,
|
||||
AppError::AccessTokenExpired => BizCode::AccessTokenExpired, // 关键
|
||||
AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired, // 关键
|
||||
// Infra
|
||||
AppError::DbError(_) => BizCode::DbError,
|
||||
AppError::CacheError(_) => BizCode::CacheError,
|
||||
AppError::ExternalReqError(_) => BizCode::ExternalServiceError,
|
||||
AppError::MqError(_) | AppError::IoError(_) | AppError::AnyhowError(_) => {
|
||||
BizCode::ServerError
|
||||
}
|
||||
AppError::SerdeError(_) => BizCode::SerializationError,
|
||||
|
||||
// Auth
|
||||
AppError::AuthError(_) => BizCode::Unauthorized,
|
||||
AppError::MissingAuthHeader => BizCode::MissingHeader,
|
||||
AppError::AccessTokenExpired => BizCode::AccessTokenExpired,
|
||||
AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired,
|
||||
AppError::PermissionDenied(_) => BizCode::PermissionDenied,
|
||||
AppError::AccountLocked => BizCode::AccountDisabled,
|
||||
|
||||
// Client
|
||||
AppError::NotFound(_) => BizCode::ResourceNotFound,
|
||||
AppError::AlreadyExists(_) => BizCode::ResourceAlreadyExists,
|
||||
AppError::BadRequest(_) => BizCode::BadRequest,
|
||||
AppError::ValidationError(_) => BizCode::ValidationError,
|
||||
|
||||
// Biz
|
||||
AppError::RateLimitExceeded => BizCode::RateLimitExceeded,
|
||||
AppError::BusinessLogicError(_) => BizCode::PreconditionFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,20 +226,32 @@ impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let status = self.http_status();
|
||||
let biz_code = self.biz_code();
|
||||
let message = self.to_string();
|
||||
// 生产环境通常不把详细的 DB 报错返回给前端,防止泄露表结构
|
||||
// 但这里为了演示,我们先直接使用 self.to_string()
|
||||
// 建议:在生产环境针对 DbError/AnyhowError 返回统一的 "Internal Server Error"
|
||||
let message = match self {
|
||||
AppError::DbError(_) | AppError::AnyhowError(_) => {
|
||||
// 如果是生产环境(release模式),隐藏敏感信息
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
"Internal server error".to_string()
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
_ => self.to_string(),
|
||||
};
|
||||
|
||||
// 尝试获取当前的 Trace ID (如果有 telemetry 功能)
|
||||
// 这里只是简单的占位,实际需要配合 opentelemetry 获取 span id
|
||||
let trace_id = None;
|
||||
|
||||
// 1. 自动记录错误日志 (利用 tracing)
|
||||
// 这样业务代码里只需要 return Err(...),不需要手动 error!(...)
|
||||
// 1. 自动打印 Error 日志 (带上 Trace)
|
||||
// 只有开启 telemetry feature 时才打印
|
||||
#[cfg(feature = "telemetry")]
|
||||
{
|
||||
tracing::error!(
|
||||
%status,
|
||||
code = ?biz_code,
|
||||
error = %message,
|
||||
code = ?biz_code, // 打印枚举名
|
||||
error_msg = %self, // 打印详细错误信息
|
||||
"Request failed"
|
||||
);
|
||||
}
|
||||
@@ -118,7 +260,7 @@ impl IntoResponse for AppError {
|
||||
let body = Json(ErrorResponse {
|
||||
code: biz_code as u32,
|
||||
message,
|
||||
trace_id,
|
||||
trace_id: None, // 实际项目中从 tracing context 获取
|
||||
});
|
||||
|
||||
(status, body).into_response()
|
||||
|
||||
Reference in New Issue
Block a user