diff --git a/src/alipay.rs b/src/alipay.rs index bc4c6b6..d3d6652 100644 --- a/src/alipay.rs +++ b/src/alipay.rs @@ -276,7 +276,7 @@ mod signature_tests { assert!(result.is_ok(), "Signing failed: {:?}", result.err()); let signature = result.unwrap(); - println!("Our signature: {}", &signature[..signature.len().min(50)]); + tracing::debug!("Generated signature for test verification ({} chars)", signature.len()); // To verify this matches OpenSSL, you would need to run: // echo -n "test content..." | openssl dgst -sha256 -sign key.pem | base64 diff --git a/src/auth.rs b/src/auth.rs index 7b27902..677d2c2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -77,7 +77,7 @@ pub fn generate_refresh_token(user_id: i32, secret: &str) -> Result Result<(i32, i64), String> { +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))?; diff --git a/src/config.rs b/src/config.rs index 901c0d4..b5732be 100644 --- a/src/config.rs +++ b/src/config.rs @@ -32,7 +32,7 @@ impl AppConfig { let config_dir = manifest_dir.join("config"); let default_path = config_dir.join("default.toml"); - let env_path = config_dir.join(&format!("{}.toml", env)); + let env_path = config_dir.join(format!("{}.toml", env)); let mut settings = toml::Table::new(); @@ -72,11 +72,10 @@ impl AppConfig { if let Ok(val) = std::env::var("APP_RUST_LOG") { settings.insert("rust_log".into(), toml::Value::String(val)); } - if let Ok(val) = std::env::var("APP_FREE_USER_DATA_LIMIT") { - if let Ok(num) = val.parse::() { + if let Ok(val) = std::env::var("APP_FREE_USER_DATA_LIMIT") + && let Ok(num) = val.parse::() { settings.insert("free_user_data_limit".into(), toml::Value::Integer(num)); } - } let app_config: AppConfig = toml::from_str(&settings.to_string()) .map_err(|e| format!("配置反序列化失败: {}", e))?; diff --git a/src/db.rs b/src/db.rs index b428265..574be1a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -11,7 +11,7 @@ use crate::error::AppError; pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result { // 配额检查:非付费用户数据条数限制 let user = get_user_by_id(pool, user_id).await?; - let is_paid_active = user.is_paid && (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > Utc::now()); + let is_paid_active = user.is_paid && user.paid_expires_at.map_or(true, |expires| expires > Utc::now()); if !is_paid_active { let current_count = count_user_weather_data(pool, user_id).await?; @@ -490,8 +490,7 @@ pub async fn get_user_quota( ) -> Result<(i64, bool, Option>), AppError> { let user = get_user_by_id(pool, user_id).await?; let is_paid_active = user.is_paid - && (user.paid_expires_at.is_none() - || user.paid_expires_at.unwrap() > chrono::Utc::now()); + && user.paid_expires_at.map_or(true, |expires| expires > chrono::Utc::now()); let used = count_user_weather_data(pool, user_id).await?; Ok((used, is_paid_active, user.paid_expires_at)) } diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 5b3c4ff..fbff9b6 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -5,16 +5,14 @@ use reqwest::Client; use sqlx::postgres::PgPool; use tracing::{debug, error, info, warn}; -use crate::auth::{generate_token, generate_refresh_token, verify_refresh_token}; +use crate::auth::{generate_token, generate_refresh_token}; use crate::db; -use crate::error::{AppError, ErrorResponse}; -use crate::models::Claims; +use crate::error::ErrorResponse; use crate::models::{ AppState, LoginResponse, RefreshTokenRequest, TokenRefreshResponse, WeChatApiResponse, WeChatLoginRequest, }; use crate::rate_limiter::LOGIN_RATE_LIMITER; -use std::sync::Arc; #[post("/api/login")] pub async fn login( @@ -248,7 +246,7 @@ pub async fn mock_login( let user_id = match query.user_id { Some(id) => { // 验证用户存在 - match sqlx::query_as::<_, (i32,)>(("SELECT id FROM users WHERE id = $1")) + match sqlx::query_as::<_, (i32,)>("SELECT id FROM users WHERE id = $1" ) .bind(id) .fetch_optional(pool.get_ref()) .await @@ -447,8 +445,8 @@ pub async fn web_generate_login_code( .map(|_| { let chars = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; let idx = (Utc::now().timestamp_millis() % 36) as u8; - let ch = chars[(idx % 36) as usize] as char; - ch + + chars[(idx % 36) as usize] as char }) .collect(); let ts = Utc::now().timestamp(); diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index 62e2e00..bcc4122 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -1,12 +1,12 @@ // handlers/payment.rs — 支付相关处理器(接入支付宝) use actix_web::{get, post, web, HttpRequest, HttpResponse}; use chrono::Utc; -use rsa::pkcs1v15::{Pkcs1v15Sign, SigningKey, VerifyingKey}; +use rsa::pkcs1v15::SigningKey; use rsa::pkcs8::DecodePrivateKey; -use rsa::signature::{SignatureEncoding, Signer, Verifier}; +use rsa::signature::{SignatureEncoding, Signer}; use rsa::RsaPrivateKey; use serde::Deserialize; -use sha2::{Digest, Sha256}; +use sha2::Sha256; use sqlx::postgres::PgPool; use std::collections::BTreeMap; use tracing::info; @@ -110,6 +110,54 @@ fn rsa2_sign(content: &str, private_key_pem: &str) -> Result { )) } +/// 合并 mock 支付 HTML(在 payment_page 和 alipay_pay_page 中复用) +fn build_mock_pay_html(order_no: &str, display_name: &str, jwt: &str) -> String { + format!(r#" + +模拟支付 + +

模拟支付环境

+

当前为沙箱模拟环境,无需真实支付

+

订单号: {}

+

套餐: {}

+ +

+

返回重试

+ + +"#, order_no, display_name, jwt, order_no) +} + /// 验证 RSA2 签名 #[allow(dead_code)] fn rsa2_verify(content: &str, sign: &str, public_key_pem: &str) -> Result { @@ -577,51 +625,7 @@ pub async fn payment_page( let Some(config) = AlipayConfig::from_env() else { let jwt_for_mock = token.clone(); - let mock_html = format!(r#" - -模拟支付 - -

模拟支付环境

-

当前为沙箱模拟环境,无需真实支付

-

订单号: {}

-

套餐: {}

- -

-

返回重试

- - - -"#, order_no, pkg.display_name, jwt_for_mock, order_no); + let mock_html = build_mock_pay_html(&order_no, pkg.display_name, &jwt_for_mock); return Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(mock_html)); @@ -676,51 +680,7 @@ pub async fn alipay_pay_page( let Some(config) = AlipayConfig::from_env() else { let jwt_for_mock = token.clone(); - let mock_html = format!(r#" - -模拟支付 - -

模拟支付环境

-

当前为沙箱模拟环境,无需真实支付

-

订单号: {}

-

套餐: {}

- -

-

返回重试

- - - -"#, query.order_no, pkg.display_name, jwt_for_mock, query.order_no); + let mock_html = build_mock_pay_html(&query.order_no, pkg.display_name, &jwt_for_mock); return Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(mock_html)); @@ -860,11 +820,10 @@ pub async fn payment_success( let order_no = query.order_no.as_deref().unwrap_or(""); // 同步确认订单(幂等:已确认的订单会跳过) - if !order_no.is_empty() { - if let Err(e) = db::confirm_payment_order_by_orderno(pool.get_ref(), order_no).await { + if !order_no.is_empty() + && let Err(e) = db::confirm_payment_order_by_orderno(pool.get_ref(), order_no).await { tracing::warn!("支付成功页同步确认失败(可能是异步回调已处理): {}", e); } - } let html = build_success_html(order_no); HttpResponse::Ok() @@ -948,8 +907,10 @@ pub async fn sync_order( ) -> Result { let user_id = claims.user_id; - // 尝试确认订单(幂等) - let _ = db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await; + // 尝试确认订单(幂等),失败时记录日志 + if let Err(e) = db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await { + tracing::warn!("确认订单失败(可能已被异步回调处理): {}", e); + } // 查询最新状态 let user = sqlx::query_as::<_, (bool, Option>)>( @@ -962,7 +923,7 @@ pub async fn sync_order( .ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?; let (is_paid, paid_expires_at) = user; - let is_paid_active = is_paid && (paid_expires_at.is_none() || paid_expires_at.unwrap() > Utc::now()); + let is_paid_active = is_paid && paid_expires_at.map_or(true, |expires| expires > Utc::now()); Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, @@ -1082,7 +1043,7 @@ pub async fn generate_code( pub async fn payment_login_status( pool: web::Data, query: web::Query, - app_state: web::Data, + _app_state: web::Data, ) -> Result { let code = query.code.trim(); diff --git a/src/handlers/user.rs b/src/handlers/user.rs index e9828b1..d20930e 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -17,7 +17,7 @@ pub async fn get_current_user_profile( let user = db::get_user_by_id(pool.get_ref(), user_id).await?; let is_paid_active = user.is_paid && - (user.paid_expires_at.is_none() || user.paid_expires_at.unwrap() > chrono::Utc::now()); + user.paid_expires_at.map_or(true, |expires| expires > chrono::Utc::now()); Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true,