diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index c2e0660..a1359d7 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -1,5 +1,6 @@ -use actix_web::{web, HttpResponse, Responder, post}; +use actix_web::{web, HttpResponse, Responder, post, get}; use chrono::{Utc, Duration}; +use serde::{Deserialize, Serialize}; use reqwest::Client; use sqlx::postgres::PgPool; use tracing::{debug, error, info, warn}; @@ -190,3 +191,129 @@ pub async fn refresh_token( }; HttpResponse::Ok().json(response) } + +/// 模拟登录请求(仅供测试/沙箱环境使用) +#[derive(Debug, Deserialize)] +pub struct MockLoginRequest { + /// 指定用户 ID(如不指定则自动创建测试用户) + pub user_id: Option, +} + +/// 模拟登录响应 +#[derive(Debug, Serialize)] +pub struct MockLoginResponse { + pub success: bool, + pub token: String, + pub refresh_token: String, + pub user_id: i32, +} + +/// GET /api/mock-login +/// 沙箱测试用:无需微信 code,直接返回 JWT +/// 通过环境变量 MOCK_LOGIN_ENABLED 控制是否启用 +#[get("/api/mock-login")] +pub async fn mock_login( + pool: web::Data, + query: web::Query, + app_state: web::Data, +) -> impl Responder { + // 检查是否启用 + if std::env::var("MOCK_LOGIN_ENABLED").ok() != Some("true".to_string()) { + return HttpResponse::NotFound().json(ErrorResponse::<()>::error("模拟登录未启用")); + } + + let user_id = match query.user_id { + Some(id) => { + // 验证用户存在 + match sqlx::query_as::<_, (i32,)>(("SELECT id FROM users WHERE id = $1")) + .bind(id) + .fetch_optional(pool.get_ref()) + .await + { + Ok(Some((uid,))) => uid, + Ok(None) => { + return HttpResponse::BadRequest() + .json(ErrorResponse::<()>::error("用户不存在")); + } + Err(e) => { + error!("查询用户失败: {}", e); + return HttpResponse::InternalServerError() + .json(ErrorResponse::<()>::error("数据库错误")); + } + } + } + None => { + // 创建或获取测试用户 + let test_openid = format!("mock_test_{}", Utc::now().timestamp_millis()); + match sqlx::query_as::<_, (i32,)>( + r#"INSERT INTO users (openid, name, type) VALUES ($1, $2, 2) + ON CONFLICT (openid) DO UPDATE SET id = users.id RETURNING id"#, + ) + .bind(&test_openid) + .bind(&test_openid[..8.min(test_openid.len())]) + .fetch_one(pool.get_ref()) + .await + { + Ok((id,)) => id, + Err(e) => { + error!("创建测试用户失败: {}", e); + return HttpResponse::InternalServerError() + .json(ErrorResponse::<()>::error("创建测试用户失败")); + } + } + } + }; + + // 获取 openid + let openid: String = match sqlx::query_as::<_, (String,)>("SELECT openid FROM users WHERE id = $1") + .bind(user_id) + .fetch_optional(pool.get_ref()) + .await + { + Ok(Some((o,))) => o, + Ok(None) => { + return HttpResponse::InternalServerError() + .json(ErrorResponse::<()>::error("用户无 openid")); + } + Err(e) => { + error!("查询 openid 失败: {}", e); + return HttpResponse::InternalServerError() + .json(ErrorResponse::<()>::error("数据库错误")); + } + }; + + // 生成 token + let token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) { + Ok(t) => t, + Err(e) => { + error!("JWT 生成失败: {}", e); + return HttpResponse::InternalServerError() + .json(ErrorResponse::<()>::error("生成令牌失败")); + } + }; + + // 生成 refresh_token + let refresh_token_str = match generate_refresh_token(user_id, &app_state.jwt_secret) { + Ok(t) => t, + Err(e) => { + error!("refresh_token 生成失败: {}", e); + return HttpResponse::InternalServerError() + .json(ErrorResponse::<()>::error("生成刷新令牌失败")); + } + }; + + let refresh_expires_at = Utc::now() + Duration::days(7); + if let Err(e) = + db::create_refresh_token(pool.get_ref(), user_id, &refresh_token_str, refresh_expires_at).await + { + warn!("保存 refresh_token 失败(继续): {}", e); + } + + info!("[MOCK LOGIN] user_id={} 登录成功", user_id); + HttpResponse::Ok().json(MockLoginResponse { + success: true, + token, + refresh_token: refresh_token_str, + user_id, + }) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4e4401d..f730bbe 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -18,6 +18,7 @@ pub static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); pub use admin::admin_get_user; pub use admin::admin_update_user_payment; pub use auth::login; +pub use auth::mock_login; pub use auth::refresh_token; pub use favorites::{add_favorite, get_favorites, remove_favorite}; pub use health::health_check; diff --git a/src/main.rs b/src/main.rs index 93acbc4..3be47d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ use handlers::{ admin_get_user, admin_update_user_payment, add_favorite, alipay_notify, alipay_pay_page, create_order, delete_weather, generate_temp_token_handler, get_current_user_profile, get_favorites, get_user_quota, get_weather_brief, get_weather_details, - health_check, login, mock_confirm, payment_index, payment_page, payment_success, + health_check, login, mock_login, mock_confirm, payment_index, payment_page, payment_success, post_weather_data, refresh_token, remove_favorite, root, save_user_profile, serve_static_files, }; @@ -72,6 +72,7 @@ fn create_server_config( .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files))) // API 接口 .service(login) // #[post("/api/login")] + .service(mock_login) // #[get("/api/mock-login")](沙箱测试用) .service(refresh_token) // #[post("/api/refresh-token")](公开接口,无需认证) .service(get_weather_details) // #[get("/weather/details")](支持 JWT 或 temp_token,公开接口) .service(health_check) // #[get("/health")](公开接口,无需认证)