From a7ffd387d950df57f3fedb0803ed226f3533a017 Mon Sep 17 00:00:00 2001 From: milky0217 Date: Mon, 20 Jul 2026 15:08:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AE=BF=E5=AE=A2?= =?UTF-8?q?=E7=99=BB=E5=BD=95=20API=20(POST=20/api/guest-login)=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BD=91=E7=AB=99=E7=8B=AC=E7=AB=8B=E6=94=AF?= =?UTF-8?q?=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 web_guest_login handler: 创建访客用户(免微信 code),返回 JWT - 路由注册在公共区(无需 JWT 认证) - /payment 页面新增「访客登录」按钮,直接选择套餐支付 - 保留微信小程序登录作为备选 - 已部署到 dev.xmclassmate.top 验证通过 --- src/handlers/auth.rs | 94 +++++++++++++++++++++++++++++++++++++++++ src/handlers/mod.rs | 1 + src/handlers/payment.rs | 67 ++++++++++++++++++++++++++--- src/main.rs | 3 +- 4 files changed, 158 insertions(+), 7 deletions(-) diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index cbf4b17..36ef280 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -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, +} + +/// POST /api/guest-login +/// 网站独立支付用:无需微信 code,直接创建访客用户并返回 JWT +#[post("/api/guest-login")] +pub async fn web_guest_login( + pool: web::Data, + app_state: web::Data, +) -> 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) = + 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), + }; + + 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 控制启用 diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 06b514c..bae1ac8 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -23,6 +23,7 @@ pub use admin::admin_force_confirm_order; pub use admin::admin_refund_order; pub use auth::login; pub use auth::mock_login; +pub use auth::web_guest_login; pub use auth::refresh_token; pub use auth::web_generate_login_code; pub use auth::web_login_confirm; diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index 37d9382..a22952a 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -307,9 +307,17 @@ pub async fn payment_index() -> Result { .btn-login { display: block; width: 100%; background: #07c160; color: #fff; border: none; border-radius: 12px; padding: 16px; font-size: 17px; font-weight: 600; cursor: pointer; margin-bottom: 16px; } .btn-login:disabled { background: #d9d9d9; cursor: not-allowed; } - .btn-refresh { background: #fff; color: #666; border: 1px solid #d9d9d9; } + .btn-refresh { background: #fff; color: #666; border: 1px solid #d9d9d9; } .login-note { font-size: 12px; color: #bbb; margin-top: 12px; } + /* ===== 访客登录 ===== */ + .guest-divider { display: flex; align-items: center; margin: 20px 0; color: #ccc; font-size: 13px; gap: 12px; } + .guest-divider::before, .guest-divider::after { content: ""; flex: 1; border-top: 1px solid #eee; } + .btn-guest { display: block; width: 100%; background: #fff; color: #333; border: 1px solid #d9d9d9; border-radius: 12px; padding: 14px; font-size: 15px; cursor: pointer; margin-bottom: 8px; } + .btn-guest:hover { border-color: #07c160; color: #07c160; } + .btn-guest:disabled { background: #f5f5f5; color: #ccc; cursor: not-allowed; border-color: #eee; } + .guest-hint { font-size: 12px; color: #bbb; text-align: center; margin-bottom: 4px; } + /* ===== 会员区域 ===== */ .paid-section { display: none; } .paid-status { background: linear-gradient(135deg, #07c160, #06ad56); color: #fff; padding: 36px 20px; text-align: center; } @@ -371,18 +379,27 @@ pub async fn payment_index() -> Result {