use axum::{ Json, http::StatusCode, response::{IntoResponse, Response}, }; use serde::{Deserialize, Serialize}; use thiserror::Error; /// 业务状态码 (Business Code) /// 前端根据此 Code 进行逻辑跳转 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] #[repr(u32)] pub enum BizCode { Success = 0, // 10xxx: 服务端通用错误 ServerError = 10000, // 未知/兜底错误 DbError = 10001, // 数据库操作失败 CacheError = 10002, // Redis/缓存失败 SerializationError = 10003, // JSON 解析/序列化失败 ExternalServiceError = 10004, // 调用第三方/下游服务失败 ConfigError = 10005, // 配置加载失败 // 20xxx: 认证授权相关 (双 Token 核心逻辑) 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(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("Serialization error: {0}")] SerdeError(#[from] serde_json::Error), // 任意未预期错误 (兜底) #[cfg(feature = "with-anyhow")] #[error("Unexpected error: {0}")] AnyhowError(#[from] anyhow::Error), // ====================================================== // 2. 认证与授权层 (Auth) - 双 Token 核心 // ====================================================== #[error("Authentication failed: {0}")] AuthError(String), // 通用认证失败 (签名错误、格式错误) #[error("Missing authorization header")] MissingAuthHeader, #[error("Access token has expired")] AccessTokenExpired, // 触发 refresh #[error("Refresh token has expired")] RefreshTokenExpired, // 触发 logout #[error("Permission denied: {0}")] 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 结构 #[derive(Serialize)] pub struct ErrorResponse { pub code: u32, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub trace_id: Option, // 可选:返回 trace_id 方便排查 } // 1. 实现 sqlx::Error -> AppError #[cfg(feature = "with-sqlx")] impl From 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 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::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 { // 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, } } } // 核心:实现 Axum 的 IntoResponse impl IntoResponse for AppError { fn into_response(self) -> Response { let status = self.http_status(); let biz_code = self.biz_code(); // 生产环境通常不把详细的 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(), }; // 1. 自动打印 Error 日志 (带上 Trace) // 只有开启 telemetry feature 时才打印 #[cfg(feature = "telemetry")] { tracing::error!( %status, code = ?biz_code, // 打印枚举名 error_msg = %self, // 打印详细错误信息 "Request failed" ); } // 2. 构建 JSON let body = Json(ErrorResponse { code: biz_code as u32, message, trace_id: None, // 实际项目中从 tracing context 获取 }); (status, body).into_response() } }