use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::Serialize; /// 统一错误响应格式 #[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: String, pub message: String, #[serde(skip_serializing_if = "Option::is_none")] pub details: Option, } /// 平台级错误类型 #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("资源未找到: {0}")] NotFound(String), #[error("验证失败: {0}")] Validation(String), #[error("未授权")] Unauthorized, #[error("禁止访问: {0}")] Forbidden(String), #[error("冲突: {0}")] Conflict(String), #[error("版本冲突: 数据已被其他操作修改,请刷新后重试")] VersionMismatch, #[error("请求过于频繁,请稍后重试")] TooManyRequests, #[error("内部错误: {0}")] Internal(String), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match &self { AppError::NotFound(_) => (StatusCode::NOT_FOUND, self.to_string()), AppError::Validation(_) => (StatusCode::BAD_REQUEST, self.to_string()), AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "未授权".to_string()), AppError::Forbidden(_) => (StatusCode::FORBIDDEN, self.to_string()), AppError::Conflict(_) => (StatusCode::CONFLICT, self.to_string()), AppError::VersionMismatch => (StatusCode::CONFLICT, self.to_string()), AppError::TooManyRequests => (StatusCode::TOO_MANY_REQUESTS, self.to_string()), AppError::Internal(msg) => { tracing::error!("Internal error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, "内部错误".to_string()) } }; let body = ErrorResponse { error: status.canonical_reason().unwrap_or("Error").to_string(), message, details: None, }; (status, Json(body)).into_response() } } impl From for AppError { fn from(err: anyhow::Error) -> Self { AppError::Internal(err.to_string()) } } impl From for AppError { fn from(err: sea_orm::DbErr) -> Self { match err { sea_orm::DbErr::RecordNotFound(msg) => AppError::NotFound(msg), sea_orm::DbErr::Query(sea_orm::RuntimeErr::SqlxError(e)) if e.to_string().contains("duplicate key") => { AppError::Conflict("记录已存在".to_string()) } _ => AppError::Internal(err.to_string()), } } } pub type AppResult = Result; /// 检查乐观锁版本是否匹配。 /// /// 返回下一个版本号(actual + 1),或 VersionMismatch 错误。 pub fn check_version(expected: i32, actual: i32) -> AppResult { if expected == actual { Ok(actual + 1) } else { Err(AppError::VersionMismatch) } } #[cfg(test)] mod tests { use super::*; #[test] fn check_version_ok() { assert_eq!(check_version(1, 1).unwrap(), 2); assert_eq!(check_version(5, 5).unwrap(), 6); } #[test] fn check_version_mismatch() { let result = check_version(1, 2); assert!(result.is_err()); match result.unwrap_err() { AppError::VersionMismatch => {} other => panic!("Expected VersionMismatch, got {:?}", other), } } #[test] fn db_err_record_not_found_maps_to_not_found() { let err = sea_orm::DbErr::RecordNotFound("test".to_string()); let app_err: AppError = err.into(); match app_err { AppError::NotFound(msg) => assert_eq!(msg, "test"), other => panic!("Expected NotFound, got {:?}", other), } } #[test] fn db_err_generic_maps_to_internal() { let db_err = sea_orm::DbErr::Custom("some error".to_string()); let app_err: AppError = db_err.into(); match app_err { AppError::Internal(msg) => assert!(msg.contains("some error")), other => panic!("Expected Internal, got {:?}", other), } } #[test] fn app_error_into_response_status_codes() { // NotFound -> 404 let resp = AppError::NotFound("test".to_string()).into_response(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); // Validation -> 400 let resp = AppError::Validation("bad input".to_string()).into_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); // Unauthorized -> 401 let resp = AppError::Unauthorized.into_response(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); // Forbidden -> 403 let resp = AppError::Forbidden("no access".to_string()).into_response(); assert_eq!(resp.status(), StatusCode::FORBIDDEN); // VersionMismatch -> 409 let resp = AppError::VersionMismatch.into_response(); assert_eq!(resp.status(), StatusCode::CONFLICT); // TooManyRequests -> 429 let resp = AppError::TooManyRequests.into_response(); assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS); // Internal -> 500 let resp = AppError::Internal("oops".to_string()).into_response(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn app_error_internal_hides_details_from_response() { // Internal errors should map to 500 with a generic message let resp = AppError::Internal("sensitive db error detail".to_string()).into_response(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } #[test] fn anyhow_error_maps_to_internal() { let err: AppError = anyhow::anyhow!("something went wrong").into(); match err { AppError::Internal(msg) => assert_eq!(msg, "something went wrong"), other => panic!("Expected Internal, got {:?}", other), } } }