feat(auth): 添加沙箱测试用 mock_login 接口 (GET /api/mock-login)

This commit is contained in:
2026-04-23 13:11:10 +08:00
parent 502ae8babb
commit ea705f0917
3 changed files with 131 additions and 2 deletions

View File

@@ -1,5 +1,6 @@
use actix_web::{web, HttpResponse, Responder, post}; use actix_web::{web, HttpResponse, Responder, post, get};
use chrono::{Utc, Duration}; use chrono::{Utc, Duration};
use serde::{Deserialize, Serialize};
use reqwest::Client; use reqwest::Client;
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
@@ -190,3 +191,129 @@ pub async fn refresh_token(
}; };
HttpResponse::Ok().json(response) HttpResponse::Ok().json(response)
} }
/// 模拟登录请求(仅供测试/沙箱环境使用)
#[derive(Debug, Deserialize)]
pub struct MockLoginRequest {
/// 指定用户 ID如不指定则自动创建测试用户
pub user_id: Option<i32>,
}
/// 模拟登录响应
#[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<PgPool>,
query: web::Query<MockLoginRequest>,
app_state: web::Data<AppState>,
) -> 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,
})
}

View File

@@ -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_get_user;
pub use admin::admin_update_user_payment; pub use admin::admin_update_user_payment;
pub use auth::login; pub use auth::login;
pub use auth::mock_login;
pub use auth::refresh_token; pub use auth::refresh_token;
pub use favorites::{add_favorite, get_favorites, remove_favorite}; pub use favorites::{add_favorite, get_favorites, remove_favorite};
pub use health::health_check; pub use health::health_check;

View File

@@ -20,7 +20,7 @@ use handlers::{
admin_get_user, admin_update_user_payment, add_favorite, alipay_notify, alipay_pay_page, 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, create_order, delete_weather, generate_temp_token_handler, get_current_user_profile,
get_favorites, get_user_quota, get_weather_brief, get_weather_details, 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, post_weather_data,
refresh_token, remove_favorite, root, save_user_profile, serve_static_files, 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))) .service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
// API 接口 // API 接口
.service(login) // #[post("/api/login")] .service(login) // #[post("/api/login")]
.service(mock_login) // #[get("/api/mock-login")](沙箱测试用)
.service(refresh_token) // #[post("/api/refresh-token")](公开接口,无需认证) .service(refresh_token) // #[post("/api/refresh-token")](公开接口,无需认证)
.service(get_weather_details) // #[get("/weather/details")](支持 JWT 或 temp_token公开接口 .service(get_weather_details) // #[get("/weather/details")](支持 JWT 或 temp_token公开接口
.service(health_check) // #[get("/health")](公开接口,无需认证) .service(health_check) // #[get("/health")](公开接口,无需认证)