Skip to main content

tdm_server_rust/error/
app_error.rs

1//! 业务异常类型 (Application Error)
2//!
3//! 对齐 Java `TdmBusinessException` / `LoginExpiredException` 的异常体系。
4//! 通过 [`IntoResponse`] 实现自动转换为 JSON 或纯文本 HTTP 响应。
5//!
6//! # 示例
7//!
8//! ```rust,ignore
9//! use tdm_server_rust::error::{AppError, ApiResult};
10//!
11//! fn validate_name(name: &str) -> ApiResult<()> {
12//!     if name.is_empty() {
13//!         return Err(AppError::business("名称不能为空喵!"));
14//!     }
15//!     Ok(())
16//! }
17//!
18//! // unique 便捷构造适用于唯一约束冲突
19//! fn check_duplicate(name: &str) -> ApiResult<()> {
20//!     Err(AppError::unique("用户名"))  // code=1002
21//! }
22//! ```
23
24use crate::common::{error_code::ErrorCode, result::ResultBody};
25use crate::utils::{error_log, fast_json};
26use axum::{
27    http::{header, StatusCode},
28    response::{IntoResponse, Response},
29};
30
31/// 应用层统一错误类型
32///
33/// 封装所有业务异常,通过 [`IntoResponse`] 自动转换为 HTTP 响应。
34///
35/// # 错误变体
36///
37/// | 变体 | HTTP 状态 | 响应格式 | 说明 |
38/// |------|-----------|----------|------|
39/// | `Business` | 200 | JSON `{code, msg}` | 业务逻辑错误 |
40/// | `LoginExpired` | 200 | JSON `{code:401, msg}` | Token 过期或未登录 |
41/// | `DownloadUnAuth` | 403 | 纯文本 | 下载权限不足 |
42/// | `DownloadFailed` | 500 | 纯文本 | 文件不存在或下载失败 |
43/// | `Oss` | 200 | JSON | OSS 对象存储异常 |
44/// | `Database` | 200 | JSON | 数据库查询/写入失败 |
45/// | `Internal` | 200 | JSON | 内部未知错误 |
46///
47/// # Panics
48///
49/// 本类型不 panic。所有变体均通过 [`IntoResponse`] 优雅降级:
50///
51/// - `Database` / `Internal` 会将完整错误栈输出到 `tracing::error!` 日志
52/// - `Download*` 返回纯文本以避免浏览器 blob 下载保存 JSON
53/// - JSON 序列化失败时有 fallback 硬编码 JSON 字符串
54///
55/// # 从 sqlx::Error 转换
56///
57/// ```rust,ignore
58/// // Repository 层可直接使用 ? 传播数据库错误
59/// let row = sqlx::query("SELECT ...").fetch_one(&pool).await?;
60/// // sqlx::Error 自动转为 AppError::Database
61/// ```
62#[derive(Debug, Clone)]
63pub enum AppError {
64    /// 业务异常(带码)
65    Business { code: i32, msg: String },
66    /// 登录过期
67    LoginExpired { msg: String },
68    /// 下载无权限(返回纯文本 403)
69    DownloadUnAuth { msg: String },
70    /// 下载失败(文件不存在等,返回纯文本 500,避免 blob 客户端保存 JSON)
71    DownloadFailed { msg: String },
72    /// OSS 异常
73    Oss { code: Option<i32>, msg: String },
74    /// 数据库错误
75    Database(String),
76    /// 内部错误
77    Internal(String),
78}
79
80impl AppError {
81    /// 构造通用业务错误(错误码 500)。
82    ///
83    /// # 返回值
84    ///
85    /// 返回 [`AppError::Business`] 变体,`code=500`。
86    pub fn business(msg: impl Into<String>) -> Self {
87        Self::Business {
88            code: ErrorCode::SYSTEM_ERROR,
89            msg: msg.into(),
90        }
91    }
92
93    /// 构造指定错误码的业务错误。
94    ///
95    /// # 参数
96    ///
97    /// - `code`: 业务错误码,参见 [`ErrorCode`] 常量
98    /// - `msg`: 错误描述
99    ///
100    /// # 返回值
101    ///
102    /// 返回 [`AppError::Business`] 变体。
103    pub fn business_code(code: i32, msg: impl Into<String>) -> Self {
104        Self::Business {
105            code,
106            msg: msg.into(),
107        }
108    }
109
110    /// 构造唯一约束冲突错误(错误码 1002)。
111    ///
112    /// 消息格式:`"不允许重复的{field}喵!"`
113    ///
114    /// # 返回值
115    ///
116    /// 返回 [`AppError::Business`] 变体,`code=1002`。
117    pub fn unique(field: &str) -> Self {
118        Self::Business {
119            code: ErrorCode::UNIQUE_VIOLATION,
120            msg: format!("不允许重复的{field}喵!"),
121        }
122    }
123
124    /// 构造登录过期错误(错误码 401)。
125    ///
126    /// # 返回值
127    ///
128    /// 返回 [`AppError::LoginExpired`] 变体,HTTP 响应 `{"code":401,"msg":"..."}`。
129    pub fn login_expired(msg: impl Into<String>) -> Self {
130        Self::LoginExpired { msg: msg.into() }
131    }
132
133    /// 构造下载无权限错误(HTTP 403 纯文本)。
134    ///
135    /// 返回纯文本而非 JSON,避免浏览器 blob 下载保存错误 JSON。
136    ///
137    /// # 返回值
138    ///
139    /// 返回 [`AppError::DownloadUnAuth`] 变体。
140    pub fn download_unauth(msg: impl Into<String>) -> Self {
141        Self::DownloadUnAuth { msg: msg.into() }
142    }
143
144    /// 构造下载失败错误(HTTP 500 纯文本)。
145    ///
146    /// 当 legacy 下载和 OSS 下载均不可用时调用。
147    /// 返回纯文本以避免浏览器 blob 下载保存 JSON。
148    ///
149    /// # 返回值
150    ///
151    /// 返回 [`AppError::DownloadFailed`] 变体。
152    pub fn download_failed(msg: impl Into<String>) -> Self {
153        Self::DownloadFailed { msg: msg.into() }
154    }
155}
156
157/// 自动将 `sqlx::Error` 转换为 [`AppError::Database`]。
158///
159/// 允许 Repository 层直接使用 `?` 传播数据库错误:
160///
161/// ```rust,ignore
162/// let row = sqlx::query("SELECT ...").fetch_one(&pool).await?;
163/// ```
164impl From<sqlx::Error> for AppError {
165    fn from(e: sqlx::Error) -> Self {
166        AppError::Database(e.to_string())
167    }
168}
169
170/// 将 [`AppError`] 转为 HTTP 响应。
171///
172/// # 转换规则
173///
174/// | 变体 | Content-Type | HTTP 状态 |
175/// |------|-------------|-----------|
176/// | `Business` / `LoginExpired` / `Oss` | `application/json;charset=UTF-8` | 200 |
177/// | `DownloadUnAuth` | `text/plain` | 403 |
178/// | `DownloadFailed` | `text/plain` | 500 |
179/// | `Database` / `Internal` | `application/json;charset=UTF-8` | 200 |
180///
181/// # Errors
182///
183/// JSON 序列化失败时有硬编码 fallback,不会 panic。
184impl IntoResponse for AppError {
185    fn into_response(self) -> Response {
186        match self {
187            AppError::DownloadUnAuth { msg } => {
188                error_log::log_app_error("download_unauth", 403, &msg, None);
189                crate::common::result::download_forbidden(msg)
190            }
191            AppError::DownloadFailed { msg } => {
192                error_log::log_app_error("download_failed", 500, &msg, None);
193                crate::common::result::download_failed(msg)
194            }
195            AppError::Business { code, msg } => {
196                if code != ErrorCode::SUCCESS {
197                    error_log::log_app_error("business", code, &msg, None);
198                }
199                let body = ResultBody::<()> {
200                    code,
201                    msg,
202                    data: None,
203                };
204                (
205                    StatusCode::OK,
206                    [(header::CONTENT_TYPE, "application/json;charset=UTF-8")],
207                    fast_json::to_vec(&body).unwrap_or_else(|e| {
208                        format!(r#"{{"code":500,"msg":"JSON 序列化失败: {e}","data":null}}"#)
209                            .into_bytes()
210                    }),
211                )
212                    .into_response()
213            }
214            AppError::LoginExpired { msg } => {
215                error_log::log_app_error("login_expired", ErrorCode::LOGIN_REQUIRED, &msg, None);
216                let body = ResultBody::<()> {
217                    code: ErrorCode::LOGIN_REQUIRED,
218                    msg,
219                    data: None,
220                };
221                (
222                    StatusCode::OK,
223                    [(header::CONTENT_TYPE, "application/json;charset=UTF-8")],
224                    fast_json::to_vec(&body).unwrap_or_else(|e| {
225                        format!(r#"{{"code":500,"msg":"JSON 序列化失败: {e}","data":null}}"#)
226                            .into_bytes()
227                    }),
228                )
229                    .into_response()
230            }
231            AppError::Oss { code, msg } => {
232                let c = code.unwrap_or(ErrorCode::SYSTEM_ERROR);
233                error_log::log_app_error("oss", c, &msg, None);
234                crate::common::result::json_response(&ResultBody::<()> {
235                    code: c,
236                    msg,
237                    data: None,
238                })
239            }
240            AppError::Database(e) | AppError::Internal(e) => {
241                error_log::log_app_error("internal", ErrorCode::SYSTEM_ERROR, "系统异常", Some(&e));
242                tracing::error!("系统异常: {e}");
243                let msg = format!(
244                    "操作失败喵……请截图本错误信息并联系Gum979喵!错误:\n{e}\n错误原因:"
245                );
246                crate::common::result::json_response(&ResultBody::<()> {
247                    code: ErrorCode::SYSTEM_ERROR,
248                    msg,
249                    data: None,
250                })
251            }
252        }
253    }
254}
255
256/// Handler 层便捷 Result 别名。
257///
258/// 所有 Axum handler 的推荐返回类型。
259///
260/// # 示例
261///
262/// ```rust,ignore
263/// use tdm_server_rust::error::ApiResult;
264/// use axum::Json;
265///
266/// async fn my_handler() -> ApiResult<Json<MyData>> {
267///     let data = fetch_data().await?;  // ? 自动将 AppError 转为 HTTP 响应
268///     Ok(Json(data))
269/// }
270/// ```
271///
272/// # 错误传播
273///
274/// 当返回 `Err(AppError::LoginExpired { .. })` 时,
275/// axum 自动调用 [`AppError::into_response()`] 生成 401 JSON 响应。
276pub type ApiResult<T> = Result<T, AppError>;