Files
asd-backend/src/handlers/auth.rs
milky0217 2ce97243ab
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
fix: 全量代码审计修复 — 30项
P0 - Panic 风险修复:
  - payment.rs: unwrap() → let-else safe handling
  - payment.rs: get_jwt_secret() expect → Result/AppError
  - auth.rs: openid 切片添加 len >= 8 守卫
  - main.rs: 启动时 expect → unwrap_or_else 描述性 panic
  - main.rs: Directive::from_str 添加 fallback

P1 - 逻辑/安全修复:
  - payment.rs: urlencoding() + 解码 bug 修复 (移除 had_escape)
  - payment.rs: Mock 支付添加 check_mock_payment_allowed 检查
  - db.rs: 永久会员 NULL → 2099-12-31 一致化
  - user.rs: 维护模式添加安全说明注释
  - 自动清理 unused_variables 警告 (_is_mobile)

P2 - 错误吞没修复:
  - main.rs: 3 处定时任务 let _ = → if let Err = tracing::error!
  - db.rs + admin.rs: 7 处通知/审计日志 let _ = → tracing::warn!
  - auth.rs: refresh token 保存 add warn 日志

P3 - 死代码清理:
  - models.rs: 移除 TokenResponse (dead)
  - models.rs: 移除 AppState 中 5 个未使用字段 (env var 直接读取)
  - error.rs: 移除 3 个 dead ErrorResponse 方法
  - rate_limiter.rs: extract_client_ip_from_header → #[cfg(test)]
  - models.rs: 注释 typo fix (user_ytpe → user_type)
  - db.rs: RefreshToken 添加 deserialization 注释

Shell 脚本修复:
  - deploy.sh: run_migrations 移到 restart_service 之前
  - test.sh: 移除 EXIT trap 覆盖; heredoc 引号修复; 维护模式添加 restart
  - common.sh: mock_key 添加 sed 转义 (防 / & 注入)

验证: cargo check 0 warnings, 8 tests passed
2026-07-23 12:40:47 +08:00

897 lines
30 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 uuid::Uuid;
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 = if id.len() >= 8 {
format!("{}***{}", &id[0..4], &id[id.len() - 4..])
} else {
id.to_string()
};
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,
}
// ===== 访客登录(免微信,直接网站支付用) =====
#[derive(Debug, Serialize)]
pub struct GuestLoginResponse {
pub success: bool,
pub token: String,
pub refresh_token: String,
pub user_id: i32,
pub is_active_member: bool,
pub membership_expires_at: Option<String>,
}
/// POST /api/guest-login
/// 网站独立支付用:无需微信 code直接创建访客用户并返回 JWT
#[post("/api/guest-login")]
pub async fn web_guest_login(
pool: web::Data<PgPool>,
app_state: web::Data<AppState>,
) -> impl Responder {
let guest_openid = format!("web_guest_{}", Uuid::new_v4());
let guest_name = format!("访客_{}", &guest_openid[10..18]);
// 创建访客用户
let user_id = 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(&guest_openid)
.bind(&guest_name)
.fetch_one(pool.get_ref())
.await
{
Ok((id,)) => id,
Err(e) => {
error!("创建访客用户失败: {}", e);
return HttpResponse::InternalServerError()
.json(ErrorResponse::<()>::error("创建用户失败"));
}
};
// 生成 JWT
let token = match generate_token(user_id, &guest_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);
}
// 查询付费状态
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),
};
info!("[GUEST LOGIN] user_id={} 访客登录成功", user_id);
HttpResponse::Ok().json(GuestLoginResponse {
success: true,
token,
refresh_token: refresh_token_str,
user_id,
is_active_member,
membership_expires_at,
})
}
/// 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,
})
}