新增以下文档文件: - PROJECT_OVERVIEW.md 项目总览文档 - BACKEND_ARCHITECTURE.md 后端架构文档 - FRONTEND_ARCHITECTURE.md 前端架构文档 - FLOW_ENGINE.md 流程引擎文档 - SERVICES.md 服务层文档 - ERROR_HANDLING.md 错误处理模块文档 文档内容涵盖项目整体介绍、技术架构、核心模块设计和实现细节
30 KiB
30 KiB
UdminAI 响应格式模块文档
概述
UdminAI 项目的响应格式模块定义了统一的 API 响应结构,确保所有接口返回一致的数据格式。该模块基于 Axum 框架和 Serde 序列化库构建,提供了类型安全的响应处理机制。
设计原则
核心理念
- 一致性: 所有 API 接口使用统一的响应格式
- 类型安全: 编译时类型检查,避免运行时错误
- 可扩展性: 支持不同类型的响应数据
- 用户友好: 清晰的响应结构和错误信息
- 标准化: 遵循 RESTful API 设计规范
响应分类
- 成功响应: 操作成功时的数据返回
- 分页响应: 列表数据的分页返回
- 错误响应: 操作失败时的错误信息
- 空响应: 无数据返回的成功操作
响应结构定义 (response.rs)
基础响应类型
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);
}
}
}
使用示例
基础响应
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(),
}
}
分页响应
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(),
}
}
批量操作响应
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)
}
统计响应
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(),
}
}
健康检查响应
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)
}
中间件集成
响应处理中间件
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
}
响应压缩中间件
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)
}
性能优化
响应缓存
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);
}
}
响应流式处理
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()
}
}
测试支持
响应测试工具
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");
}
}
最佳实践
响应设计原则
- 一致性: 所有接口使用统一的响应格式
- 可预测性: 响应结构应该是可预测的
- 信息完整性: 包含足够的信息用于客户端处理
- 向后兼容性: 新增字段不应破坏现有客户端
性能考虑
- 序列化优化: 使用高效的序列化库
- 响应压缩: 对大响应启用压缩
- 缓存策略: 对适当的响应启用缓存
- 流式处理: 对大数据集使用流式响应
安全考虑
- 敏感信息: 避免在响应中暴露敏感信息
- 错误信息: 错误响应不应泄露系统内部信息
- 响应头: 设置适当的安全响应头
- 数据验证: 确保响应数据的完整性
总结
UdminAI 的响应格式模块提供了完整的 API 响应解决方案,具有以下特点:
- 统一格式: 所有接口使用一致的响应结构
- 类型安全: 编译时类型检查,避免运行时错误
- 功能丰富: 支持多种响应类型(成功、分页、批量、统计等)
- 易于使用: 提供宏和扩展 trait 简化使用
- 高性能: 支持缓存、压缩和流式处理
- 可测试: 提供完整的测试支持工具
通过统一的响应格式设计,确保了 API 的一致性、可维护性和用户体验。