use actix_web::{web, HttpResponse, HttpRequest, Responder, post, get}; use chrono::{Utc, Duration}; use serde::{Deserialize, Serialize}; use reqwest::Client; use sqlx::postgres::PgPool; use tracing::{debug, error, info, warn}; use crate::auth::{generate_token, generate_refresh_token}; use crate::db; use crate::error::ErrorResponse; use crate::models::{ AppState, LoginResponse, RefreshTokenRequest, TokenRefreshResponse, WeChatApiResponse, WeChatLoginRequest, }; use crate::rate_limiter::LOGIN_RATE_LIMITER; #[post("/api/login")] pub async fn login( pool: web::Data, req: web::Json, http_client: web::Data, app_state: web::Data, 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(|| "unknown".to_string()); if let Err(e) = LOGIN_RATE_LIMITER.check_rate_limit(&client_ip).await { warn!("登录请求被限流: client_ip={}", client_ip); return HttpResponse::TooManyRequests().json(ErrorResponse::<()>::error(e.to_string())); } let url = format!( "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code", app_state.wechat_appid, app_state.wechat_secret, req.code ); debug!("准备调用微信API: {}", url.replace(&app_state.wechat_secret, "***")); let wechat_response = match http_client.get(&url).send().await { Ok(response) => { debug!("微信API请求成功,状态码: {}", response.status()); response } Err(e) => { error!("微信API请求失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("请求微信服务失败")); } }; let wechat_data: WeChatApiResponse = match wechat_response.json().await { Ok(data) => { debug!("微信API响应解析成功"); data } Err(e) => { error!("微信API响应解析失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("解析微信响应失败")); } }; if let Some(errcode) = wechat_data.errcode { warn!( "微信登录验证失败 - 错误码: {}, 错误信息: {:?}", errcode, wechat_data.errmsg ); let errmsg = wechat_data.errmsg.unwrap_or_default(); return HttpResponse::BadRequest().json(ErrorResponse::<()>::error(format!( "微信登录失败: {} {}", errcode, errmsg ))); } let openid = match wechat_data.openid { Some(id) => { let masked_openid = format!("{}***{}", &id[0..4], &id[id.len() - 4..]); debug!("获取到用户openid: {}", masked_openid); id } None => { error!("微信API响应中未包含openid"); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("微信API未返回openid")); } }; let upsert_query = r#" INSERT INTO users (openid, name, type) VALUES ($1, left($1, 8), 2) ON CONFLICT (openid) DO UPDATE SET id = users.id RETURNING id "#; let user_id = match sqlx::query_as::<_, (i32,)>(upsert_query) .bind(&openid) .fetch_one(pool.get_ref()) .await { Ok((id,)) => { info!("用户登录/注册成功,用户ID: {}", id); id } Err(e) => { error!("用户信息处理失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("用户信息处理失败")); } }; let token = match generate_token(user_id, &openid, 2, &app_state.jwt_secret) { Ok(t) => { debug!("JWT生成成功: {}", &t[0..8]); t } Err(e) => { error!("JWT生成失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("生成身份令牌失败")); } }; 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 { error!("保存 refresh_token 失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("保存刷新令牌失败")); } let login_response = LoginResponse::new(token, refresh_token_str); HttpResponse::Ok().json(login_response) } #[post("/api/refresh-token")] pub async fn refresh_token( pool: web::Data, req: web::Json, app_state: web::Data, 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 { Ok(record) => record, Err(e) => { warn!("Refresh token 验证失败: {}", e); return HttpResponse::Unauthorized().json(ErrorResponse::<()>::error("Refresh token 无效或已过期")); } }; let user_id = refresh_token_record.user_id; let user_query = r#"SELECT openid FROM users WHERE id = $1"#; let openid: String = match sqlx::query_as::<_, (String,)>(user_query) .bind(user_id) .fetch_optional(pool.get_ref()) .await { Ok(Some((openid,))) => openid, Ok(None) => { error!("用户不存在: {}", user_id); return HttpResponse::Unauthorized().json(ErrorResponse::<()>::error("用户不存在")); } Err(e) => { error!("查询用户失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("查询用户信息失败")); } }; let new_access_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("生成令牌失败")); } }; let new_refresh_token = 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("生成刷新令牌失败")); } }; if let Err(e) = db::delete_specific_refresh_token(pool.get_ref(), refresh_token).await { warn!("删除旧 refresh_token 失败(继续): {}", e); } let new_expires_at = Utc::now() + Duration::days(7); if let Err(e) = db::create_refresh_token(pool.get_ref(), user_id, &new_refresh_token, new_expires_at).await { error!("保存新 refresh_token 失败: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse::<()>::error("保存刷新令牌失败")); } let response = TokenRefreshResponse { success: true, token: new_access_token, refresh_token: new_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 + MOCK_LOGIN_KEY 控制启用 /// MOCK_LOGIN_ENABLED=true 且未设 MOCK_LOGIN_KEY:任何请求可用(不安全) /// MOCK_LOGIN_ENABLED=true 且设了 MOCK_LOGIN_KEY:需携带 X-Mock-Key 请求头 #[get("/api/mock-login")] pub async fn mock_login( pool: web::Data, query: web::Query, app_state: web::Data, req: HttpRequest, ) -> impl Responder { // 检查是否启用 if std::env::var("MOCK_LOGIN_ENABLED").ok() != Some("true".to_string()) { return HttpResponse::NotFound().json(ErrorResponse::<()>::error("模拟登录未启用")); } // 如果设置了 MOCK_LOGIN_KEY,验证请求头 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("模拟登录密钥错误")); } } let user_id = match query.user_id { Some(id) => { // 验证用户存在,同时检查是否管理员 match sqlx::query_as::<_, (i32, bool)>("SELECT id, is_admin FROM users WHERE id = $1" ) .bind(id) .fetch_optional(pool.get_ref()) .await { Ok(Some((uid, is_admin))) => { if is_admin { return HttpResponse::Forbidden() .json(ErrorResponse::<()>::error("模拟登录不能用于管理员账户")); } 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, }) } // ===== 网页端微信扫码登录 ===== /// 生成网页端登录码(小程序调用) /// POST /api/web-login/code /// Body: { code: string } (小程序的微信登录 code) #[derive(Debug, Deserialize)] pub struct WebLoginCodeRequest { /// 小程序 wx.login() 得到的 code pub code: String, } /// 登录码生成响应 #[derive(Debug, Serialize)] pub struct WebLoginCodeResponse { pub success: bool, /// 展示给用户的登录码,如 "ASD-XR7K2M" pub display_code: String, /// 轮询用的简短码(不含前缀,方便输入) pub short_code: String, pub expires_in: i64, // 有效期秒数 } /// 生成登录码:后端先用 code 换 openid(不创建 JWT),存入临时表 #[post("/api/web-login/code")] pub async fn web_generate_login_code( pool: web::Data, req: web::Json, http_client: web::Data, app_state: web::Data, ) -> impl Responder { let code = req.code.trim(); // 1. 用 code 换取 openid(和登录流程一样) let url = format!( "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code", app_state.wechat_appid, app_state.wechat_secret, code ); let wechat_response = match http_client.get(&url).send().await { Ok(r) => r, Err(e) => { error!("微信 API 请求失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("微信服务请求失败")); } }; let wechat_data: WeChatApiResponse = match wechat_response.json().await { Ok(d) => d, Err(e) => { error!("微信响应解析失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("微信响应解析失败")); } }; if let Some(errcode) = wechat_data.errcode { let errmsg = wechat_data.errmsg.unwrap_or_default(); error!("微信 code 换取 openid 失败: {} - {}", errcode, errmsg); return HttpResponse::BadRequest() .json(ErrorResponse::<()>::error(format!("微信登录失败: {}", errmsg))); } let openid = match wechat_data.openid { Some(o) => o, None => { return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("未获取到 openid")); } }; // 2. 通过 openid 查询或创建用户(不依赖 JWT) let user_id: i32 = match sqlx::query_as::<_, (i32,)>( r#" INSERT INTO users (openid, name, type) VALUES ($1, left($1, 8), 2) ON CONFLICT (openid) DO UPDATE SET id = users.id RETURNING id "#, ) .bind(&openid) .fetch_one(pool.get_ref()) .await { Ok((id,)) => id, Err(e) => { error!("用户查询/创建失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("用户处理失败")); } }; // 3. 清理该 openid 的旧登录码(避免重复使用) if let Err(e) = sqlx::query("DELETE FROM web_login_codes WHERE openid = $1") .bind(&openid) .execute(pool.get_ref()) .await { warn!("清理旧登录码失败(继续): {}", e); } // 3. 生成随机登录码 let random_part: String = (0..6) .map(|_| { let chars = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; let idx = (Utc::now().timestamp_millis() % 36) as u8; chars[(idx % 36) as usize] as char }) .collect(); let ts = Utc::now().timestamp(); let short_code = format!( "{:X}{}", ts % 0xFFFF, &random_part[..4] ); let display_code = format!("ASD-{}", &short_code[..7].to_uppercase()); let code_for_db = display_code.clone(); // 4. 存入临时表(关联 user_id) let expires_at = Utc::now() + chrono::Duration::minutes(10); if let Err(e) = sqlx::query( "INSERT INTO web_login_codes (code, openid, user_id, expires_at) VALUES ($1, $2, $3, $4)", ) .bind(&code_for_db) .bind(&openid) .bind(user_id) .bind(expires_at) .execute(pool.get_ref()) .await { error!("保存登录码失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("生成登录码失败")); } info!("[WEB LOGIN CODE] user_id={} openid={} code={}", user_id, openid, display_code); HttpResponse::Ok().json(WebLoginCodeResponse { success: true, display_code, short_code, expires_in: 600, }) } /// 确认网页端登录(小程序调用) /// POST /api/web-login/confirm /// Body: { code: string } /// 后端查询登录码,找到了就生成 JWT,更新 token 字段,删除登录码 #[derive(Debug, Deserialize)] pub struct WebLoginConfirmRequest { pub code: String, } /// 确认登录响应 #[derive(Debug, Serialize)] pub struct WebLoginConfirmResponse { pub success: bool, pub token: Option, pub is_active_member: bool, pub membership_expires_at: Option, } #[post("/api/web-login/confirm")] pub async fn web_login_confirm( pool: web::Data, req: web::Json, app_state: web::Data, ) -> impl Responder { let short_code = req.code.trim(); tracing::info!("[web_login_confirm] received code={}", short_code); // 精确匹配登录码 let record: Option<(String, String, chrono::DateTime, Option)> = sqlx::query_as( "SELECT code, openid, expires_at, user_id FROM web_login_codes WHERE code = $1", ) .bind(short_code) .fetch_optional(pool.get_ref()) .await .inspect_err(|e| tracing::warn!("数据库查询登录码失败: {}", e)) .ok() .flatten(); let (code, openid, expires_at, existing_user_id) = match record { Some(r) => r, None => { return HttpResponse::Ok().json(WebLoginConfirmResponse { success: false, token: None, is_active_member: false, membership_expires_at: None, }); } }; // 检查是否过期 if Utc::now() > expires_at { // 清理过期码 if let Err(e) = sqlx::query("DELETE FROM web_login_codes WHERE code = $1") .bind(&code) .execute(pool.get_ref()) .await { tracing::warn!("清理过期登录码失败: {}", e); } return HttpResponse::Ok().json(WebLoginConfirmResponse { success: false, token: None, is_active_member: false, membership_expires_at: None, }); } // 如果已有用户,验证并获取最新 is_member 信息 let user_id = if let Some(uid) = existing_user_id { uid } else { // 创建或获取用户(UPSERT) let name = format!("user_{}", &openid[..8.min(openid.len())]); 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(&openid) .bind(&name) .fetch_one(pool.get_ref()) .await { Ok((uid,)) => uid, Err(e) => { error!("创建用户失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("创建用户失败")); } } }; // 生成 JWT 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("生成令牌失败")); } }; // 查询付费状态 let (is_active_member, membership_expires_at): (bool, Option) = match sqlx::query_as::<_, (bool, Option>)>( "SELECT is_member, membership_expires_at FROM users WHERE id = $1", ) .bind(user_id) .fetch_optional(pool.get_ref()) .await { Ok(Some((is_member, expires))) => { let active = is_member && expires.is_none_or(|e| e > Utc::now()); (active, expires.map(|e| e.to_rfc3339())) } _ => (false, None), }; // 更新登录码记录,设置 token(而非删除,让轮询接口能查到) if let Err(e) = sqlx::query("UPDATE web_login_codes SET token = $1 WHERE code = $2") .bind(&token) .bind(&code) .execute(pool.get_ref()) .await { tracing::warn!("更新登录码 token 失败: {}", e); } info!("[WEB LOGIN CONFIRM] user_id={} is_member={}", user_id, is_active_member); HttpResponse::Ok().json(WebLoginConfirmResponse { success: true, token: Some(token), is_active_member, membership_expires_at, }) } #[derive(Debug, Deserialize)] pub struct AutoConfirmRequest { pub code: String, } #[derive(Debug, Serialize)] pub struct AutoConfirmResponse { pub success: bool, pub token: Option, pub is_active_member: bool, pub membership_expires_at: Option, pub payment_url: Option, } #[post("/api/web-login/auto-confirm")] pub async fn web_login_auto_confirm( pool: web::Data, http_client: web::Data, app_state: web::Data, req: web::Json, ) -> impl Responder { let code = req.code.clone(); let openid = if (code.starts_with("mock_") || code == "test_mock") && std::env::var("MOCK_LOGIN_ENABLED").ok() == Some("true".to_string()) { format!("mock_openid_{}", Utc::now().timestamp_millis()) } else { let url = format!( "https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code", app_state.wechat_appid, app_state.wechat_secret, code ); let wechat_response = match http_client.get(&url).send().await { Ok(r) => r, Err(e) => { error!("微信 API 请求失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("微信服务请求失败")); } }; let wechat_data: WeChatApiResponse = match wechat_response.json().await { Ok(d) => d, Err(e) => { error!("微信响应解析失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("微信响应解析失败")); } }; if let Some(errcode) = wechat_data.errcode { let errmsg = wechat_data.errmsg.unwrap_or_default(); error!("微信 code 换取 openid 失败: {} - {}", errcode, errmsg); return HttpResponse::BadRequest() .json(ErrorResponse::<()>::error(format!("微信登录失败: {}", errmsg))); } match wechat_data.openid { Some(o) => o, None => { return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("未获取到 openid")); } } }; let user_id: i32 = match sqlx::query_as::<_, (i32,)>( r#" INSERT INTO users (openid, name, type) VALUES ($1, left($1, 8), 2) ON CONFLICT (openid) DO UPDATE SET id = users.id RETURNING id "#, ) .bind(&openid) .fetch_one(pool.get_ref()) .await { Ok((id,)) => id, Err(e) => { error!("用户创建/查询失败: {}", e); return HttpResponse::InternalServerError() .json(ErrorResponse::<()>::error("用户处理失败")); } }; 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("生成令牌失败")); } }; let paid_info = sqlx::query_as::<_, (bool, Option>)>( "SELECT is_member, membership_expires_at FROM users WHERE id = $1", ) .bind(user_id) .fetch_optional(pool.get_ref()) .await .ok() .flatten(); let (is_active_member, membership_expires_at): (bool, Option) = match paid_info { Some((is_member, expires)) => { let active = is_member && expires.is_none_or(|e| e > Utc::now()); (active, expires.map(|e| e.to_rfc3339())) } None => (false, None), }; let base_url = std::env::var("APP_BASE_URL") .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); let payment_url = if is_active_member { None } else { Some(format!("{}/payment?jwt={}", base_url, token)) }; info!("[WEB LOGIN AUTO-CONFIRM] user_id={} is_member={}", user_id, is_active_member); HttpResponse::Ok().json(AutoConfirmResponse { success: true, token: Some(token), is_active_member, membership_expires_at, payment_url, }) }