185 lines
5.6 KiB
Rust
185 lines
5.6 KiB
Rust
use actix_web::{ResponseError, http::StatusCode, HttpResponse};
|
|
use serde::Serialize;
|
|
use std::fmt;
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ErrorResponse<T = ()> {
|
|
pub success: bool,
|
|
pub data: Option<T>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
impl<T> ErrorResponse<T> {
|
|
pub fn success(data: T) -> Self {
|
|
Self {
|
|
success: true,
|
|
data: Some(data),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
pub fn error(msg: impl Into<String>) -> Self {
|
|
Self {
|
|
success: false,
|
|
data: None,
|
|
error: Some(msg.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Serialize> ErrorResponse<T> {
|
|
pub fn to_json_response(self) -> HttpResponse {
|
|
HttpResponse::Ok().json(self)
|
|
}
|
|
}
|
|
|
|
impl ErrorResponse<()> {
|
|
pub fn to_error_response(&self, status: StatusCode) -> HttpResponse {
|
|
let body = serde_json::json!({
|
|
"success": false,
|
|
"error": self.error.clone().unwrap_or_else(|| "Unknown error".to_string())
|
|
});
|
|
HttpResponse::build(status).json(body)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum AppError {
|
|
Unauthorized(String),
|
|
Forbidden(String),
|
|
NotFound(String),
|
|
BadRequest(String),
|
|
Internal(String),
|
|
Database(String),
|
|
TooManyRequests(String),
|
|
ServiceUnavailable(String),
|
|
}
|
|
|
|
impl fmt::Display for AppError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
AppError::Unauthorized(msg) => write!(f, "{}", msg),
|
|
AppError::Forbidden(msg) => write!(f, "{}", msg),
|
|
AppError::NotFound(msg) => write!(f, "{}", msg),
|
|
AppError::BadRequest(msg) => write!(f, "{}", msg),
|
|
AppError::Internal(msg) => write!(f, "{}", msg),
|
|
AppError::Database(msg) => write!(f, "{}", msg),
|
|
AppError::TooManyRequests(msg) => write!(f, "{}", msg),
|
|
AppError::ServiceUnavailable(msg) => write!(f, "{}", msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ResponseError for AppError {
|
|
fn status_code(&self) -> StatusCode {
|
|
match self {
|
|
AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
|
|
AppError::Forbidden(_) => StatusCode::FORBIDDEN,
|
|
AppError::NotFound(_) => StatusCode::NOT_FOUND,
|
|
AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
|
AppError::Internal(_) | AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
AppError::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
|
|
AppError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
|
|
}
|
|
}
|
|
|
|
fn error_response(&self) -> actix_web::HttpResponse {
|
|
let error_message = self.to_string();
|
|
let status_code = self.status_code().as_u16();
|
|
|
|
let html = format!(r#"<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>错误 {status_code}</title>
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}}
|
|
.container {{
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 48px;
|
|
max-width: 480px;
|
|
width: 100%;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
text-align: center;
|
|
}}
|
|
.error-code {{
|
|
font-size: 72px;
|
|
font-weight: bold;
|
|
color: #e74c3c;
|
|
margin-bottom: 16px;
|
|
}}
|
|
h1 {{
|
|
color: #333;
|
|
margin-bottom: 16px;
|
|
font-size: 24px;
|
|
}}
|
|
.message {{
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin: 24px 0;
|
|
color: #555;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
}}
|
|
.hint {{
|
|
color: #888;
|
|
font-size: 13px;
|
|
margin-top: 24px;
|
|
}}
|
|
.back-link {{
|
|
display: inline-block;
|
|
margin-top: 24px;
|
|
padding: 12px 24px;
|
|
background: #3498db;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: background 0.3s;
|
|
}}
|
|
.back-link:hover {{
|
|
background: #2980b9;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="error-code">{status_code}</div>
|
|
<h1>{error_title}</h1>
|
|
<div class="message">{error_message}</div>
|
|
<p style="margin-top: 24px; color: #3498db; font-size: 14px;">请返回小程序重新生成新的下载链接</p>
|
|
<div class="hint">如果问题持续存在,请联系技术支持</div>
|
|
</div>
|
|
</body>
|
|
</html>"#,
|
|
error_title = match self {
|
|
AppError::Unauthorized(_) => "认证失败",
|
|
AppError::Forbidden(_) => "权限不足",
|
|
AppError::NotFound(_) => "内容未找到",
|
|
AppError::BadRequest(_) => "请求无效",
|
|
AppError::Internal(_) | AppError::Database(_) => "服务器错误",
|
|
AppError::TooManyRequests(_) => "请求过于频繁",
|
|
AppError::ServiceUnavailable(_) => "服务暂不可用",
|
|
}
|
|
);
|
|
|
|
HttpResponse::build(self.status_code())
|
|
.content_type("text/html; charset=utf-8")
|
|
.body(html)
|
|
}
|
|
}
|
|
|