新增以下文档文件: - PROJECT_OVERVIEW.md 项目总览文档 - BACKEND_ARCHITECTURE.md 后端架构文档 - FRONTEND_ARCHITECTURE.md 前端架构文档 - FLOW_ENGINE.md 流程引擎文档 - SERVICES.md 服务层文档 - ERROR_HANDLING.md 错误处理模块文档 文档内容涵盖项目整体介绍、技术架构、核心模块设计和实现细节
878 lines
28 KiB
Markdown
878 lines
28 KiB
Markdown
# UdminAI 错误处理模块文档
|
||
|
||
## 概述
|
||
|
||
UdminAI 项目的错误处理模块提供了统一的错误定义、处理和响应机制。该模块基于 Rust 的 `Result` 类型和 `thiserror` 库构建,确保错误信息的一致性、可追踪性和用户友好性。
|
||
|
||
## 设计原则
|
||
|
||
### 核心理念
|
||
|
||
- **统一性**: 所有模块使用统一的错误类型
|
||
- **可追踪性**: 错误包含足够的上下文信息
|
||
- **用户友好**: 面向用户的错误消息清晰易懂
|
||
- **开发友好**: 面向开发者的错误信息详细准确
|
||
- **类型安全**: 编译时错误类型检查
|
||
|
||
### 错误分层
|
||
|
||
1. **应用层错误**: 业务逻辑错误
|
||
2. **服务层错误**: 服务调用错误
|
||
3. **数据层错误**: 数据库和缓存错误
|
||
4. **网络层错误**: HTTP 和网络通信错误
|
||
5. **系统层错误**: 系统资源和配置错误
|
||
|
||
## 错误类型定义 (error.rs)
|
||
|
||
### 主要错误类型
|
||
|
||
```rust
|
||
use axum::{
|
||
http::StatusCode,
|
||
response::{IntoResponse, Response},
|
||
Json,
|
||
};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::fmt;
|
||
use thiserror::Error;
|
||
use tracing::error;
|
||
|
||
/// 应用主错误类型
|
||
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
|
||
pub enum AppError {
|
||
// 认证和授权错误
|
||
#[error("认证失败: {message}")]
|
||
AuthenticationFailed { message: String },
|
||
|
||
#[error("授权失败: {message}")]
|
||
AuthorizationFailed { message: String },
|
||
|
||
#[error("令牌无效: {message}")]
|
||
InvalidToken { message: String },
|
||
|
||
#[error("令牌已过期")]
|
||
TokenExpired,
|
||
|
||
// 验证错误
|
||
#[error("验证失败: {field} - {message}")]
|
||
ValidationFailed { field: String, message: String },
|
||
|
||
#[error("请求参数无效: {message}")]
|
||
InvalidRequest { message: String },
|
||
|
||
#[error("必需字段缺失: {field}")]
|
||
MissingField { field: String },
|
||
|
||
// 资源错误
|
||
#[error("资源未找到: {resource}")]
|
||
NotFound(String),
|
||
|
||
#[error("资源已存在: {resource}")]
|
||
AlreadyExists(String),
|
||
|
||
#[error("资源冲突: {message}")]
|
||
Conflict { message: String },
|
||
|
||
// 业务逻辑错误
|
||
#[error("业务规则违反: {message}")]
|
||
BusinessRuleViolation { message: String },
|
||
|
||
#[error("操作不被允许: {message}")]
|
||
OperationNotAllowed { message: String },
|
||
|
||
#[error("状态无效: 当前状态 {current}, 期望状态 {expected}")]
|
||
InvalidState { current: String, expected: String },
|
||
|
||
// 数据库错误
|
||
#[error("数据库错误: {0}")]
|
||
DatabaseError(String),
|
||
|
||
#[error("数据库连接失败: {message}")]
|
||
DatabaseConnectionFailed { message: String },
|
||
|
||
#[error("事务失败: {message}")]
|
||
TransactionFailed { message: String },
|
||
|
||
// 缓存错误
|
||
#[error("缓存错误: {0}")]
|
||
CacheError(String),
|
||
|
||
#[error("缓存连接失败: {message}")]
|
||
CacheConnectionFailed { message: String },
|
||
|
||
// 网络和外部服务错误
|
||
#[error("网络错误: {message}")]
|
||
NetworkError { message: String },
|
||
|
||
#[error("外部服务错误: {service} - {message}")]
|
||
ExternalServiceError { service: String, message: String },
|
||
|
||
#[error("HTTP 请求失败: {status} - {message}")]
|
||
HttpRequestFailed { status: u16, message: String },
|
||
|
||
// 文件和 I/O 错误
|
||
#[error("文件操作失败: {message}")]
|
||
FileOperationFailed { message: String },
|
||
|
||
#[error("文件未找到: {path}")]
|
||
FileNotFound { path: String },
|
||
|
||
#[error("文件权限不足: {path}")]
|
||
FilePermissionDenied { path: String },
|
||
|
||
// 配置和环境错误
|
||
#[error("配置错误: {message}")]
|
||
ConfigurationError { message: String },
|
||
|
||
#[error("环境变量缺失: {variable}")]
|
||
MissingEnvironmentVariable { variable: String },
|
||
|
||
// 序列化和反序列化错误
|
||
#[error("序列化失败: {message}")]
|
||
SerializationFailed { message: String },
|
||
|
||
#[error("反序列化失败: {message}")]
|
||
DeserializationFailed { message: String },
|
||
|
||
#[error("JSON 格式错误: {message}")]
|
||
JsonFormatError { message: String },
|
||
|
||
// 流程引擎错误
|
||
#[error("流程执行失败: {flow_id} - {message}")]
|
||
FlowExecutionFailed { flow_id: String, message: String },
|
||
|
||
#[error("流程解析失败: {message}")]
|
||
FlowParsingFailed { message: String },
|
||
|
||
#[error("节点执行失败: {node_id} - {message}")]
|
||
NodeExecutionFailed { node_id: String, message: String },
|
||
|
||
// 调度任务错误
|
||
#[error("任务调度失败: {job_id} - {message}")]
|
||
JobSchedulingFailed { job_id: String, message: String },
|
||
|
||
#[error("Cron 表达式无效: {expression}")]
|
||
InvalidCronExpression { expression: String },
|
||
|
||
#[error("任务执行超时: {job_id}")]
|
||
JobExecutionTimeout { job_id: String },
|
||
|
||
// 系统错误
|
||
#[error("内部服务器错误: {message}")]
|
||
InternalServerError { message: String },
|
||
|
||
#[error("服务不可用: {message}")]
|
||
ServiceUnavailable { message: String },
|
||
|
||
#[error("请求超时: {message}")]
|
||
RequestTimeout { message: String },
|
||
|
||
#[error("资源耗尽: {resource}")]
|
||
ResourceExhausted { resource: String },
|
||
|
||
// 限流和安全错误
|
||
#[error("请求频率过高: {message}")]
|
||
RateLimitExceeded { message: String },
|
||
|
||
#[error("请求体过大: 当前大小 {current}, 最大允许 {max}")]
|
||
PayloadTooLarge { current: usize, max: usize },
|
||
|
||
#[error("不支持的媒体类型: {media_type}")]
|
||
UnsupportedMediaType { media_type: String },
|
||
}
|
||
|
||
/// 错误响应结构
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ErrorResponse {
|
||
pub error: ErrorDetail,
|
||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||
pub request_id: Option<String>,
|
||
}
|
||
|
||
/// 错误详情
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ErrorDetail {
|
||
pub code: String,
|
||
pub message: String,
|
||
pub details: Option<serde_json::Value>,
|
||
pub field: Option<String>,
|
||
}
|
||
|
||
impl AppError {
|
||
/// 获取错误代码
|
||
pub fn error_code(&self) -> &'static str {
|
||
match self {
|
||
// 认证和授权
|
||
AppError::AuthenticationFailed { .. } => "AUTH_FAILED",
|
||
AppError::AuthorizationFailed { .. } => "AUTHORIZATION_FAILED",
|
||
AppError::InvalidToken { .. } => "INVALID_TOKEN",
|
||
AppError::TokenExpired => "TOKEN_EXPIRED",
|
||
|
||
// 验证
|
||
AppError::ValidationFailed { .. } => "VALIDATION_FAILED",
|
||
AppError::InvalidRequest { .. } => "INVALID_REQUEST",
|
||
AppError::MissingField { .. } => "MISSING_FIELD",
|
||
|
||
// 资源
|
||
AppError::NotFound(_) => "NOT_FOUND",
|
||
AppError::AlreadyExists(_) => "ALREADY_EXISTS",
|
||
AppError::Conflict { .. } => "CONFLICT",
|
||
|
||
// 业务逻辑
|
||
AppError::BusinessRuleViolation { .. } => "BUSINESS_RULE_VIOLATION",
|
||
AppError::OperationNotAllowed { .. } => "OPERATION_NOT_ALLOWED",
|
||
AppError::InvalidState { .. } => "INVALID_STATE",
|
||
|
||
// 数据库
|
||
AppError::DatabaseError(_) => "DATABASE_ERROR",
|
||
AppError::DatabaseConnectionFailed { .. } => "DATABASE_CONNECTION_FAILED",
|
||
AppError::TransactionFailed { .. } => "TRANSACTION_FAILED",
|
||
|
||
// 缓存
|
||
AppError::CacheError(_) => "CACHE_ERROR",
|
||
AppError::CacheConnectionFailed { .. } => "CACHE_CONNECTION_FAILED",
|
||
|
||
// 网络
|
||
AppError::NetworkError { .. } => "NETWORK_ERROR",
|
||
AppError::ExternalServiceError { .. } => "EXTERNAL_SERVICE_ERROR",
|
||
AppError::HttpRequestFailed { .. } => "HTTP_REQUEST_FAILED",
|
||
|
||
// 文件
|
||
AppError::FileOperationFailed { .. } => "FILE_OPERATION_FAILED",
|
||
AppError::FileNotFound { .. } => "FILE_NOT_FOUND",
|
||
AppError::FilePermissionDenied { .. } => "FILE_PERMISSION_DENIED",
|
||
|
||
// 配置
|
||
AppError::ConfigurationError { .. } => "CONFIGURATION_ERROR",
|
||
AppError::MissingEnvironmentVariable { .. } => "MISSING_ENV_VAR",
|
||
|
||
// 序列化
|
||
AppError::SerializationFailed { .. } => "SERIALIZATION_FAILED",
|
||
AppError::DeserializationFailed { .. } => "DESERIALIZATION_FAILED",
|
||
AppError::JsonFormatError { .. } => "JSON_FORMAT_ERROR",
|
||
|
||
// 流程引擎
|
||
AppError::FlowExecutionFailed { .. } => "FLOW_EXECUTION_FAILED",
|
||
AppError::FlowParsingFailed { .. } => "FLOW_PARSING_FAILED",
|
||
AppError::NodeExecutionFailed { .. } => "NODE_EXECUTION_FAILED",
|
||
|
||
// 调度任务
|
||
AppError::JobSchedulingFailed { .. } => "JOB_SCHEDULING_FAILED",
|
||
AppError::InvalidCronExpression { .. } => "INVALID_CRON_EXPRESSION",
|
||
AppError::JobExecutionTimeout { .. } => "JOB_EXECUTION_TIMEOUT",
|
||
|
||
// 系统
|
||
AppError::InternalServerError { .. } => "INTERNAL_SERVER_ERROR",
|
||
AppError::ServiceUnavailable { .. } => "SERVICE_UNAVAILABLE",
|
||
AppError::RequestTimeout { .. } => "REQUEST_TIMEOUT",
|
||
AppError::ResourceExhausted { .. } => "RESOURCE_EXHAUSTED",
|
||
|
||
// 限流和安全
|
||
AppError::RateLimitExceeded { .. } => "RATE_LIMIT_EXCEEDED",
|
||
AppError::PayloadTooLarge { .. } => "PAYLOAD_TOO_LARGE",
|
||
AppError::UnsupportedMediaType { .. } => "UNSUPPORTED_MEDIA_TYPE",
|
||
}
|
||
}
|
||
|
||
/// 获取 HTTP 状态码
|
||
pub fn status_code(&self) -> StatusCode {
|
||
match self {
|
||
// 4xx 客户端错误
|
||
AppError::AuthenticationFailed { .. } => StatusCode::UNAUTHORIZED,
|
||
AppError::AuthorizationFailed { .. } => StatusCode::FORBIDDEN,
|
||
AppError::InvalidToken { .. } => StatusCode::UNAUTHORIZED,
|
||
AppError::TokenExpired => StatusCode::UNAUTHORIZED,
|
||
|
||
AppError::ValidationFailed { .. } => StatusCode::BAD_REQUEST,
|
||
AppError::InvalidRequest { .. } => StatusCode::BAD_REQUEST,
|
||
AppError::MissingField { .. } => StatusCode::BAD_REQUEST,
|
||
|
||
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
||
AppError::AlreadyExists(_) => StatusCode::CONFLICT,
|
||
AppError::Conflict { .. } => StatusCode::CONFLICT,
|
||
|
||
AppError::BusinessRuleViolation { .. } => StatusCode::BAD_REQUEST,
|
||
AppError::OperationNotAllowed { .. } => StatusCode::FORBIDDEN,
|
||
AppError::InvalidState { .. } => StatusCode::BAD_REQUEST,
|
||
|
||
AppError::FileNotFound { .. } => StatusCode::NOT_FOUND,
|
||
AppError::FilePermissionDenied { .. } => StatusCode::FORBIDDEN,
|
||
|
||
AppError::JsonFormatError { .. } => StatusCode::BAD_REQUEST,
|
||
AppError::InvalidCronExpression { .. } => StatusCode::BAD_REQUEST,
|
||
|
||
AppError::RateLimitExceeded { .. } => StatusCode::TOO_MANY_REQUESTS,
|
||
AppError::PayloadTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
|
||
AppError::UnsupportedMediaType { .. } => StatusCode::UNSUPPORTED_MEDIA_TYPE,
|
||
|
||
// 5xx 服务器错误
|
||
AppError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::DatabaseConnectionFailed { .. } => StatusCode::SERVICE_UNAVAILABLE,
|
||
AppError::TransactionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
|
||
AppError::CacheError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::CacheConnectionFailed { .. } => StatusCode::SERVICE_UNAVAILABLE,
|
||
|
||
AppError::NetworkError { .. } => StatusCode::BAD_GATEWAY,
|
||
AppError::ExternalServiceError { .. } => StatusCode::BAD_GATEWAY,
|
||
AppError::HttpRequestFailed { .. } => StatusCode::BAD_GATEWAY,
|
||
|
||
AppError::FileOperationFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
|
||
AppError::ConfigurationError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::MissingEnvironmentVariable { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
|
||
AppError::SerializationFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::DeserializationFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
|
||
AppError::FlowExecutionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::FlowParsingFailed { .. } => StatusCode::BAD_REQUEST,
|
||
AppError::NodeExecutionFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
|
||
AppError::JobSchedulingFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::JobExecutionTimeout { .. } => StatusCode::REQUEST_TIMEOUT,
|
||
|
||
AppError::InternalServerError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||
AppError::ServiceUnavailable { .. } => StatusCode::SERVICE_UNAVAILABLE,
|
||
AppError::RequestTimeout { .. } => StatusCode::REQUEST_TIMEOUT,
|
||
AppError::ResourceExhausted { .. } => StatusCode::SERVICE_UNAVAILABLE,
|
||
}
|
||
}
|
||
|
||
/// 获取错误字段(如果适用)
|
||
pub fn error_field(&self) -> Option<String> {
|
||
match self {
|
||
AppError::ValidationFailed { field, .. } => Some(field.clone()),
|
||
AppError::MissingField { field } => Some(field.clone()),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// 是否为客户端错误
|
||
pub fn is_client_error(&self) -> bool {
|
||
self.status_code().is_client_error()
|
||
}
|
||
|
||
/// 是否为服务器错误
|
||
pub fn is_server_error(&self) -> bool {
|
||
self.status_code().is_server_error()
|
||
}
|
||
|
||
/// 创建错误响应
|
||
pub fn to_error_response(&self, request_id: Option<String>) -> ErrorResponse {
|
||
ErrorResponse {
|
||
error: ErrorDetail {
|
||
code: self.error_code().to_string(),
|
||
message: self.to_string(),
|
||
details: None,
|
||
field: self.error_field(),
|
||
},
|
||
timestamp: chrono::Utc::now(),
|
||
request_id,
|
||
}
|
||
}
|
||
|
||
/// 记录错误日志
|
||
pub fn log_error(&self, request_id: Option<&str>) {
|
||
let level = if self.is_server_error() {
|
||
tracing::Level::ERROR
|
||
} else {
|
||
tracing::Level::WARN
|
||
};
|
||
|
||
match level {
|
||
tracing::Level::ERROR => {
|
||
error!(
|
||
target = "udmin",
|
||
error_code = %self.error_code(),
|
||
error_message = %self,
|
||
request_id = ?request_id,
|
||
"application.error.server"
|
||
);
|
||
}
|
||
_ => {
|
||
tracing::warn!(
|
||
target = "udmin",
|
||
error_code = %self.error_code(),
|
||
error_message = %self,
|
||
request_id = ?request_id,
|
||
"application.error.client"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 实现 IntoResponse,使错误可以直接作为 HTTP 响应返回
|
||
impl IntoResponse for AppError {
|
||
fn into_response(self) -> Response {
|
||
// 从请求上下文获取 request_id(实际实现中可能需要通过中间件传递)
|
||
let request_id = None; // 这里应该从上下文获取
|
||
|
||
// 记录错误日志
|
||
self.log_error(request_id.as_deref());
|
||
|
||
// 创建错误响应
|
||
let error_response = self.to_error_response(request_id);
|
||
let status_code = self.status_code();
|
||
|
||
(status_code, Json(error_response)).into_response()
|
||
}
|
||
}
|
||
|
||
/// 应用结果类型别名
|
||
pub type AppResult<T> = Result<T, AppError>;
|
||
|
||
/// 错误转换实现
|
||
impl From<sea_orm::DbErr> for AppError {
|
||
fn from(err: sea_orm::DbErr) -> Self {
|
||
match err {
|
||
sea_orm::DbErr::RecordNotFound(_) => AppError::NotFound("记录不存在".to_string()),
|
||
sea_orm::DbErr::Custom(msg) => AppError::DatabaseError(msg),
|
||
sea_orm::DbErr::Conn(msg) => AppError::DatabaseConnectionFailed { message: msg },
|
||
sea_orm::DbErr::Exec(msg) => AppError::DatabaseError(msg),
|
||
sea_orm::DbErr::Query(msg) => AppError::DatabaseError(msg),
|
||
_ => AppError::DatabaseError("未知数据库错误".to_string()),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<redis::RedisError> for AppError {
|
||
fn from(err: redis::RedisError) -> Self {
|
||
match err.kind() {
|
||
redis::ErrorKind::IoError => AppError::CacheConnectionFailed {
|
||
message: err.to_string(),
|
||
},
|
||
redis::ErrorKind::AuthenticationFailed => AppError::CacheConnectionFailed {
|
||
message: "Redis 认证失败".to_string(),
|
||
},
|
||
_ => AppError::CacheError(err.to_string()),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<reqwest::Error> for AppError {
|
||
fn from(err: reqwest::Error) -> Self {
|
||
if err.is_timeout() {
|
||
AppError::RequestTimeout {
|
||
message: "HTTP 请求超时".to_string(),
|
||
}
|
||
} else if err.is_connect() {
|
||
AppError::NetworkError {
|
||
message: "网络连接失败".to_string(),
|
||
}
|
||
} else if let Some(status) = err.status() {
|
||
AppError::HttpRequestFailed {
|
||
status: status.as_u16(),
|
||
message: err.to_string(),
|
||
}
|
||
} else {
|
||
AppError::NetworkError {
|
||
message: err.to_string(),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<serde_json::Error> for AppError {
|
||
fn from(err: serde_json::Error) -> Self {
|
||
if err.is_syntax() {
|
||
AppError::JsonFormatError {
|
||
message: "JSON 语法错误".to_string(),
|
||
}
|
||
} else if err.is_data() {
|
||
AppError::DeserializationFailed {
|
||
message: err.to_string(),
|
||
}
|
||
} else {
|
||
AppError::SerializationFailed {
|
||
message: err.to_string(),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<std::io::Error> for AppError {
|
||
fn from(err: std::io::Error) -> Self {
|
||
match err.kind() {
|
||
std::io::ErrorKind::NotFound => AppError::FileNotFound {
|
||
path: "未知路径".to_string(),
|
||
},
|
||
std::io::ErrorKind::PermissionDenied => AppError::FilePermissionDenied {
|
||
path: "未知路径".to_string(),
|
||
},
|
||
_ => AppError::FileOperationFailed {
|
||
message: err.to_string(),
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
impl From<tokio::time::error::Elapsed> for AppError {
|
||
fn from(_: tokio::time::error::Elapsed) -> Self {
|
||
AppError::RequestTimeout {
|
||
message: "操作超时".to_string(),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 错误构建器
|
||
pub struct ErrorBuilder {
|
||
error: AppError,
|
||
}
|
||
|
||
impl ErrorBuilder {
|
||
pub fn new(error: AppError) -> Self {
|
||
Self { error }
|
||
}
|
||
|
||
pub fn with_details(mut self, details: serde_json::Value) -> Self {
|
||
// 这里可以扩展错误以包含更多详情
|
||
self
|
||
}
|
||
|
||
pub fn with_field(mut self, field: String) -> Self {
|
||
// 设置错误字段
|
||
self
|
||
}
|
||
|
||
pub fn build(self) -> AppError {
|
||
self.error
|
||
}
|
||
}
|
||
|
||
/// 错误宏
|
||
#[macro_export]
|
||
macro_rules! app_error {
|
||
($error_type:ident, $($field:ident = $value:expr),*) => {
|
||
AppError::$error_type {
|
||
$($field: $value.into()),*
|
||
}
|
||
};
|
||
}
|
||
|
||
/// 结果扩展 trait
|
||
pub trait ResultExt<T> {
|
||
/// 将错误转换为 AppError
|
||
fn map_app_error<F>(self, f: F) -> AppResult<T>
|
||
where
|
||
F: FnOnce() -> AppError;
|
||
|
||
/// 添加错误上下文
|
||
fn with_context<F>(self, f: F) -> AppResult<T>
|
||
where
|
||
F: FnOnce() -> String;
|
||
}
|
||
|
||
impl<T, E> ResultExt<T> for Result<T, E>
|
||
where
|
||
E: Into<AppError>,
|
||
{
|
||
fn map_app_error<F>(self, f: F) -> AppResult<T>
|
||
where
|
||
F: FnOnce() -> AppError,
|
||
{
|
||
self.map_err(|_| f())
|
||
}
|
||
|
||
fn with_context<F>(self, f: F) -> AppResult<T>
|
||
where
|
||
F: FnOnce() -> String,
|
||
{
|
||
self.map_err(|e| {
|
||
let context = f();
|
||
match e.into() {
|
||
AppError::InternalServerError { message } => AppError::InternalServerError {
|
||
message: format!("{}: {}", context, message),
|
||
},
|
||
other => other,
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_error_codes() {
|
||
let error = AppError::NotFound("用户".to_string());
|
||
assert_eq!(error.error_code(), "NOT_FOUND");
|
||
assert_eq!(error.status_code(), StatusCode::NOT_FOUND);
|
||
}
|
||
|
||
#[test]
|
||
fn test_error_response() {
|
||
let error = AppError::ValidationFailed {
|
||
field: "email".to_string(),
|
||
message: "格式无效".to_string(),
|
||
};
|
||
|
||
let response = error.to_error_response(Some("req-123".to_string()));
|
||
assert_eq!(response.error.code, "VALIDATION_FAILED");
|
||
assert_eq!(response.error.field, Some("email".to_string()));
|
||
assert_eq!(response.request_id, Some("req-123".to_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_error_conversion() {
|
||
let db_error = sea_orm::DbErr::RecordNotFound("test".to_string());
|
||
let app_error: AppError = db_error.into();
|
||
|
||
match app_error {
|
||
AppError::NotFound(_) => assert!(true),
|
||
_ => assert!(false, "Expected NotFound error"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_error_macro() {
|
||
let error = app_error!(ValidationFailed,
|
||
field = "username",
|
||
message = "用户名已存在"
|
||
);
|
||
|
||
match error {
|
||
AppError::ValidationFailed { field, message } => {
|
||
assert_eq!(field, "username");
|
||
assert_eq!(message, "用户名已存在");
|
||
}
|
||
_ => assert!(false, "Expected ValidationFailed error"),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_result_ext() {
|
||
let result: Result<i32, &str> = Err("test error");
|
||
let app_result = result.map_app_error(|| AppError::InternalServerError {
|
||
message: "转换错误".to_string(),
|
||
});
|
||
|
||
assert!(app_result.is_err());
|
||
}
|
||
}
|
||
```
|
||
|
||
## 错误处理策略
|
||
|
||
### 错误传播
|
||
|
||
```rust
|
||
/// 错误传播示例
|
||
pub async fn create_user(req: CreateUserReq) -> AppResult<UserDoc> {
|
||
// 验证请求
|
||
validate_create_user_request(&req)?;
|
||
|
||
// 检查用户是否已存在
|
||
if user_exists(&req.email).await? {
|
||
return Err(AppError::AlreadyExists("用户邮箱已存在".to_string()));
|
||
}
|
||
|
||
// 创建用户
|
||
let user = User::create(req).await
|
||
.with_context(|| "创建用户失败")?;
|
||
|
||
Ok(user.into())
|
||
}
|
||
```
|
||
|
||
### 错误恢复
|
||
|
||
```rust
|
||
/// 错误恢复示例
|
||
pub async fn get_user_with_fallback(id: &str) -> AppResult<UserDoc> {
|
||
// 首先尝试从缓存获取
|
||
match get_user_from_cache(id).await {
|
||
Ok(user) => return Ok(user),
|
||
Err(AppError::CacheError(_)) => {
|
||
// 缓存错误,尝试从数据库获取
|
||
tracing::warn!("缓存获取用户失败,尝试数据库");
|
||
}
|
||
Err(e) => return Err(e),
|
||
}
|
||
|
||
// 从数据库获取
|
||
let user = get_user_from_db(id).await?;
|
||
|
||
// 尝试更新缓存(忽略错误)
|
||
if let Err(e) = set_user_cache(id, &user).await {
|
||
tracing::warn!(error = %e, "更新用户缓存失败");
|
||
}
|
||
|
||
Ok(user)
|
||
}
|
||
```
|
||
|
||
### 错误聚合
|
||
|
||
```rust
|
||
/// 错误聚合示例
|
||
pub struct ValidationErrors {
|
||
pub errors: Vec<AppError>,
|
||
}
|
||
|
||
impl ValidationErrors {
|
||
pub fn new() -> Self {
|
||
Self { errors: Vec::new() }
|
||
}
|
||
|
||
pub fn add(&mut self, error: AppError) {
|
||
self.errors.push(error);
|
||
}
|
||
|
||
pub fn is_empty(&self) -> bool {
|
||
self.errors.is_empty()
|
||
}
|
||
|
||
pub fn into_result(self) -> AppResult<()> {
|
||
if self.errors.is_empty() {
|
||
Ok(())
|
||
} else {
|
||
// 返回第一个错误,或者可以创建一个聚合错误类型
|
||
Err(self.errors.into_iter().next().unwrap())
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn validate_user_data(data: &CreateUserReq) -> AppResult<()> {
|
||
let mut errors = ValidationErrors::new();
|
||
|
||
if data.email.is_empty() {
|
||
errors.add(AppError::MissingField { field: "email".to_string() });
|
||
}
|
||
|
||
if data.password.len() < 8 {
|
||
errors.add(AppError::ValidationFailed {
|
||
field: "password".to_string(),
|
||
message: "密码长度至少8位".to_string(),
|
||
});
|
||
}
|
||
|
||
errors.into_result()
|
||
}
|
||
```
|
||
|
||
## 中间件集成
|
||
|
||
### 错误处理中间件
|
||
|
||
```rust
|
||
use axum::{
|
||
extract::Request,
|
||
middleware::Next,
|
||
response::Response,
|
||
};
|
||
use uuid::Uuid;
|
||
|
||
/// 错误处理中间件
|
||
pub async fn error_handler_middleware(
|
||
mut request: Request,
|
||
next: Next,
|
||
) -> Response {
|
||
// 生成请求 ID
|
||
let request_id = Uuid::new_v4().to_string();
|
||
request.extensions_mut().insert(request_id.clone());
|
||
|
||
// 执行请求
|
||
let response = next.run(request).await;
|
||
|
||
// 如果响应是错误,添加请求 ID
|
||
if response.status().is_client_error() || response.status().is_server_error() {
|
||
// 这里可以修改响应头,添加请求 ID
|
||
tracing::info!(
|
||
target = "udmin",
|
||
request_id = %request_id,
|
||
status = %response.status(),
|
||
"request.completed.error"
|
||
);
|
||
}
|
||
|
||
response
|
||
}
|
||
```
|
||
|
||
## 监控和告警
|
||
|
||
### 错误指标收集
|
||
|
||
```rust
|
||
use prometheus::{Counter, Histogram, Registry};
|
||
use std::sync::Arc;
|
||
|
||
/// 错误指标收集器
|
||
#[derive(Clone)]
|
||
pub struct ErrorMetrics {
|
||
error_counter: Counter,
|
||
error_duration: Histogram,
|
||
}
|
||
|
||
impl ErrorMetrics {
|
||
pub fn new(registry: &Registry) -> Self {
|
||
let error_counter = Counter::new(
|
||
"app_errors_total",
|
||
"Total number of application errors"
|
||
).unwrap();
|
||
|
||
let error_duration = Histogram::new(
|
||
"error_handling_duration_seconds",
|
||
"Time spent handling errors"
|
||
).unwrap();
|
||
|
||
registry.register(Box::new(error_counter.clone())).unwrap();
|
||
registry.register(Box::new(error_duration.clone())).unwrap();
|
||
|
||
Self {
|
||
error_counter,
|
||
error_duration,
|
||
}
|
||
}
|
||
|
||
pub fn record_error(&self, error: &AppError) {
|
||
self.error_counter.inc();
|
||
|
||
// 可以根据错误类型添加标签
|
||
tracing::info!(
|
||
target = "udmin",
|
||
error_code = %error.error_code(),
|
||
error_type = %std::any::type_name::<AppError>(),
|
||
"metrics.error.recorded"
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 错误设计原则
|
||
|
||
1. **明确性**: 错误消息应该清楚地说明发生了什么
|
||
2. **可操作性**: 错误消息应该告诉用户如何解决问题
|
||
3. **一致性**: 相同类型的错误应该有一致的格式和处理方式
|
||
4. **安全性**: 不要在错误消息中泄露敏感信息
|
||
|
||
### 错误处理模式
|
||
|
||
1. **快速失败**: 尽早检测和报告错误
|
||
2. **优雅降级**: 在可能的情况下提供备选方案
|
||
3. **错误隔离**: 防止错误在系统中传播
|
||
4. **错误恢复**: 在适当的时候尝试从错误中恢复
|
||
|
||
### 日志记录
|
||
|
||
1. **结构化日志**: 使用结构化格式记录错误信息
|
||
2. **上下文信息**: 包含足够的上下文信息用于调试
|
||
3. **敏感信息**: 避免在日志中记录敏感信息
|
||
4. **日志级别**: 根据错误严重程度选择合适的日志级别
|
||
|
||
## 总结
|
||
|
||
UdminAI 的错误处理模块提供了完整的错误管理解决方案,具有以下特点:
|
||
|
||
- **类型安全**: 编译时错误类型检查
|
||
- **统一处理**: 所有错误使用统一的类型和格式
|
||
- **用户友好**: 清晰的错误消息和适当的 HTTP 状态码
|
||
- **可观测性**: 完整的错误日志和指标收集
|
||
- **可扩展性**: 易于添加新的错误类型和处理逻辑
|
||
|
||
通过合理的错误处理设计,确保了系统的稳定性、可维护性和用户体验。 |