优化: Keep-Alive/rate-limiter/dead-code清理 + deploy.sh蓝绿自动检测 + 压测文档

This commit is contained in:
2026-07-03 19:45:52 +08:00
parent 6d066c8e82
commit 178008496e
18 changed files with 645 additions and 57 deletions

View File

@@ -77,6 +77,7 @@ pub fn generate_refresh_token(user_id: i32, secret: &str) -> Result<String, Stri
}
// 解析 refresh_token返回 (user_id, expires_at)
#[allow(dead_code)]
pub fn verify_refresh_token(token: &str, _secret: &str) -> Result<(i32, i64), String> {
let decoded = BASE64.decode(token)
.map_err(|e| format!("Refresh token 格式错误: {}", e))?;

View File

@@ -42,18 +42,6 @@ impl AppConfig {
Ok(app_config)
}
pub fn database_url(&self) -> &str {
&self.database_url
}
pub fn rust_log(&self) -> &str {
&self.rust_log
}
pub fn is_production(&self) -> bool {
self.environment == "production"
}
}
fn env_or_fail(key: &str) -> Result<String, String> {

View File

@@ -11,7 +11,7 @@ use crate::error::AppError;
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, AppError> {
// 配额检查:非付费用户数据条数限制
let user = get_user_by_id(pool, user_id).await?;
let is_active_member = user.is_member && user.membership_expires_at.map_or(true, |expires| expires > Utc::now());
let is_active_member = user.is_member && user.membership_expires_at.is_none_or(|expires| expires > Utc::now());
// 维护模式:非会员使用更高的临时限额,防止资源滥用
let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string());
@@ -800,7 +800,7 @@ pub async fn get_user_quota(
) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>, bool), AppError> {
let user = get_user_by_id(pool, user_id).await?;
let is_active_member = user.is_member
&& user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now());
&& user.membership_expires_at.is_none_or(|expires| expires > chrono::Utc::now());
let used = count_user_weather_data(pool, user_id).await?;
Ok((used, is_active_member, user.membership_expires_at, user.is_member))
}
@@ -879,10 +879,14 @@ pub async fn set_weather_favorite(
#[derive(Debug, FromRow)]
pub struct RefreshToken {
#[allow(dead_code)]
pub id: i32,
pub user_id: i32,
#[allow(dead_code)]
pub token: String,
#[allow(dead_code)]
pub expires_at: chrono::DateTime<chrono::Utc>,
#[allow(dead_code)]
pub created_at: chrono::DateTime<chrono::Utc>,
}
@@ -926,6 +930,7 @@ pub async fn verify_refresh_token(
.ok_or_else(|| AppError::Unauthorized("Refresh token 无效或已过期".to_string()))
}
#[allow(dead_code)]
pub async fn delete_refresh_token(pool: &PgPool, user_id: i32) -> Result<(), AppError> {
let query = r#"DELETE FROM refresh_tokens WHERE user_id = $1"#;

View File

@@ -11,6 +11,7 @@ pub struct ErrorResponse<T = ()> {
}
impl<T> ErrorResponse<T> {
#[allow(dead_code)]
pub fn success(data: T) -> Self {
Self {
success: true,
@@ -29,12 +30,14 @@ impl<T> ErrorResponse<T> {
}
impl<T: Serialize> ErrorResponse<T> {
#[allow(dead_code, clippy::wrong_self_convention)]
pub fn to_json_response(self) -> HttpResponse {
HttpResponse::Ok().json(self)
}
}
impl ErrorResponse<()> {
#[allow(dead_code)]
pub fn to_error_response(&self, status: StatusCode) -> HttpResponse {
let body = serde_json::json!({
"success": false,

View File

@@ -149,7 +149,31 @@ pub async fn refresh_token(
pool: web::Data<PgPool>,
req: web::Json<RefreshTokenRequest>,
app_state: web::Data<AppState>,
http_req: HttpRequest,
) -> impl Responder {
let client_ip = http_req
.headers()
.get("X-Forwarded-For")
.and_then(|v| v.to_str().ok())
.map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
.or_else(|| {
http_req
.headers()
.get("X-Real-IP")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.unwrap_or_else(|| {
http_req
.peer_addr()
.map(|a| a.ip().to_string())
.unwrap_or_else(|| "unknown".to_string())
});
if let Err(e) = LOGIN_RATE_LIMITER.check_rate_limit(&client_ip).await {
warn!("refresh_token 请求被限流: client_ip={}", client_ip);
return HttpResponse::TooManyRequests().json(ErrorResponse::<()>::error(e.to_string()));
}
let refresh_token = &req.refresh_token;
let refresh_token_record = match db::verify_refresh_token(pool.get_ref(), refresh_token).await {
@@ -247,15 +271,13 @@ pub async fn mock_login(
}
// 如果设置了 MOCK_LOGIN_KEY验证请求头
if let Ok(key) = std::env::var("MOCK_LOGIN_KEY") {
if !key.is_empty() {
let header_key = req.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return HttpResponse::Forbidden().json(ErrorResponse::<()>::error("模拟登录密钥错误"));
}
if let Ok(key) = std::env::var("MOCK_LOGIN_KEY") && !key.is_empty() {
let header_key = req.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return HttpResponse::Forbidden().json(ErrorResponse::<()>::error("模拟登录密钥错误"));
}
}
@@ -613,7 +635,7 @@ pub async fn web_login_confirm(
.await
{
Ok(Some((is_member, expires))) => {
let active = is_member && expires.map_or(true, |e| e > Utc::now());
let active = is_member && expires.is_none_or(|e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
_ => (false, None),
@@ -745,7 +767,7 @@ pub async fn web_login_auto_confirm(
let (is_active_member, membership_expires_at): (bool, Option<String>) = match paid_info {
Some((is_member, expires)) => {
let active = is_member && expires.map_or(true, |e| e > Utc::now());
let active = is_member && expires.is_none_or(|e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
None => (false, None),

View File

@@ -849,8 +849,8 @@ fn urlencoding(s: &str) -> String {
while let Some(b) = chars.next() {
if b == b'%' {
had_escape = true;
let hi = chars.next().and_then(|c| hex_val(c));
let lo = chars.next().and_then(|c| hex_val(c));
let hi = chars.next().and_then(hex_val);
let lo = chars.next().and_then(hex_val);
if let (Some(h), Some(l)) = (hi, lo) {
result.push((h << 4 | l) as char);
} else {
@@ -1171,16 +1171,14 @@ fn check_mock_payment_allowed(req: &HttpRequest) -> Result<(), AppError> {
}
// 规则 3如果设了 MOCK_PAY_KEY验证请求头
if let Ok(key) = std::env::var("MOCK_PAY_KEY") {
if !key.is_empty() {
let header_key = req
.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return Err(AppError::Forbidden("Mock 支付密钥错误".to_string()));
}
if let Ok(key) = std::env::var("MOCK_PAY_KEY") && !key.is_empty() {
let header_key = req
.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return Err(AppError::Forbidden("Mock 支付密钥错误".to_string()));
}
}
@@ -1246,7 +1244,7 @@ pub async fn sync_order(
.ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?;
let (is_member, membership_expires_at) = user;
let is_active_member = is_member && membership_expires_at.map_or(true, |expires| expires > Utc::now());
let is_active_member = is_member && membership_expires_at.is_none_or(|expires| expires > Utc::now());
Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true,
@@ -1407,6 +1405,7 @@ pub async fn payment_login_status(
let code = query.code.trim();
// 查询登录码记录(包含 token 字段用于判断是否已确认)
#[allow(clippy::type_complexity)]
let record: Option<(String, chrono::DateTime<chrono::Utc>, Option<String>, Option<i32>)> =
sqlx::query_as(
"SELECT code, expires_at, token, user_id FROM web_login_codes WHERE code = $1",
@@ -1472,7 +1471,7 @@ pub async fn payment_login_status(
.await
{
Ok(Some((is_member, expires))) => {
let active = is_member && expires.map_or(true, |e| e > Utc::now());
let active = is_member && expires.is_none_or(|e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
_ => (false, None),

View File

@@ -3,8 +3,10 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct SentryEventRequest {
#[allow(dead_code)]
pub level: Option<String>,
pub message: String,
#[allow(dead_code)]
pub stack: Option<String>,
pub page: Option<String>,
pub user_id: Option<i32>,
@@ -28,11 +30,11 @@ pub async fn report_frontend_error(body: web::Json<SentryEventRequest>) -> HttpR
..Default::default()
}));
}
if let Some(extra) = &body.extra {
if let Some(obj) = extra.as_object() {
for (k, v) in obj {
scope.set_extra(k, sentry::protocol::Value::from(v.clone()));
}
if let Some(extra) = &body.extra
&& let Some(obj) = extra.as_object()
{
for (k, v) in obj {
scope.set_extra(k, v.clone());
}
}
},

View File

@@ -21,7 +21,7 @@ pub async fn get_current_user_profile(
true
} else {
user.is_member &&
user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now())
user.membership_expires_at.is_none_or(|expires| expires > chrono::Utc::now())
};
Ok(HttpResponse::Ok().json(serde_json::json!({

View File

@@ -324,6 +324,8 @@ async fn main() -> std::io::Result<()> {
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.keep_alive(std::time::Duration::from_secs(30))
.backlog(1024)
.bind_openssl(&addr, ssl_builder)
{
Ok(s) => {
@@ -345,6 +347,8 @@ async fn main() -> std::io::Result<()> {
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.keep_alive(std::time::Duration::from_secs(30))
.backlog(1024)
.bind(&addr)
{
Ok(s) => {

View File

@@ -52,12 +52,14 @@ impl LoginResponse {
}
// 兼容旧的 TokenResponse
#[allow(dead_code)]
#[derive(Debug, Serialize, Clone)]
pub struct TokenResponse {
pub success: bool,
pub token: String,
}
#[allow(dead_code)]
impl TokenResponse {
pub fn new(token: String) -> Self {
Self {
@@ -345,11 +347,16 @@ pub struct AppState {
pub jwt_secret: String,
pub wechat_appid: String,
pub wechat_secret: String,
#[allow(dead_code)]
pub free_user_data_limit: i32,
// ===== 支付宝配置 =====
#[allow(dead_code)]
pub alipay_app_id: Option<String>,
#[allow(dead_code)]
pub alipay_private_key: Option<String>,
#[allow(dead_code)]
pub alipay_alipay_public_key: Option<String>,
#[allow(dead_code)]
pub alipay_gateway: Option<String>,
}

View File

@@ -40,6 +40,7 @@ impl RateLimiter {
}
}
#[allow(dead_code)]
pub fn extract_client_ip_from_header(headers: &actix_web::http::header::HeaderMap) -> String {
headers
.get("X-Forwarded-For")