feat(mod): add response
This commit is contained in:
29
src/error.rs
29
src/error.rs
@@ -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 获取
|
||||
});
|
||||
|
||||
|
||||
12
src/lib.rs
12
src/lib.rs
@@ -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
112
src/response.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user