Files
asd-backend/src/handlers/auth.rs

799 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PgPool>,
req: web::Json<WeChatLoginRequest>,
http_client: web::Data<Client>,
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(|| "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<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 {
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<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 + 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<PgPool>,
query: web::Query<MockLoginRequest>,
app_state: web::Data<AppState>,
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<PgPool>,
req: web::Json<WebLoginCodeRequest>,
http_client: web::Data<Client>,
app_state: web::Data<AppState>,
) -> 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<String>,
pub is_active_member: bool,
pub membership_expires_at: Option<String>,
}
#[post("/api/web-login/confirm")]
pub async fn web_login_confirm(
pool: web::Data<PgPool>,
req: web::Json<WebLoginConfirmRequest>,
app_state: web::Data<AppState>,
) -> impl Responder {
let short_code = req.code.trim();
tracing::info!("[web_login_confirm] received code={}", short_code);
// 精确匹配登录码
let record: Option<(String, String, chrono::DateTime<chrono::Utc>, Option<i32>)> =
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<String>) =
match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
"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<String>,
pub is_active_member: bool,
pub membership_expires_at: Option<String>,
pub payment_url: Option<String>,
}
#[post("/api/web-login/auto-confirm")]
pub async fn web_login_auto_confirm(
pool: web::Data<PgPool>,
http_client: web::Data<Client>,
app_state: web::Data<AppState>,
req: web::Json<AutoConfirmRequest>,
) -> 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<chrono::DateTime<chrono::Utc>>)>(
"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<String>) = 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,
})
}