Files
common-telemetry/src/error.rs
2026-01-29 15:49:46 +08:00

269 lines
9.4 KiB
Rust

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<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::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()
}
}