新增以下文档文件: - PROJECT_OVERVIEW.md 项目总览文档 - BACKEND_ARCHITECTURE.md 后端架构文档 - FRONTEND_ARCHITECTURE.md 前端架构文档 - FLOW_ENGINE.md 流程引擎文档 - SERVICES.md 服务层文档 - ERROR_HANDLING.md 错误处理模块文档 文档内容涵盖项目整体介绍、技术架构、核心模块设计和实现细节
1161 lines
30 KiB
Markdown
1161 lines
30 KiB
Markdown
# UdminAI 响应格式模块文档
|
||
|
||
## 概述
|
||
|
||
UdminAI 项目的响应格式模块定义了统一的 API 响应结构,确保所有接口返回一致的数据格式。该模块基于 Axum 框架和 Serde 序列化库构建,提供了类型安全的响应处理机制。
|
||
|
||
## 设计原则
|
||
|
||
### 核心理念
|
||
|
||
- **一致性**: 所有 API 接口使用统一的响应格式
|
||
- **类型安全**: 编译时类型检查,避免运行时错误
|
||
- **可扩展性**: 支持不同类型的响应数据
|
||
- **用户友好**: 清晰的响应结构和错误信息
|
||
- **标准化**: 遵循 RESTful API 设计规范
|
||
|
||
### 响应分类
|
||
|
||
1. **成功响应**: 操作成功时的数据返回
|
||
2. **分页响应**: 列表数据的分页返回
|
||
3. **错误响应**: 操作失败时的错误信息
|
||
4. **空响应**: 无数据返回的成功操作
|
||
|
||
## 响应结构定义 (response.rs)
|
||
|
||
### 基础响应类型
|
||
|
||
```rust
|
||
use axum::{
|
||
http::StatusCode,
|
||
response::{IntoResponse, Response},
|
||
Json,
|
||
};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::collections::HashMap;
|
||
|
||
/// API 响应的基础结构
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ApiResponse<T> {
|
||
/// 响应状态码
|
||
pub code: u16,
|
||
/// 响应消息
|
||
pub message: String,
|
||
/// 响应数据
|
||
pub data: Option<T>,
|
||
/// 响应时间戳
|
||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||
/// 请求追踪 ID
|
||
pub request_id: Option<String>,
|
||
}
|
||
|
||
/// 分页响应结构
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct PageResponse<T> {
|
||
/// 数据列表
|
||
pub items: Vec<T>,
|
||
/// 分页信息
|
||
pub pagination: PaginationInfo,
|
||
}
|
||
|
||
/// 分页信息
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct PaginationInfo {
|
||
/// 当前页码(从1开始)
|
||
pub current_page: u64,
|
||
/// 每页大小
|
||
pub page_size: u64,
|
||
/// 总记录数
|
||
pub total_count: u64,
|
||
/// 总页数
|
||
pub total_pages: u64,
|
||
/// 是否有下一页
|
||
pub has_next: bool,
|
||
/// 是否有上一页
|
||
pub has_prev: bool,
|
||
}
|
||
|
||
/// 批量操作响应
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct BatchResponse<T> {
|
||
/// 成功处理的项目
|
||
pub success: Vec<T>,
|
||
/// 失败的项目及错误信息
|
||
pub failed: Vec<BatchError>,
|
||
/// 成功数量
|
||
pub success_count: usize,
|
||
/// 失败数量
|
||
pub failed_count: usize,
|
||
}
|
||
|
||
/// 批量操作错误
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct BatchError {
|
||
/// 项目标识
|
||
pub id: String,
|
||
/// 错误代码
|
||
pub error_code: String,
|
||
/// 错误消息
|
||
pub error_message: String,
|
||
}
|
||
|
||
/// 统计响应
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct StatsResponse {
|
||
/// 统计数据
|
||
pub stats: HashMap<String, serde_json::Value>,
|
||
/// 统计时间范围
|
||
pub time_range: Option<TimeRange>,
|
||
/// 更新时间
|
||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||
}
|
||
|
||
/// 时间范围
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct TimeRange {
|
||
/// 开始时间
|
||
pub start: chrono::DateTime<chrono::Utc>,
|
||
/// 结束时间
|
||
pub end: chrono::DateTime<chrono::Utc>,
|
||
}
|
||
|
||
/// 健康检查响应
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct HealthResponse {
|
||
/// 服务状态
|
||
pub status: HealthStatus,
|
||
/// 版本信息
|
||
pub version: String,
|
||
/// 启动时间
|
||
pub uptime: String,
|
||
/// 依赖服务状态
|
||
pub dependencies: HashMap<String, DependencyStatus>,
|
||
}
|
||
|
||
/// 健康状态
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[serde(rename_all = "lowercase")]
|
||
pub enum HealthStatus {
|
||
Healthy,
|
||
Degraded,
|
||
Unhealthy,
|
||
}
|
||
|
||
/// 依赖服务状态
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct DependencyStatus {
|
||
/// 状态
|
||
pub status: HealthStatus,
|
||
/// 响应时间(毫秒)
|
||
pub response_time_ms: Option<u64>,
|
||
/// 错误信息
|
||
pub error: Option<String>,
|
||
}
|
||
|
||
impl<T> ApiResponse<T> {
|
||
/// 创建成功响应
|
||
pub fn success(data: T) -> Self {
|
||
Self {
|
||
code: 200,
|
||
message: "操作成功".to_string(),
|
||
data: Some(data),
|
||
timestamp: chrono::Utc::now(),
|
||
request_id: None,
|
||
}
|
||
}
|
||
|
||
/// 创建成功响应(带自定义消息)
|
||
pub fn success_with_message(data: T, message: impl Into<String>) -> Self {
|
||
Self {
|
||
code: 200,
|
||
message: message.into(),
|
||
data: Some(data),
|
||
timestamp: chrono::Utc::now(),
|
||
request_id: None,
|
||
}
|
||
}
|
||
|
||
/// 创建创建成功响应
|
||
pub fn created(data: T) -> Self {
|
||
Self {
|
||
code: 201,
|
||
message: "创建成功".to_string(),
|
||
data: Some(data),
|
||
timestamp: chrono::Utc::now(),
|
||
request_id: None,
|
||
}
|
||
}
|
||
|
||
/// 创建无内容响应
|
||
pub fn no_content() -> ApiResponse<()> {
|
||
ApiResponse {
|
||
code: 204,
|
||
message: "操作成功".to_string(),
|
||
data: None,
|
||
timestamp: chrono::Utc::now(),
|
||
request_id: None,
|
||
}
|
||
}
|
||
|
||
/// 设置请求 ID
|
||
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
|
||
self.request_id = Some(request_id.into());
|
||
self
|
||
}
|
||
|
||
/// 设置状态码
|
||
pub fn with_code(mut self, code: u16) -> Self {
|
||
self.code = code;
|
||
self
|
||
}
|
||
|
||
/// 设置消息
|
||
pub fn with_message(mut self, message: impl Into<String>) -> Self {
|
||
self.message = message.into();
|
||
self
|
||
}
|
||
}
|
||
|
||
impl<T> PageResponse<T> {
|
||
/// 创建分页响应
|
||
pub fn new(
|
||
items: Vec<T>,
|
||
current_page: u64,
|
||
page_size: u64,
|
||
total_count: u64,
|
||
) -> Self {
|
||
let total_pages = if total_count == 0 {
|
||
1
|
||
} else {
|
||
(total_count + page_size - 1) / page_size
|
||
};
|
||
|
||
let has_next = current_page < total_pages;
|
||
let has_prev = current_page > 1;
|
||
|
||
Self {
|
||
items,
|
||
pagination: PaginationInfo {
|
||
current_page,
|
||
page_size,
|
||
total_count,
|
||
total_pages,
|
||
has_next,
|
||
has_prev,
|
||
},
|
||
}
|
||
}
|
||
|
||
/// 创建空分页响应
|
||
pub fn empty(page_size: u64) -> Self {
|
||
Self::new(Vec::new(), 1, page_size, 0)
|
||
}
|
||
}
|
||
|
||
impl<T> BatchResponse<T> {
|
||
/// 创建批量响应
|
||
pub fn new(success: Vec<T>, failed: Vec<BatchError>) -> Self {
|
||
let success_count = success.len();
|
||
let failed_count = failed.len();
|
||
|
||
Self {
|
||
success,
|
||
failed,
|
||
success_count,
|
||
failed_count,
|
||
}
|
||
}
|
||
|
||
/// 创建全部成功的批量响应
|
||
pub fn all_success(success: Vec<T>) -> Self {
|
||
Self::new(success, Vec::new())
|
||
}
|
||
|
||
/// 创建全部失败的批量响应
|
||
pub fn all_failed(failed: Vec<BatchError>) -> Self {
|
||
Self::new(Vec::new(), failed)
|
||
}
|
||
}
|
||
|
||
impl StatsResponse {
|
||
/// 创建统计响应
|
||
pub fn new(stats: HashMap<String, serde_json::Value>) -> Self {
|
||
Self {
|
||
stats,
|
||
time_range: None,
|
||
updated_at: chrono::Utc::now(),
|
||
}
|
||
}
|
||
|
||
/// 设置时间范围
|
||
pub fn with_time_range(
|
||
mut self,
|
||
start: chrono::DateTime<chrono::Utc>,
|
||
end: chrono::DateTime<chrono::Utc>,
|
||
) -> Self {
|
||
self.time_range = Some(TimeRange { start, end });
|
||
self
|
||
}
|
||
}
|
||
|
||
impl HealthResponse {
|
||
/// 创建健康响应
|
||
pub fn new(
|
||
status: HealthStatus,
|
||
version: impl Into<String>,
|
||
uptime: impl Into<String>,
|
||
) -> Self {
|
||
Self {
|
||
status,
|
||
version: version.into(),
|
||
uptime: uptime.into(),
|
||
dependencies: HashMap::new(),
|
||
}
|
||
}
|
||
|
||
/// 添加依赖状态
|
||
pub fn with_dependency(
|
||
mut self,
|
||
name: impl Into<String>,
|
||
status: DependencyStatus,
|
||
) -> Self {
|
||
self.dependencies.insert(name.into(), status);
|
||
self
|
||
}
|
||
}
|
||
|
||
impl DependencyStatus {
|
||
/// 创建健康的依赖状态
|
||
pub fn healthy(response_time_ms: u64) -> Self {
|
||
Self {
|
||
status: HealthStatus::Healthy,
|
||
response_time_ms: Some(response_time_ms),
|
||
error: None,
|
||
}
|
||
}
|
||
|
||
/// 创建不健康的依赖状态
|
||
pub fn unhealthy(error: impl Into<String>) -> Self {
|
||
Self {
|
||
status: HealthStatus::Unhealthy,
|
||
response_time_ms: None,
|
||
error: Some(error.into()),
|
||
}
|
||
}
|
||
|
||
/// 创建降级的依赖状态
|
||
pub fn degraded(response_time_ms: u64, error: impl Into<String>) -> Self {
|
||
Self {
|
||
status: HealthStatus::Degraded,
|
||
response_time_ms: Some(response_time_ms),
|
||
error: Some(error.into()),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 实现 IntoResponse,使响应可以直接返回
|
||
impl<T> IntoResponse for ApiResponse<T>
|
||
where
|
||
T: Serialize,
|
||
{
|
||
fn into_response(self) -> Response {
|
||
let status_code = StatusCode::from_u16(self.code)
|
||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||
|
||
(status_code, Json(self)).into_response()
|
||
}
|
||
}
|
||
|
||
/// 响应构建器
|
||
pub struct ResponseBuilder<T> {
|
||
response: ApiResponse<T>,
|
||
}
|
||
|
||
impl<T> ResponseBuilder<T> {
|
||
/// 创建新的响应构建器
|
||
pub fn new(data: T) -> Self {
|
||
Self {
|
||
response: ApiResponse::success(data),
|
||
}
|
||
}
|
||
|
||
/// 设置状态码
|
||
pub fn code(mut self, code: u16) -> Self {
|
||
self.response.code = code;
|
||
self
|
||
}
|
||
|
||
/// 设置消息
|
||
pub fn message(mut self, message: impl Into<String>) -> Self {
|
||
self.response.message = message.into();
|
||
self
|
||
}
|
||
|
||
/// 设置请求 ID
|
||
pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
|
||
self.response.request_id = Some(request_id.into());
|
||
self
|
||
}
|
||
|
||
/// 构建响应
|
||
pub fn build(self) -> ApiResponse<T> {
|
||
self.response
|
||
}
|
||
}
|
||
|
||
/// 响应宏
|
||
#[macro_export]
|
||
macro_rules! ok_response {
|
||
($data:expr) => {
|
||
ApiResponse::success($data)
|
||
};
|
||
($data:expr, $message:expr) => {
|
||
ApiResponse::success_with_message($data, $message)
|
||
};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! created_response {
|
||
($data:expr) => {
|
||
ApiResponse::created($data)
|
||
};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! no_content_response {
|
||
() => {
|
||
ApiResponse::no_content()
|
||
};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! page_response {
|
||
($items:expr, $page:expr, $size:expr, $total:expr) => {
|
||
ApiResponse::success(PageResponse::new($items, $page, $size, $total))
|
||
};
|
||
}
|
||
|
||
/// 响应扩展 trait
|
||
pub trait ResponseExt<T> {
|
||
/// 转换为 API 响应
|
||
fn into_api_response(self) -> ApiResponse<T>;
|
||
|
||
/// 转换为分页响应
|
||
fn into_page_response(
|
||
self,
|
||
current_page: u64,
|
||
page_size: u64,
|
||
total_count: u64,
|
||
) -> ApiResponse<PageResponse<T>>
|
||
where
|
||
Self: IntoIterator<Item = T>,
|
||
Self::IntoIter: ExactSizeIterator;
|
||
}
|
||
|
||
impl<T> ResponseExt<T> for T {
|
||
fn into_api_response(self) -> ApiResponse<T> {
|
||
ApiResponse::success(self)
|
||
}
|
||
|
||
fn into_page_response(
|
||
self,
|
||
current_page: u64,
|
||
page_size: u64,
|
||
total_count: u64,
|
||
) -> ApiResponse<PageResponse<T>>
|
||
where
|
||
Self: IntoIterator<Item = T>,
|
||
Self::IntoIter: ExactSizeIterator,
|
||
{
|
||
let items: Vec<T> = self.into_iter().collect();
|
||
let page_response = PageResponse::new(items, current_page, page_size, total_count);
|
||
ApiResponse::success(page_response)
|
||
}
|
||
}
|
||
|
||
impl<T> ResponseExt<T> for Vec<T> {
|
||
fn into_api_response(self) -> ApiResponse<Vec<T>> {
|
||
ApiResponse::success(self)
|
||
}
|
||
|
||
fn into_page_response(
|
||
self,
|
||
current_page: u64,
|
||
page_size: u64,
|
||
total_count: u64,
|
||
) -> ApiResponse<PageResponse<T>>
|
||
where
|
||
Self: IntoIterator<Item = T>,
|
||
Self::IntoIter: ExactSizeIterator,
|
||
{
|
||
let page_response = PageResponse::new(self, current_page, page_size, total_count);
|
||
ApiResponse::success(page_response)
|
||
}
|
||
}
|
||
|
||
/// 常用响应类型别名
|
||
pub type JsonResponse<T> = ApiResponse<T>;
|
||
pub type ListResponse<T> = ApiResponse<Vec<T>>;
|
||
pub type PageResp<T> = ApiResponse<PageResponse<T>>;
|
||
pub type BatchResp<T> = ApiResponse<BatchResponse<T>>;
|
||
pub type StatsResp = ApiResponse<StatsResponse>;
|
||
pub type HealthResp = ApiResponse<HealthResponse>;
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use serde_json::json;
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||
struct TestData {
|
||
id: u32,
|
||
name: String,
|
||
}
|
||
|
||
#[test]
|
||
fn test_success_response() {
|
||
let data = TestData {
|
||
id: 1,
|
||
name: "test".to_string(),
|
||
};
|
||
|
||
let response = ApiResponse::success(data.clone());
|
||
assert_eq!(response.code, 200);
|
||
assert_eq!(response.message, "操作成功");
|
||
assert_eq!(response.data, Some(data));
|
||
}
|
||
|
||
#[test]
|
||
fn test_created_response() {
|
||
let data = TestData {
|
||
id: 1,
|
||
name: "test".to_string(),
|
||
};
|
||
|
||
let response = ApiResponse::created(data.clone());
|
||
assert_eq!(response.code, 201);
|
||
assert_eq!(response.message, "创建成功");
|
||
assert_eq!(response.data, Some(data));
|
||
}
|
||
|
||
#[test]
|
||
fn test_no_content_response() {
|
||
let response = ApiResponse::no_content();
|
||
assert_eq!(response.code, 204);
|
||
assert_eq!(response.message, "操作成功");
|
||
assert_eq!(response.data, None);
|
||
}
|
||
|
||
#[test]
|
||
fn test_page_response() {
|
||
let items = vec![
|
||
TestData { id: 1, name: "test1".to_string() },
|
||
TestData { id: 2, name: "test2".to_string() },
|
||
];
|
||
|
||
let page_response = PageResponse::new(items.clone(), 1, 10, 25);
|
||
|
||
assert_eq!(page_response.items, items);
|
||
assert_eq!(page_response.pagination.current_page, 1);
|
||
assert_eq!(page_response.pagination.page_size, 10);
|
||
assert_eq!(page_response.pagination.total_count, 25);
|
||
assert_eq!(page_response.pagination.total_pages, 3);
|
||
assert_eq!(page_response.pagination.has_next, true);
|
||
assert_eq!(page_response.pagination.has_prev, false);
|
||
}
|
||
|
||
#[test]
|
||
fn test_batch_response() {
|
||
let success_items = vec![
|
||
TestData { id: 1, name: "test1".to_string() },
|
||
TestData { id: 2, name: "test2".to_string() },
|
||
];
|
||
|
||
let failed_items = vec![
|
||
BatchError {
|
||
id: "3".to_string(),
|
||
error_code: "VALIDATION_FAILED".to_string(),
|
||
error_message: "名称不能为空".to_string(),
|
||
},
|
||
];
|
||
|
||
let batch_response = BatchResponse::new(success_items.clone(), failed_items.clone());
|
||
|
||
assert_eq!(batch_response.success, success_items);
|
||
assert_eq!(batch_response.failed, failed_items);
|
||
assert_eq!(batch_response.success_count, 2);
|
||
assert_eq!(batch_response.failed_count, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_stats_response() {
|
||
let mut stats = HashMap::new();
|
||
stats.insert("total_users".to_string(), json!(100));
|
||
stats.insert("active_users".to_string(), json!(85));
|
||
|
||
let stats_response = StatsResponse::new(stats.clone());
|
||
|
||
assert_eq!(stats_response.stats, stats);
|
||
assert!(stats_response.time_range.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn test_health_response() {
|
||
let health_response = HealthResponse::new(
|
||
HealthStatus::Healthy,
|
||
"1.0.0",
|
||
"2 days",
|
||
)
|
||
.with_dependency(
|
||
"database",
|
||
DependencyStatus::healthy(50),
|
||
)
|
||
.with_dependency(
|
||
"redis",
|
||
DependencyStatus::degraded(200, "连接缓慢"),
|
||
);
|
||
|
||
assert!(matches!(health_response.status, HealthStatus::Healthy));
|
||
assert_eq!(health_response.version, "1.0.0");
|
||
assert_eq!(health_response.uptime, "2 days");
|
||
assert_eq!(health_response.dependencies.len(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn test_response_builder() {
|
||
let data = TestData {
|
||
id: 1,
|
||
name: "test".to_string(),
|
||
};
|
||
|
||
let response = ResponseBuilder::new(data.clone())
|
||
.code(201)
|
||
.message("自定义消息")
|
||
.request_id("req-123")
|
||
.build();
|
||
|
||
assert_eq!(response.code, 201);
|
||
assert_eq!(response.message, "自定义消息");
|
||
assert_eq!(response.request_id, Some("req-123".to_string()));
|
||
assert_eq!(response.data, Some(data));
|
||
}
|
||
|
||
#[test]
|
||
fn test_response_macros() {
|
||
let data = TestData {
|
||
id: 1,
|
||
name: "test".to_string(),
|
||
};
|
||
|
||
let ok_resp = ok_response!(data.clone());
|
||
assert_eq!(ok_resp.code, 200);
|
||
|
||
let ok_resp_with_msg = ok_response!(data.clone(), "自定义消息");
|
||
assert_eq!(ok_resp_with_msg.message, "自定义消息");
|
||
|
||
let created_resp = created_response!(data.clone());
|
||
assert_eq!(created_resp.code, 201);
|
||
|
||
let no_content_resp = no_content_response!();
|
||
assert_eq!(no_content_resp.code, 204);
|
||
|
||
let items = vec![data.clone()];
|
||
let page_resp = page_response!(items, 1, 10, 1);
|
||
assert_eq!(page_resp.code, 200);
|
||
}
|
||
|
||
#[test]
|
||
fn test_response_ext() {
|
||
let data = TestData {
|
||
id: 1,
|
||
name: "test".to_string(),
|
||
};
|
||
|
||
let response = data.clone().into_api_response();
|
||
assert_eq!(response.code, 200);
|
||
assert_eq!(response.data, Some(data));
|
||
|
||
let items = vec![
|
||
TestData { id: 1, name: "test1".to_string() },
|
||
TestData { id: 2, name: "test2".to_string() },
|
||
];
|
||
|
||
let page_response = items.clone().into_page_response(1, 10, 25);
|
||
assert_eq!(page_response.code, 200);
|
||
|
||
if let Some(page_data) = page_response.data {
|
||
assert_eq!(page_data.items, items);
|
||
assert_eq!(page_data.pagination.total_count, 25);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 使用示例
|
||
|
||
### 基础响应
|
||
|
||
```rust
|
||
use axum::{extract::Path, Json};
|
||
use crate::response::{ApiResponse, ok_response, created_response};
|
||
|
||
/// 获取用户信息
|
||
pub async fn get_user(Path(id): Path<String>) -> impl IntoResponse {
|
||
match user_service::get_by_id(&id).await {
|
||
Ok(user) => ok_response!(user, "获取用户信息成功"),
|
||
Err(e) => e.into_response(),
|
||
}
|
||
}
|
||
|
||
/// 创建用户
|
||
pub async fn create_user(Json(req): Json<CreateUserReq>) -> impl IntoResponse {
|
||
match user_service::create(req).await {
|
||
Ok(user) => created_response!(user),
|
||
Err(e) => e.into_response(),
|
||
}
|
||
}
|
||
|
||
/// 删除用户
|
||
pub async fn delete_user(Path(id): Path<String>) -> impl IntoResponse {
|
||
match user_service::delete(&id).await {
|
||
Ok(_) => no_content_response!(),
|
||
Err(e) => e.into_response(),
|
||
}
|
||
}
|
||
```
|
||
|
||
### 分页响应
|
||
|
||
```rust
|
||
use axum::extract::Query;
|
||
use crate::response::{PageResponse, page_response};
|
||
|
||
/// 获取用户列表
|
||
pub async fn list_users(Query(params): Query<ListUsersReq>) -> impl IntoResponse {
|
||
match user_service::list(params).await {
|
||
Ok((users, total)) => {
|
||
page_response!(users, params.page, params.page_size, total)
|
||
}
|
||
Err(e) => e.into_response(),
|
||
}
|
||
}
|
||
```
|
||
|
||
### 批量操作响应
|
||
|
||
```rust
|
||
use crate::response::{BatchResponse, BatchError};
|
||
|
||
/// 批量创建用户
|
||
pub async fn batch_create_users(
|
||
Json(req): Json<BatchCreateUsersReq>,
|
||
) -> impl IntoResponse {
|
||
let mut success = Vec::new();
|
||
let mut failed = Vec::new();
|
||
|
||
for (index, user_req) in req.users.into_iter().enumerate() {
|
||
match user_service::create(user_req).await {
|
||
Ok(user) => success.push(user),
|
||
Err(e) => failed.push(BatchError {
|
||
id: index.to_string(),
|
||
error_code: e.error_code().to_string(),
|
||
error_message: e.to_string(),
|
||
}),
|
||
}
|
||
}
|
||
|
||
let batch_response = BatchResponse::new(success, failed);
|
||
ApiResponse::success(batch_response)
|
||
}
|
||
```
|
||
|
||
### 统计响应
|
||
|
||
```rust
|
||
use std::collections::HashMap;
|
||
use serde_json::json;
|
||
use crate::response::StatsResponse;
|
||
|
||
/// 获取用户统计
|
||
pub async fn get_user_stats() -> impl IntoResponse {
|
||
let mut stats = HashMap::new();
|
||
|
||
match user_service::get_stats().await {
|
||
Ok(user_stats) => {
|
||
stats.insert("total_users".to_string(), json!(user_stats.total));
|
||
stats.insert("active_users".to_string(), json!(user_stats.active));
|
||
stats.insert("new_users_today".to_string(), json!(user_stats.new_today));
|
||
|
||
let stats_response = StatsResponse::new(stats)
|
||
.with_time_range(
|
||
chrono::Utc::now() - chrono::Duration::days(30),
|
||
chrono::Utc::now(),
|
||
);
|
||
|
||
ApiResponse::success(stats_response)
|
||
}
|
||
Err(e) => e.into_response(),
|
||
}
|
||
}
|
||
```
|
||
|
||
### 健康检查响应
|
||
|
||
```rust
|
||
use crate::response::{HealthResponse, HealthStatus, DependencyStatus};
|
||
|
||
/// 健康检查
|
||
pub async fn health_check() -> impl IntoResponse {
|
||
let mut health_response = HealthResponse::new(
|
||
HealthStatus::Healthy,
|
||
env!("CARGO_PKG_VERSION"),
|
||
format_uptime(),
|
||
);
|
||
|
||
// 检查数据库连接
|
||
match check_database().await {
|
||
Ok(response_time) => {
|
||
health_response = health_response.with_dependency(
|
||
"database",
|
||
DependencyStatus::healthy(response_time),
|
||
);
|
||
}
|
||
Err(e) => {
|
||
health_response = health_response.with_dependency(
|
||
"database",
|
||
DependencyStatus::unhealthy(e.to_string()),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 检查 Redis 连接
|
||
match check_redis().await {
|
||
Ok(response_time) => {
|
||
health_response = health_response.with_dependency(
|
||
"redis",
|
||
DependencyStatus::healthy(response_time),
|
||
);
|
||
}
|
||
Err(e) => {
|
||
health_response = health_response.with_dependency(
|
||
"redis",
|
||
DependencyStatus::unhealthy(e.to_string()),
|
||
);
|
||
}
|
||
}
|
||
|
||
ApiResponse::success(health_response)
|
||
}
|
||
```
|
||
|
||
## 中间件集成
|
||
|
||
### 响应处理中间件
|
||
|
||
```rust
|
||
use axum::{
|
||
extract::Request,
|
||
middleware::Next,
|
||
response::Response,
|
||
};
|
||
use uuid::Uuid;
|
||
|
||
/// 响应处理中间件
|
||
pub async fn response_middleware(
|
||
mut request: Request,
|
||
next: Next,
|
||
) -> Response {
|
||
// 生成请求 ID
|
||
let request_id = Uuid::new_v4().to_string();
|
||
request.extensions_mut().insert(request_id.clone());
|
||
|
||
// 执行请求
|
||
let mut response = next.run(request).await;
|
||
|
||
// 添加响应头
|
||
response.headers_mut().insert(
|
||
"X-Request-ID",
|
||
request_id.parse().unwrap(),
|
||
);
|
||
|
||
response.headers_mut().insert(
|
||
"X-Response-Time",
|
||
chrono::Utc::now().timestamp_millis().to_string().parse().unwrap(),
|
||
);
|
||
|
||
response
|
||
}
|
||
```
|
||
|
||
### 响应压缩中间件
|
||
|
||
```rust
|
||
use axum::{
|
||
body::Body,
|
||
http::{HeaderValue, header},
|
||
response::Response,
|
||
};
|
||
use tower_http::compression::CompressionLayer;
|
||
|
||
/// 响应压缩配置
|
||
pub fn compression_layer() -> CompressionLayer {
|
||
CompressionLayer::new()
|
||
.gzip(true)
|
||
.deflate(true)
|
||
.br(true)
|
||
}
|
||
```
|
||
|
||
## 性能优化
|
||
|
||
### 响应缓存
|
||
|
||
```rust
|
||
use std::collections::HashMap;
|
||
use std::sync::Arc;
|
||
use tokio::sync::RwLock;
|
||
use chrono::{DateTime, Utc, Duration};
|
||
|
||
/// 响应缓存
|
||
#[derive(Clone)]
|
||
pub struct ResponseCache {
|
||
cache: Arc<RwLock<HashMap<String, CachedResponse>>>,
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
struct CachedResponse {
|
||
data: Vec<u8>,
|
||
expires_at: DateTime<Utc>,
|
||
content_type: String,
|
||
}
|
||
|
||
impl ResponseCache {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
cache: Arc::new(RwLock::new(HashMap::new())),
|
||
}
|
||
}
|
||
|
||
/// 获取缓存的响应
|
||
pub async fn get(&self, key: &str) -> Option<Response<Body>> {
|
||
let cache = self.cache.read().await;
|
||
|
||
if let Some(cached) = cache.get(key) {
|
||
if cached.expires_at > Utc::now() {
|
||
let mut response = Response::new(Body::from(cached.data.clone()));
|
||
response.headers_mut().insert(
|
||
header::CONTENT_TYPE,
|
||
HeaderValue::from_str(&cached.content_type).unwrap(),
|
||
);
|
||
response.headers_mut().insert(
|
||
"X-Cache",
|
||
HeaderValue::from_static("HIT"),
|
||
);
|
||
return Some(response);
|
||
}
|
||
}
|
||
|
||
None
|
||
}
|
||
|
||
/// 设置响应缓存
|
||
pub async fn set(
|
||
&self,
|
||
key: String,
|
||
data: Vec<u8>,
|
||
content_type: String,
|
||
ttl: Duration,
|
||
) {
|
||
let mut cache = self.cache.write().await;
|
||
|
||
cache.insert(key, CachedResponse {
|
||
data,
|
||
expires_at: Utc::now() + ttl,
|
||
content_type,
|
||
});
|
||
}
|
||
|
||
/// 清理过期缓存
|
||
pub async fn cleanup_expired(&self) {
|
||
let mut cache = self.cache.write().await;
|
||
let now = Utc::now();
|
||
|
||
cache.retain(|_, cached| cached.expires_at > now);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 响应流式处理
|
||
|
||
```rust
|
||
use axum::{
|
||
body::Body,
|
||
response::{IntoResponse, Response},
|
||
};
|
||
use futures::stream::{self, StreamExt};
|
||
use tokio_util::codec::{FramedWrite, LinesCodec};
|
||
|
||
/// 流式响应
|
||
pub struct StreamResponse<T> {
|
||
items: Vec<T>,
|
||
}
|
||
|
||
impl<T> StreamResponse<T>
|
||
where
|
||
T: Serialize + Send + 'static,
|
||
{
|
||
pub fn new(items: Vec<T>) -> Self {
|
||
Self { items }
|
||
}
|
||
}
|
||
|
||
impl<T> IntoResponse for StreamResponse<T>
|
||
where
|
||
T: Serialize + Send + 'static,
|
||
{
|
||
fn into_response(self) -> Response {
|
||
let stream = stream::iter(self.items)
|
||
.map(|item| {
|
||
serde_json::to_string(&item)
|
||
.map(|json| format!("{}\n", json))
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||
})
|
||
.map(|result| result.map(axum::body::Bytes::from));
|
||
|
||
let body = Body::from_stream(stream);
|
||
|
||
Response::builder()
|
||
.header("Content-Type", "application/x-ndjson")
|
||
.header("Transfer-Encoding", "chunked")
|
||
.body(body)
|
||
.unwrap()
|
||
}
|
||
}
|
||
```
|
||
|
||
## 测试支持
|
||
|
||
### 响应测试工具
|
||
|
||
```rust
|
||
use axum::{
|
||
body::Body,
|
||
http::{Request, StatusCode},
|
||
};
|
||
use tower::ServiceExt;
|
||
use serde::de::DeserializeOwned;
|
||
|
||
/// 响应测试助手
|
||
pub struct ResponseTester;
|
||
|
||
impl ResponseTester {
|
||
/// 测试成功响应
|
||
pub async fn assert_success_response<T>(
|
||
response: axum::response::Response,
|
||
expected_code: u16,
|
||
) -> T
|
||
where
|
||
T: DeserializeOwned,
|
||
{
|
||
assert_eq!(response.status(), StatusCode::from_u16(expected_code).unwrap());
|
||
|
||
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
|
||
let api_response: ApiResponse<T> = serde_json::from_slice(&body).unwrap();
|
||
|
||
assert_eq!(api_response.code, expected_code);
|
||
api_response.data.unwrap()
|
||
}
|
||
|
||
/// 测试分页响应
|
||
pub async fn assert_page_response<T>(
|
||
response: axum::response::Response,
|
||
expected_total: u64,
|
||
) -> PageResponse<T>
|
||
where
|
||
T: DeserializeOwned,
|
||
{
|
||
let page_data: PageResponse<T> = Self::assert_success_response(response, 200).await;
|
||
assert_eq!(page_data.pagination.total_count, expected_total);
|
||
page_data
|
||
}
|
||
|
||
/// 测试错误响应
|
||
pub async fn assert_error_response(
|
||
response: axum::response::Response,
|
||
expected_status: StatusCode,
|
||
expected_error_code: &str,
|
||
) {
|
||
assert_eq!(response.status(), expected_status);
|
||
|
||
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
|
||
let error_response: crate::error::ErrorResponse =
|
||
serde_json::from_slice(&body).unwrap();
|
||
|
||
assert_eq!(error_response.error.code, expected_error_code);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod response_tests {
|
||
use super::*;
|
||
use axum::{
|
||
routing::get,
|
||
Router,
|
||
};
|
||
|
||
async fn test_handler() -> impl IntoResponse {
|
||
ok_response!("test data")
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_success_response_integration() {
|
||
let app = Router::new().route("/test", get(test_handler));
|
||
|
||
let request = Request::builder()
|
||
.uri("/test")
|
||
.body(Body::empty())
|
||
.unwrap();
|
||
|
||
let response = app.oneshot(request).await.unwrap();
|
||
|
||
let data: String = ResponseTester::assert_success_response(response, 200).await;
|
||
assert_eq!(data, "test data");
|
||
}
|
||
}
|
||
```
|
||
|
||
## 最佳实践
|
||
|
||
### 响应设计原则
|
||
|
||
1. **一致性**: 所有接口使用统一的响应格式
|
||
2. **可预测性**: 响应结构应该是可预测的
|
||
3. **信息完整性**: 包含足够的信息用于客户端处理
|
||
4. **向后兼容性**: 新增字段不应破坏现有客户端
|
||
|
||
### 性能考虑
|
||
|
||
1. **序列化优化**: 使用高效的序列化库
|
||
2. **响应压缩**: 对大响应启用压缩
|
||
3. **缓存策略**: 对适当的响应启用缓存
|
||
4. **流式处理**: 对大数据集使用流式响应
|
||
|
||
### 安全考虑
|
||
|
||
1. **敏感信息**: 避免在响应中暴露敏感信息
|
||
2. **错误信息**: 错误响应不应泄露系统内部信息
|
||
3. **响应头**: 设置适当的安全响应头
|
||
4. **数据验证**: 确保响应数据的完整性
|
||
|
||
## 总结
|
||
|
||
UdminAI 的响应格式模块提供了完整的 API 响应解决方案,具有以下特点:
|
||
|
||
- **统一格式**: 所有接口使用一致的响应结构
|
||
- **类型安全**: 编译时类型检查,避免运行时错误
|
||
- **功能丰富**: 支持多种响应类型(成功、分页、批量、统计等)
|
||
- **易于使用**: 提供宏和扩展 trait 简化使用
|
||
- **高性能**: 支持缓存、压缩和流式处理
|
||
- **可测试**: 提供完整的测试支持工具
|
||
|
||
通过统一的响应格式设计,确保了 API 的一致性、可维护性和用户体验。 |