feat(mod): add response

This commit is contained in:
2026-01-29 18:09:58 +08:00
parent 54058478c4
commit 4db955113c
9 changed files with 379 additions and 25 deletions

View File

@@ -46,6 +46,9 @@ pub enum BizCode {
#[derive(Error, Debug)]
pub enum AppError {
// --- 基础设施错误 ---
#[error("Config error: {0}")]
ConfigError(String),
#[cfg(feature = "with-sqlx")]
#[error("Database error: {0}")]
DbError(sqlx::Error),
@@ -80,6 +83,9 @@ pub enum AppError {
#[error("Authentication failed: {0}")]
AuthError(String), // 通用认证失败 (签名错误、格式错误)
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Missing authorization header")]
MissingAuthHeader,
@@ -111,6 +117,9 @@ pub enum AppError {
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Method not allowed")]
MethodNotAllowed,
// ======================================================
// 4. 业务逻辑层 (Business Logic)
// ======================================================
@@ -127,6 +136,8 @@ pub struct ErrorResponse {
pub code: u32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>, // 可选:返回 trace_id 方便排查
}
@@ -157,6 +168,7 @@ impl AppError {
// 映射 HTTP 状态码 (给网关/浏览器看)
fn http_status(&self) -> StatusCode {
match self {
AppError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
#[cfg(feature = "with-sqlx")]
AppError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR,
#[cfg(feature = "with-redis")]
@@ -169,6 +181,7 @@ impl AppError {
| AppError::SerdeError(_) => StatusCode::INTERNAL_SERVER_ERROR,
// 401 Unauthorized
AppError::AuthError(_)
| AppError::InvalidCredentials
| AppError::MissingAuthHeader
| AppError::AccessTokenExpired
| AppError::RefreshTokenExpired
@@ -186,6 +199,8 @@ impl AppError {
// 429 Too Many Requests
AppError::RateLimitExceeded => StatusCode::TOO_MANY_REQUESTS,
AppError::MethodNotAllowed => StatusCode::METHOD_NOT_ALLOWED,
// 400 Bad Request (默认)
#[cfg(feature = "with-validator")]
AppError::ValidationError(_) => StatusCode::BAD_REQUEST,
@@ -197,6 +212,7 @@ impl AppError {
fn biz_code(&self) -> BizCode {
match self {
// Infra
AppError::ConfigError(_) => BizCode::ConfigError,
#[cfg(feature = "with-sqlx")]
AppError::DbError(_) => BizCode::DbError,
#[cfg(feature = "with-redis")]
@@ -209,6 +225,7 @@ impl AppError {
// Auth
AppError::AuthError(_) => BizCode::Unauthorized,
AppError::InvalidCredentials => BizCode::InvalidCredentials,
AppError::MissingAuthHeader => BizCode::MissingHeader,
AppError::AccessTokenExpired => BizCode::AccessTokenExpired,
AppError::RefreshTokenExpired => BizCode::RefreshTokenExpired,
@@ -221,12 +238,22 @@ impl AppError {
AppError::BadRequest(_) => BizCode::BadRequest,
#[cfg(feature = "with-validator")]
AppError::ValidationError(_) => BizCode::ValidationError,
AppError::MethodNotAllowed => BizCode::MethodNotAllowed,
// Biz
AppError::RateLimitExceeded => BizCode::RateLimitExceeded,
AppError::BusinessLogicError(_) => BizCode::PreconditionFailed,
}
}
fn details(&self) -> Option<serde_json::Value> {
match self {
AppError::BadRequest(details) => Some(serde_json::Value::String(details.clone())),
#[cfg(feature = "with-validator")]
AppError::ValidationError(details) => Some(serde_json::Value::String(details.clone())),
_ => None,
}
}
}
// 核心:实现 Axum 的 IntoResponse
@@ -234,6 +261,7 @@ impl IntoResponse for AppError {
fn into_response(self) -> Response {
let status = self.http_status();
let biz_code = self.biz_code();
let details = self.details();
// 生产环境通常不把详细的 DB 报错返回给前端,防止泄露表结构
// 但这里为了演示,我们先直接使用 self.to_string()
// 建议:在生产环境针对 DbError/AnyhowError 返回统一的 "Internal Server Error"
@@ -293,6 +321,7 @@ impl IntoResponse for AppError {
let body = Json(ErrorResponse {
code: biz_code as u32,
message,
details,
trace_id: None, // 实际项目中从 tracing context 获取
});

View File

@@ -1,11 +1,17 @@
// 只有开启了 'error' feature 才会编译 error 模块
#[cfg(feature = "error")]
// 只有开启了 'response' feature 才会编译响应模块
#[cfg(feature = "response")]
pub mod error;
#[cfg(feature = "response")]
pub mod response;
// 只有开启了 'telemetry' feature 才会编译 telemetry 模块
#[cfg(feature = "telemetry")]
pub mod telemetry;
// 方便外部直接 use common_lib::AppError;
#[cfg(feature = "error")]
#[cfg(feature = "response")]
pub use error::{AppError, BizCode};
#[cfg(feature = "response")]
pub use response::{AppResponse, SuccessResponse};

112
src/response.rs Normal file
View File

@@ -0,0 +1,112 @@
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
use crate::error::BizCode;
#[derive(Serialize)]
pub struct SuccessResponse<T> {
pub code: u32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
}
pub enum AppResponse<T> {
Ok(SuccessResponse<T>),
Created(SuccessResponse<T>),
Accepted(SuccessResponse<T>),
}
impl<T> AppResponse<T> {
pub fn ok(data: T) -> Self {
Self::Ok(SuccessResponse {
code: BizCode::Success as u32,
message: "Success".into(),
data: Some(data),
trace_id: None,
})
}
pub fn created(data: T) -> Self {
Self::Created(SuccessResponse {
code: BizCode::Success as u32,
message: "Created".into(),
data: Some(data),
trace_id: None,
})
}
pub fn accepted(data: T) -> Self {
Self::Accepted(SuccessResponse {
code: BizCode::Success as u32,
message: "Accepted".into(),
data: Some(data),
trace_id: None,
})
}
pub fn message(mut self, message: impl Into<String>) -> Self {
let message = message.into();
match &mut self {
AppResponse::Ok(body) | AppResponse::Created(body) | AppResponse::Accepted(body) => {
body.message = message;
}
}
self
}
pub fn trace_id(mut self, trace_id: impl Into<String>) -> Self {
let trace_id = trace_id.into();
match &mut self {
AppResponse::Ok(body) | AppResponse::Created(body) | AppResponse::Accepted(body) => {
body.trace_id = Some(trace_id);
}
}
self
}
}
impl AppResponse<()> {
pub fn ok_empty() -> Self {
Self::Ok(SuccessResponse {
code: BizCode::Success as u32,
message: "Success".into(),
data: None,
trace_id: None,
})
}
pub fn created_empty() -> Self {
Self::Created(SuccessResponse {
code: BizCode::Success as u32,
message: "Created".into(),
data: None,
trace_id: None,
})
}
pub fn accepted_empty() -> Self {
Self::Accepted(SuccessResponse {
code: BizCode::Success as u32,
message: "Accepted".into(),
data: None,
trace_id: None,
})
}
}
impl<T: Serialize> IntoResponse for AppResponse<T> {
fn into_response(self) -> Response {
match self {
AppResponse::Ok(body) => (StatusCode::OK, Json(body)).into_response(),
AppResponse::Created(body) => (StatusCode::CREATED, Json(body)).into_response(),
AppResponse::Accepted(body) => (StatusCode::ACCEPTED, Json(body)).into_response(),
}
}
}