feat: 添加访客登录 API (POST /api/guest-login) 支持网站独立支付
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
- 新增 web_guest_login handler: 创建访客用户(免微信 code),返回 JWT - 路由注册在公共区(无需 JWT 认证) - /payment 页面新增「访客登录」按钮,直接选择套餐支付 - 保留微信小程序登录作为备选 - 已部署到 dev.xmclassmate.top 验证通过
This commit is contained in:
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -253,6 +254,99 @@ pub struct MockLoginResponse {
|
||||
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 控制启用
|
||||
|
||||
Reference in New Issue
Block a user