feat(project): init

This commit is contained in:
2026-01-29 14:37:31 +08:00
commit 88868050ba
8 changed files with 627 additions and 0 deletions

126
src/error.rs Normal file
View File

@@ -0,0 +1,126 @@
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,
BadRequest = 10001,
// 20xxx: 认证授权相关 (双 Token 核心逻辑)
Unauthorized = 20000, // 通用未授权/签名错误
AccessTokenExpired = 20001, // 前端捕获 -> 用 RefreshToken 换新 AccessToken (静默)
RefreshTokenExpired = 20002, // 前端捕获 -> 强制退出到登录页
PermissionDenied = 20003,
}
/// 全局应用错误枚举
#[derive(Error, Debug)]
pub enum AppError {
// --- 基础设施错误 ---
#[error("Database error: {0}")]
DbError(String), // 实际建议: DbError(#[from] sqlx::Error)
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
// --- 业务逻辑错误 ---
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Invalid parameters: {0}")]
InvalidParam(String),
// --- 认证相关 ---
#[error("Authentication failed")]
AuthError,
#[error("Access token expired")]
AccessTokenExpired,
#[error("Refresh token expired")]
RefreshTokenExpired,
#[error("Permission denied: {0}")]
PermissionDenied(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 方便排查
}
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::PermissionDenied(_) => StatusCode::FORBIDDEN,
}
}
// 映射业务状态码 (给前端代码看)
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, // 关键
AppError::PermissionDenied(_) => BizCode::PermissionDenied,
}
}
}
// 核心:实现 Axum 的 IntoResponse
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();
// 尝试获取当前的 Trace ID (如果有 telemetry 功能)
// 这里只是简单的占位,实际需要配合 opentelemetry 获取 span id
let trace_id = None;
// 1. 自动记录错误日志 (利用 tracing)
// 这样业务代码里只需要 return Err(...),不需要手动 error!(...)
#[cfg(feature = "telemetry")]
{
tracing::error!(
%status,
code = ?biz_code,
error = %message,
"Request failed"
);
}
// 2. 构建 JSON
let body = Json(ErrorResponse {
code: biz_code as u32,
message,
trace_id,
});
(status, body).into_response()
}
}