优化: Keep-Alive/rate-limiter/dead-code清理 + deploy.sh蓝绿自动检测 + 压测文档
This commit is contained in:
@@ -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))?;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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"#;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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!({
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user