chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -1,81 +1,83 @@
|
||||
//! 通用中间件
|
||||
//! 中间件模块
|
||||
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{HeaderValue, Request, Response},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::error::SaasError;
|
||||
use crate::auth::types::AuthContext;
|
||||
|
||||
/// 滑动窗口速率限制中间件
|
||||
///
|
||||
/// 按 account_id (从 AuthContext 提取) 做 per-minute 限流。
|
||||
/// 超限时返回 429 Too Many Requests + Retry-After header。
|
||||
/// 请求 ID 追踪中间件
|
||||
/// 为每个请求生成唯一 ID,便于日志追踪
|
||||
pub async fn request_id_middleware(
|
||||
State(_state): State<AppState>,
|
||||
mut req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response<Body> {
|
||||
let request_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
req.extensions_mut().insert(request_id.clone());
|
||||
|
||||
let mut response = next.run(req).await;
|
||||
|
||||
if let Ok(value) = HeaderValue::from_str(&request_id) {
|
||||
response.headers_mut().insert("X-Request-ID", value);
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// API 版本控制中间件
|
||||
/// 在响应头中添加版本信息
|
||||
pub async fn api_version_middleware(
|
||||
State(_state): State<AppState>,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response<Body> {
|
||||
let mut response = next.run(req).await;
|
||||
|
||||
response.headers_mut().insert("X-API-Version", HeaderValue::from_static("1.0.0"));
|
||||
response.headers_mut().insert("X-API-Deprecated", HeaderValue::from_static("false"));
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
/// 速率限制中间件
|
||||
/// 基于账号的请求频率限制
|
||||
pub async fn rate_limit_middleware(
|
||||
State(state): State<AppState>,
|
||||
req: Request,
|
||||
req: Request<Body>,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// 从 AuthContext 提取 account_id(由 auth_middleware 在此之前注入)
|
||||
let account_id = req
|
||||
.extensions()
|
||||
.get::<crate::auth::types::AuthContext>()
|
||||
.map(|ctx| ctx.account_id.clone());
|
||||
|
||||
let account_id = match account_id {
|
||||
Some(id) => id,
|
||||
None => return next.run(req).await,
|
||||
};
|
||||
|
||||
) -> Response<Body> {
|
||||
let account_id = req.extensions()
|
||||
.get::<AuthContext>()
|
||||
.map(|ctx| ctx.account_id.clone())
|
||||
.unwrap_or_else(|| "anonymous".to_string());
|
||||
|
||||
let config = state.config.read().await;
|
||||
let rpm = config.rate_limit.requests_per_minute as u64;
|
||||
let burst = config.rate_limit.burst as u64;
|
||||
let max_requests = rpm + burst;
|
||||
drop(config);
|
||||
|
||||
let rate_limit = config.rate_limit.requests_per_minute as usize;
|
||||
|
||||
let key = format!("rate_limit:{}", account_id);
|
||||
|
||||
let now = Instant::now();
|
||||
let window_start = now - std::time::Duration::from_secs(60);
|
||||
|
||||
// 滑动窗口: 清理过期条目 + 计数
|
||||
let current_count = {
|
||||
let mut entries = state.rate_limit_entries.entry(account_id.clone()).or_default();
|
||||
entries.retain(|&ts| ts > window_start);
|
||||
let count = entries.len() as u64;
|
||||
if count < max_requests {
|
||||
entries.push(now);
|
||||
0 // 未超限
|
||||
} else {
|
||||
count
|
||||
}
|
||||
};
|
||||
|
||||
if current_count >= max_requests {
|
||||
// 计算最早条目的过期时间作为 Retry-After
|
||||
let retry_after = if let Some(mut entries) = state.rate_limit_entries.get_mut(&account_id) {
|
||||
entries.sort();
|
||||
let earliest = *entries.first().unwrap_or(&now);
|
||||
let elapsed = now.duration_since(earliest).as_secs();
|
||||
60u64.saturating_sub(elapsed)
|
||||
} else {
|
||||
60
|
||||
};
|
||||
|
||||
return (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
[
|
||||
("Retry-After", retry_after.to_string()),
|
||||
("Content-Type", "application/json".to_string()),
|
||||
],
|
||||
axum::Json(serde_json::json!({
|
||||
"error": "RATE_LIMITED",
|
||||
"message": format!("请求过于频繁,请在 {} 秒后重试", retry_after),
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
|
||||
let mut entries = state.rate_limit_entries.entry(key).or_insert_with(Vec::new);
|
||||
entries.retain(|&time| time > window_start);
|
||||
|
||||
if entries.len() >= rate_limit {
|
||||
return SaasError::RateLimited(format!(
|
||||
"请求频率超限,每分钟最多 {} 次请求",
|
||||
rate_limit
|
||||
)).into_response();
|
||||
}
|
||||
|
||||
|
||||
entries.push(now);
|
||||
|
||||
next.run(req).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user