feat(payment): 支持 URL 参数直接传递 JWT 登录
This commit is contained in:
7
.env
7
.env
@@ -9,3 +9,10 @@ RUST_LOG=info
|
|||||||
APP_VERSION="0.2.0"
|
APP_VERSION="0.2.0"
|
||||||
FREE_USER_DATA_LIMIT=20
|
FREE_USER_DATA_LIMIT=20
|
||||||
SSH_SERVER=root@1panel-server
|
SSH_SERVER=root@1panel-server
|
||||||
|
|
||||||
|
# 支付宝沙箱配置
|
||||||
|
ALIPAY_APP_ID=9021000162697595
|
||||||
|
ALIPAY_PRIVATE_KEY="MIIEogIBAAKCAQEAi6jZv++OYKnuOqXv2tGd88vgn2M7YGItW/YpvZecy2ea45OUhDWgfMUiJpScCVCBAM218pfADc3YLqSMPgIhqKVAxwPZ5/VhWL8nr0REkAToetKvz+voGhY5H8t7JRP5wFJoT0hQoiZoLVJs4qQVIsKx3SG5bzjDQII9gLgYeYdcdWfewqPW970lU7KE09VbOrrd+q3DRzIrcO+JJM6DW6ynU5Ok7nXOUPNWqidfW24qj/9vyo2DgrdAj0UVCGkcRivq5hkRPiwpzT+ycU6Umx/MSY+lMNeqlsAVX6unNqPyRq7R0sL4sgGbCDNyrvu/bs7D8FXwBG3apI4OsZe0NQIDAQABAoIBAHYBGtQJhwT4MHnPocYDnfHUT5x9TTtYWTk92vD+fRaFGSO3nCLF6S/jpcJxiRJ5ujbCmj07qaRATKzEjlcKmAQWdZBzGZjcBf/lrJN0HHaQHxtAkBsgGoKISoYp+YhPw5SUTNzInpET4KyC28v4ln9DgOP2zedDT/9ezLbJVqy/w/B1R6bWYAKeHaf4P3M3aUxua4heZyBAtNpwjKHqpF9t0wZd+Vp7QbxNvrCsFv99e6aow90XyGOkpGlc2cLnsanDrTftIm/2MzUNx4aqJHhvzNPFqjR7TWr2FZGNY/bEWjOJN+Zk2zuTnu2sGgFGn+g7VgMoExltxRClpTsjiwECgYEA53m9nSBLqcP9vs2Lf7e2kDgHsTs5mnz/xT1kxv703VvpJla1LhUZdXLQdC7ZKP65MwXJJdxJaKs2Smc9U9g7s7ZyUeBgn4OeN5zJ8ybWs3bQaI1ayHDQfddS1sODisdCwm/DEcKYeG4U7eU+RyBGU3RVtGLyk+EJRpnzks4O2kECgYEAmnTOSHnG+2AN3Hdb85+YWmKYuzPmF2w3Vzs24eyjlLCtO47smk45LcTwRZe/VH4JZFjEUv4VQexC0GCCMeOxI4+HfSW4LKfZ9QDoVxAQAQwaSv13q3RBGg0QQS1tGks8QPDMdHlPAGUkQbEEu5TYW227p3/l0gvSGHA0e2nj1PUCgYAoqelWhgrkuuqDr2Y3PJXUxhML3oSjEBx6kOt4TefET1YpBwhzHP/I8y+NiYFaw0zTmsuYXBeFTLaP4Y0TgqQSURXKLmmq/Bs6V8/eaE9FXbUE+PgeyDrIMKB73ALqyVCACdQx9YaFeYkxtZj4M8JG/OPleZB1zA2uGnOAsbRIQQKBgEVO5MHg40kpcQ7dAIXxt4vXcnGOeFFKXgrmp1n5te2i2hk/qSwUrdO765VO4qwnYk+FBZO4MBHE0rZul/2LDpHGdiXSJzLinwsxvRBURNozdBjyZCCrqgrmkcStfaeu2vrJEvWw+aJTwFE5/cWupOdh9jLTUW1PoJcWwnYerf2BAoGAVRYA379sCxTOrWdesElRDy2YbXfvhyYZIbZuWhJwXg+sfP01DkcHuAnRmqS5NgEjHHsIY/JuAttYyIWYuOVxP48my9M2LhFRMeFHwWtrICMKqY9D6a7DuHo4sFz7Ld1jc9v/8Qe7ZhAQIEZbF3kLZDuCJZ5wOdD2nZZ60uWILc0="
|
||||||
|
ALIPAY_ALIPAY_PUBLIC_KEY="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwThMt6iRtRXGKAxfsJXgFOrL95PjZinbAeZKWnX8xCRm4q5SHoE1kDkQ+totcebMzQkepcdyHImVOuPEEKx5RBx1LybqiuHJaXvVj1SIqQNUCX8vNwum+giI/hI+dBIfu/XDdgXoPd1tHpMyEJu7n43t+EKwKGchyt1qXOJ0VEHTjxc+LPM9BzxO0Igo85OEi6PMoqMLt24QuHppRW2T4Qk4a7Njow3zZO0Hbqcy4uLYIGAD0qYbXICGKJVJ6G3voGInYOwnhc2zeOsl4MTTpMB533V7u4kG9BwI8Oo9cPqCxusZ3FAzGuO+2XyjBNHhFDC+q65q4ei3nYRafIR53QIDAQAB"
|
||||||
|
ALIPAY_GATEWAY="https://openapi-sandbox.dl.alipaydev.com/gateway.do"
|
||||||
|
APP_BASE_URL="https://dev.xmclassmate.top"
|
||||||
|
|||||||
48
AGENTS.md
48
AGENTS.md
@@ -284,6 +284,15 @@ created_at TIMESTAMPTZ(创建时间)
|
|||||||
| `PUT /api/admin/users/{id}/payment` | 更新用户支付状态 |
|
| `PUT /api/admin/users/{id}/payment` | 更新用户支付状态 |
|
||||||
| `GET /api/admin/users/{id}` | 获取用户信息 |
|
| `GET /api/admin/users/{id}` | 获取用户信息 |
|
||||||
|
|
||||||
|
### 登录码接口(网页授权)
|
||||||
|
|
||||||
|
| 接口 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `POST /api/web-login/code` | 小程序用微信 code 换取 display_code |
|
||||||
|
| `POST /api/web-login/confirm` | 小程序确认登录(需 JWT + code) |
|
||||||
|
| `GET /payment/generate-code` | 网页端生成登录码(已废弃,不建议使用) |
|
||||||
|
| `GET /payment/login-status` | 网页端轮询登录状态 |
|
||||||
|
|
||||||
### 支付接口
|
### 支付接口
|
||||||
|
|
||||||
| 接口 | 说明 |
|
| 接口 | 说明 |
|
||||||
@@ -436,25 +445,36 @@ ORDER BY "desc"
|
|||||||
|
|
||||||
## 支付系统
|
## 支付系统
|
||||||
|
|
||||||
### 支付流程(真实支付宝)
|
### 支付流程(登录码授权模式)
|
||||||
|
|
||||||
小程序内无法直接接入支付宝支付,采用以下方案:
|
小程序内无法直接接入支付宝支付,采用外部浏览器中转方案:
|
||||||
|
|
||||||
```
|
```
|
||||||
小程序 → outter页面 → 外部浏览器 → /payment?jwt=xxx
|
小程序(升级页) → outter页面 → 外部浏览器 → /payment?code=xxx → 支付宝
|
||||||
```
|
```
|
||||||
|
|
||||||
1. 用户在小程序选择套餐 → 点击「立即开通」
|
1. 用户在小程序升级页点击「输入登录码」
|
||||||
2. 小程序调用 `POST /api/payment/create-order` 创建订单(需 JWT)
|
2. 小程序获取微信 code,换取 display_code
|
||||||
3. 小程序跳转 `outter` 页面,URL 指向 `/payment?jwt=xxx&package=xxx`(outter 页面复制链接到剪贴板)
|
3. 小程序自动调用 `POST /api/web-login/confirm` 确认登录(无需用户操作)
|
||||||
4. 用户切换到外部浏览器,打开 `/payment?jwt=xxx`
|
4. 小程序跳转 `outter` 页面,URL 指向 `/payment?code=xxx`
|
||||||
5. 后端验证 JWT,返回套餐选择页(已携带 package 参数则预选套餐)
|
5. outter 页面提示用户在外部浏览器打开
|
||||||
6. 用户选择套餐,点击「去支付」→ 跳转到 `/payment/pay?order_no=xxx&package_type=xxx`
|
6. 用户在手机浏览器打开 `/payment?code=xxx`
|
||||||
7. 后端调用 `alipay.trade.page.pay` API,生成支付表单或跳转 URL
|
7. 网页轮询 `/payment/login-status?code=xxx`,检测到已确认后自动登录
|
||||||
8. 支付宝沙箱/正式环境支付页面展示
|
8. 显示套餐选择页
|
||||||
9. 支付完成后,支付宝异步通知 `/payment/notify`
|
9. 用户选择套餐,点击「去支付」→ 跳转到 `/payment/page?package=xxx&jwt=xxx`
|
||||||
10. 后端验证签名,确认订单状态
|
10. 后端调用 `alipay.trade.page.pay` API,返回支付跳转
|
||||||
11. 跳转成功页 `/payment/success?order_no=xxx`
|
11. 支付宝沙箱/正式环境展示支付页面
|
||||||
|
12. 支付完成后,支付宝异步通知 `/payment/notify`
|
||||||
|
13. 跳转成功页 `/payment/success?order_no=xxx`
|
||||||
|
|
||||||
|
### 登录码相关接口
|
||||||
|
|
||||||
|
| 接口 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `POST /api/web-login/code` | 小程序用微信 code 换取 display_code |
|
||||||
|
| `POST /api/web-login/confirm` | 小程序确认登录(需 JWT + code) |
|
||||||
|
| `GET /payment/generate-code` | 网页端生成登录码(已废弃,不建议使用) |
|
||||||
|
| `GET /payment/login-status` | 网页端轮询登录状态(返回 token 表示已确认) |
|
||||||
|
|
||||||
### Mock 登录(沙箱测试)
|
### Mock 登录(沙箱测试)
|
||||||
|
|
||||||
|
|||||||
@@ -614,3 +614,127 @@ pub async fn web_login_confirm(
|
|||||||
paid_expires_at,
|
paid_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_paid_active: bool,
|
||||||
|
pub paid_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" {
|
||||||
|
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 (is_paid_active, paid_expires_at): (bool, Option<String>) =
|
||||||
|
match sqlx::query_as::<_, (bool, Option<chrono::DateTime<chrono::Utc>>)>(
|
||||||
|
"SELECT is_paid_active($1)",
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.fetch_optional(pool.get_ref())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some((active, expires))) => (active, expires.map(|e| e.to_rfc3339())),
|
||||||
|
_ => (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_paid_active {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(format!("{}/payment?jwt={}", base_url, token))
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("[WEB LOGIN AUTO-CONFIRM] user_id={} is_paid={}", user_id, is_paid_active);
|
||||||
|
HttpResponse::Ok().json(AutoConfirmResponse {
|
||||||
|
success: true,
|
||||||
|
token: Some(token),
|
||||||
|
is_paid_active,
|
||||||
|
paid_expires_at,
|
||||||
|
payment_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub use auth::mock_login;
|
|||||||
pub use auth::refresh_token;
|
pub use auth::refresh_token;
|
||||||
pub use auth::web_generate_login_code;
|
pub use auth::web_generate_login_code;
|
||||||
pub use auth::web_login_confirm;
|
pub use auth::web_login_confirm;
|
||||||
|
pub use auth::web_login_auto_confirm;
|
||||||
pub use favorites::{add_favorite, get_favorites, remove_favorite};
|
pub use favorites::{add_favorite, get_favorites, remove_favorite};
|
||||||
pub use health::health_check;
|
pub use health::health_check;
|
||||||
pub use meta::root;
|
pub use meta::root;
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- 登录区 -->
|
<!-- 登录区(等待授权) -->
|
||||||
<div class="login-section" id="loginSection">
|
<div class="login-section" id="loginSection">
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
<div class="login-icon">
|
<div class="login-icon">
|
||||||
@@ -330,31 +330,18 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
|
|||||||
<path d="M15.32 8.68a.5.5 0 01.01.85l-1.6 1.28 1.6 1.28a.5.5 0 01-.74.38l-3.6-2.88a.5.5 0 01.01-.85l3.6-2.88a.5.5 0 01.72.38v3.22z"/>
|
<path d="M15.32 8.68a.5.5 0 01.01.85l-1.6 1.28 1.6 1.28a.5.5 0 01-.74.38l-3.6-2.88a.5.5 0 01.01-.85l3.6-2.88a.5.5 0 01.72.38v3.22z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-title">微信扫码登录</div>
|
<div class="login-title">等待授权</div>
|
||||||
<div class="login-desc">请在微信小程序中<br>点击「网页登录」获取登录码</div>
|
<div class="login-desc" id="loginDesc">请在微信小程序中<br>点击「去授权」完成登录</div>
|
||||||
|
|
||||||
<div class="error-msg" id="errorMsg"></div>
|
<div class="error-msg" id="errorMsg"></div>
|
||||||
|
|
||||||
<div class="code-display" id="codeDisplay" style="display:none">
|
<div class="code-display" id="codeDisplay" style="display:none">
|
||||||
<div class="code-label">登录码</div>
|
<div class="code-label">登录码</div>
|
||||||
<div class="code-value" id="codeValue">--</div>
|
<div class="code-value" id="codeValue">--</div>
|
||||||
<div class="code-hint" id="codeHint">有效期 10 分钟</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scan-status waiting" id="scanStatus" style="display:none">
|
<div class="scan-status waiting" id="scanStatus" style="display:none">
|
||||||
请在小程序中确认登录
|
等待小程序授权确认...
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn-login" id="btnGenerate" onclick="generateCode()">
|
|
||||||
获取登录码
|
|
||||||
</button>
|
|
||||||
<button class="btn-login btn-refresh" id="btnRefresh" onclick="generateCode()" style="display:none">
|
|
||||||
重新获取
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="login-note">
|
|
||||||
登录码仅用于本次支付,无需输入账号密码<br>
|
|
||||||
登录成功后可选择套餐进行支付
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,58 +418,44 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
|
|||||||
let isPaidActive = false;
|
let isPaidActive = false;
|
||||||
let paidExpiresAt = null;
|
let paidExpiresAt = null;
|
||||||
|
|
||||||
// ---- 页面初始化:检查 URL 中的 code 参数 ----
|
// ---- 页面初始化:检查 URL 中的 code 或 jwt 参数 ----
|
||||||
(function initFromUrl() {
|
(function initFromUrl() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const urlJwt = params.get('jwt');
|
||||||
|
if (urlJwt) {
|
||||||
|
jwt = urlJwt;
|
||||||
|
checkPaidStatus(urlJwt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const urlCode = params.get('code');
|
const urlCode = params.get('code');
|
||||||
if (urlCode) {
|
if (urlCode) {
|
||||||
currentShortCode = urlCode;
|
currentShortCode = urlCode;
|
||||||
document.getElementById('codeValue').textContent = currentShortCode;
|
document.getElementById('codeValue').textContent = currentShortCode;
|
||||||
document.getElementById('codeDisplay').style.display = 'block';
|
document.getElementById('codeDisplay').style.display = 'block';
|
||||||
document.getElementById('scanStatus').style.display = 'block';
|
document.getElementById('scanStatus').style.display = 'block';
|
||||||
document.getElementById('scanStatus').className = 'scan-status waiting';
|
document.getElementById('loginDesc').style.display = 'none';
|
||||||
document.getElementById('scanStatus').textContent = '等待小程序授权确认...';
|
|
||||||
document.getElementById('btnGenerate').style.display = 'none';
|
|
||||||
document.getElementById('btnRefresh').style.display = 'block';
|
|
||||||
// 启动轮询
|
// 启动轮询
|
||||||
if (pollTimer) clearInterval(pollTimer);
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
pollTimer = setInterval(pollLoginStatus, 2000);
|
pollTimer = setInterval(pollLoginStatus, 2000);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ---- 登录码流程(正式版)----
|
|
||||||
async function generateCode() {
|
|
||||||
hideError();
|
|
||||||
const btn = document.getElementById('btnGenerate');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = '正在获取...';
|
|
||||||
|
|
||||||
|
async function checkPaidStatus(token) {
|
||||||
try {
|
try {
|
||||||
// 1. 获取登录码
|
const resp = await fetch(API_BASE + '/api/user/profile', {
|
||||||
const resp = await fetch(API_BASE + '/payment/generate-code');
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
const data = await resp.json();
|
});
|
||||||
if (!data.code) throw new Error('获取登录码失败');
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
currentShortCode = data.code;
|
if (data.success) {
|
||||||
document.getElementById('codeValue').textContent = currentShortCode;
|
isPaidActive = data.data.is_paid_active || false;
|
||||||
document.getElementById('codeDisplay').style.display = 'block';
|
paidExpiresAt = data.data.paid_expires_at || null;
|
||||||
document.getElementById('scanStatus').style.display = 'block';
|
}
|
||||||
document.getElementById('scanStatus').className = 'scan-status waiting';
|
}
|
||||||
document.getElementById('scanStatus').textContent = '请在微信小程序中确认登录';
|
} catch(e) {}
|
||||||
btn.style.display = 'none';
|
showLoggedIn();
|
||||||
document.getElementById('btnRefresh').style.display = 'block';
|
|
||||||
|
|
||||||
// 2. 启动轮询
|
|
||||||
if (pollTimer) clearInterval(pollTimer);
|
|
||||||
pollTimer = setInterval(pollLoginStatus, 2000);
|
|
||||||
} catch(e) {
|
|
||||||
showError('获取登录码失败,请稍后重试');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = '重新获取';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 轮询登录状态(正式流程)----
|
|
||||||
async function pollLoginStatus() {
|
async function pollLoginStatus() {
|
||||||
if (!currentShortCode) return;
|
if (!currentShortCode) return;
|
||||||
try {
|
try {
|
||||||
@@ -529,16 +502,9 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
|
|||||||
document.getElementById('loginSection').style.display = 'block';
|
document.getElementById('loginSection').style.display = 'block';
|
||||||
document.getElementById('paidSection').style.display = 'none';
|
document.getElementById('paidSection').style.display = 'none';
|
||||||
document.getElementById('packages').style.display = 'none';
|
document.getElementById('packages').style.display = 'none';
|
||||||
resetLoginUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetLoginUI() {
|
|
||||||
document.getElementById('codeDisplay').style.display = 'none';
|
document.getElementById('codeDisplay').style.display = 'none';
|
||||||
document.getElementById('scanStatus').style.display = 'none';
|
document.getElementById('scanStatus').style.display = 'none';
|
||||||
document.getElementById('btnGenerate').style.display = 'block';
|
document.getElementById('loginDesc').style.display = 'block';
|
||||||
document.getElementById('btnGenerate').disabled = false;
|
|
||||||
document.getElementById('btnGenerate').textContent = '获取登录码';
|
|
||||||
document.getElementById('btnRefresh').style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
@@ -569,7 +535,7 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
|
|||||||
showError('请先登录');
|
showError('请先登录');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.location.href = '/payment/page?package=' + selected;
|
window.location.href = '/payment/page?package=' + selected + '&jwt=' + encodeURIComponent(jwt);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use handlers::{
|
|||||||
health_check, login, mock_login, mock_confirm, payment_index, payment_login_status, payment_page, payment_success,
|
health_check, login, mock_login, mock_confirm, payment_index, payment_login_status, payment_page, payment_success,
|
||||||
post_weather_data,
|
post_weather_data,
|
||||||
refresh_token, remove_favorite, root, save_user_profile, serve_static_files,
|
refresh_token, remove_favorite, root, save_user_profile, serve_static_files,
|
||||||
web_generate_login_code, web_login_confirm,
|
web_generate_login_code, web_login_confirm, web_login_auto_confirm,
|
||||||
};
|
};
|
||||||
use models::AppState;
|
use models::AppState;
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ fn create_server_config(
|
|||||||
.service(health_check)
|
.service(health_check)
|
||||||
.service(web_generate_login_code)
|
.service(web_generate_login_code)
|
||||||
.service(web_login_confirm)
|
.service(web_login_confirm)
|
||||||
|
.service(web_login_auto_confirm)
|
||||||
.service(
|
.service(
|
||||||
web::scope("")
|
web::scope("")
|
||||||
.wrap(from_fn(jwt_middleware))
|
.wrap(from_fn(jwt_middleware))
|
||||||
|
|||||||
Reference in New Issue
Block a user