chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -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
}