// handlers/payment.rs — 支付相关处理器(接入支付宝) use actix_web::{get, post, web, HttpRequest, HttpResponse}; use chrono::Utc; use rsa::pkcs1v15::SigningKey; use rsa::pkcs8::DecodePrivateKey; use rsa::signature::{SignatureEncoding, Signer}; use rsa::RsaPrivateKey; use serde::Deserialize; use sha2::Sha256; use sqlx::postgres::PgPool; use std::collections::BTreeMap; use tracing::info; use uuid::Uuid; use crate::db; use crate::error::AppError; use crate::models::{Claims, CreateOrderRequest, MockConfirmRequest}; // ===== 套餐定义 ===== struct PackageInfo { amount: i32, display_amount: &'static str, display_name: &'static str, days: Option, } fn get_package_info(package_type: &str) -> Option { match package_type { "monthly" => Some(PackageInfo { amount: 990, display_amount: "9.9", display_name: "包月会员", days: Some(30), }), "yearly" => Some(PackageInfo { amount: 5900, display_amount: "59", display_name: "包年会员", days: Some(365), }), "permanent" => Some(PackageInfo { amount: 19900, display_amount: "199", display_name: "永久会员", days: None, }), _ => None, } } // ===== 支付宝配置 ===== struct AlipayConfig { app_id: String, private_key: String, alipay_public_key: String, gateway: String, } impl AlipayConfig { fn from_env() -> Option { Some(Self { app_id: std::env::var("ALIPAY_APP_ID").ok()?, private_key: std::env::var("ALIPAY_PRIVATE_KEY").ok()?, alipay_public_key: std::env::var("ALIPAY_ALIPAY_PUBLIC_KEY").ok()?, gateway: std::env::var("ALIPAY_GATEWAY") .unwrap_or_else(|_| "https://openapi.alipay.com/gateway.do".to_string()), }) } } /// URL 编码(RFC 3986) fn urlencoding(s: &str) -> String { let mut result = String::new(); for c in s.chars() { match c { 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c), _ => { for b in c.to_string().as_bytes() { result.push_str(&format!("%{:02X}", b)); } } } } result } /// RSA2 (SHA256) 签名 fn rsa2_sign(content: &str, private_key_pem: &str) -> Result { let private_key = match RsaPrivateKey::from_pkcs8_pem(private_key_pem) { Ok(key) => key, Err(_) => { let decoded = base64::Engine::decode( &base64::engine::general_purpose::STANDARD, private_key_pem, ) .map_err(|e| format!("Base64 解码失败: {}", e))?; // 支付宝密钥工具生成的 Base64 解码后是 PKCS#8 DER,不是 PEM 文本 RsaPrivateKey::from_pkcs8_der(&decoded) .map_err(|e| format!("私钥解析失败: {}", e))? } }; let signing_key = SigningKey::::new(private_key); let signature = signing_key.sign(content.as_bytes()); Ok(base64::Engine::encode( &base64::engine::general_purpose::STANDARD, signature.to_bytes().as_ref(), )) } /// 合并 mock 支付 HTML(在 payment_page 和 alipay_pay_page 中复用) fn build_mock_pay_html(order_no: &str, display_name: &str, jwt: &str) -> String { format!(r#" 模拟支付

模拟支付环境

当前为沙箱模拟环境,无需真实支付

订单号: {}

套餐: {}

返回重试

"#, order_no, display_name, jwt, order_no) } /// 验证 RSA2 签名 fn rsa2_verify(content: &str, sign: &str, public_key_pem: &str) -> Result { use rsa::pkcs8::DecodePublicKey; use rsa::RsaPublicKey; use sha2::Digest; let public_key = RsaPublicKey::from_public_key_pem(public_key_pem) .map_err(|e| format!("支付宝公钥解析失败: {}", e))?; let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, sign) .map_err(|e| format!("签名 Base64 解码失败: {}", e))?; let hashed = Sha256::digest(content.as_bytes()); public_key .verify(rsa::Pkcs1v15Sign::new::(), &hashed, &sig_bytes) .map_err(|e| format!("签名验证失败: {}", e))?; Ok(true) } // ===== Helper: 生成支付宝支付表单 HTML ===== fn build_alipay_form_html( config: &AlipayConfig, out_trade_no: &str, total_amount: &str, subject: &str, notify_url: &str, return_url: &str, ) -> Result { let biz_content = serde_json::json!({ "out_trade_no": out_trade_no, "total_amount": total_amount, "subject": subject, "product_code": "FAST_INSTANT_TRADE_PAY", }); let biz_content_str = serde_json::to_string(&biz_content).map_err(|e| format!("biz_content 序列化失败: {}", e))?; let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); let mut params: BTreeMap<&str, &str> = BTreeMap::new(); params.insert("app_id", &config.app_id); params.insert("method", "alipay.trade.page.pay"); params.insert("format", "JSON"); params.insert("charset", "utf-8"); params.insert("sign_type", "RSA2"); params.insert("timestamp", ×tamp); params.insert("version", "1.0"); params.insert("biz_content", &biz_content_str); params.insert("notify_url", notify_url); params.insert("return_url", return_url); // 构造待签名串(注意:签名源使用原始值,不做 URL 编码) let sign_source: String = params .iter() .map(|(k, v)| format!("{}={}", k, *v)) .collect::>() .join("&"); let sign = rsa2_sign(&sign_source, &config.private_key)?; // 生成自动提交的表单 HTML(HTML 转义防止特殊字符破坏属性) fn h(s: &str) -> String { s.replace('&', "&") .replace('"', """) .replace('<', "<") .replace('>', ">") } let mut form_fields = String::new(); for (k, v) in ¶ms { form_fields.push_str(&format!( r#""#, h(k), h(v) )); } form_fields.push_str(&format!( r#""#, h(&sign) )); let html = format!( r##" 正在跳转支付宝...
{}
"##, config.gateway, form_fields ); Ok(html) } // ===== 提取 JWT token ===== /// 维护模式守卫:PAYMENT_MAINTENANCE_MODE=true 时所有支付接口返回 503 fn check_payment_maintenance() -> Result<(), AppError> { if std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string()) { return Err(AppError::ServiceUnavailable( "支付系统维护中,请稍后再试".to_string() )); } Ok(()) } fn extract_token(req: &HttpRequest) -> Option { req.headers() .get("Authorization")? .to_str() .ok()? .strip_prefix("Bearer ") .map(|s| s.to_string()) } fn get_jwt_secret() -> String { std::env::var("JWT_SECRET").expect("JWT_SECRET must be set") } // ===== Handler: GET /payment — 套餐选择页(网页端微信扫码登录) ===== #[get("/payment")] pub async fn payment_index() -> Result { check_payment_maintenance()?; let html = r##" 开通会员 - 大气稳定度判定
选择会员套餐
包月会员
¥9.9/月
适合短期使用需求
每月 500 次检测额度
查看完整历史记录
推荐
包年会员
¥59/年
相当于每月 ¥4.9,性价比最高
每年 5000 次检测额度
查看完整历史记录
支付成功后额度将自动到账
"##; Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html)) } // ===== Handler: GET /payment/page — 创建订单并跳转支付宝 ===== #[derive(Debug, Deserialize)] pub struct PaymentPageQuery { #[serde(rename = "package")] pub package_: String, /// JWT: 从 URL 参数传入(外部浏览器无 Cookie 时使用) #[serde(default)] pub jwt: Option, } #[get("/payment/page")] pub async fn payment_page( req: HttpRequest, pool: web::Data, query: web::Query, ) -> Result { check_payment_maintenance()?; let token = extract_token(&req) .or_else(|| query.jwt.clone()) .ok_or_else(|| AppError::Unauthorized("未登录".to_string()))?; let claims = crate::auth::verify_token(&token, &get_jwt_secret()) .map_err(|_| AppError::Unauthorized("Token 无效".to_string()))?; let pkg = get_package_info(&query.package_) .ok_or_else(|| AppError::BadRequest("无效的套餐类型".to_string()))?; let order_no = Uuid::new_v4().to_string(); let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d)); db::create_payment_order( pool.get_ref(), claims.user_id, &order_no, &query.package_, pkg.amount, expires_at, ) .await?; let base_url = std::env::var("APP_BASE_URL") .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); let notify_url = format!("{}/payment/notify", base_url); let return_url = format!("{}/payment/success?order_no={}", base_url, order_no); let Some(config) = AlipayConfig::from_env() else { let jwt_for_mock = token.clone(); let mock_html = build_mock_pay_html(&order_no, pkg.display_name, &jwt_for_mock); return Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(mock_html)); }; let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0); match build_alipay_form_html( &config, &order_no, &total_amount_str, pkg.display_name, ¬ify_url, &return_url, ) { Ok(form_html) => Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(form_html)), Err(e) => { tracing::error!("支付宝下单失败: {}", e); Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(format_error_html(&e, &order_no))) } } } // ===== Handler: GET /payment/pay — 直接支付接口 ===== #[derive(Debug, Deserialize)] pub struct AlipayPayQuery { pub order_no: String, pub package_type: String, } #[get("/payment/pay")] pub async fn alipay_pay_page( req: HttpRequest, query: web::Query, ) -> Result { check_payment_maintenance()?; let token = extract_token(&req).ok_or_else(|| AppError::Unauthorized("未登录".to_string()))?; crate::auth::verify_token(&token, &get_jwt_secret()) .map_err(|_| AppError::Unauthorized("Token 无效".to_string()))?; let pkg = get_package_info(&query.package_type) .ok_or_else(|| AppError::BadRequest("无效的套餐类型".to_string()))?; let base_url = std::env::var("APP_BASE_URL") .unwrap_or_else(|_| "https://dev.xmclassmate.top".to_string()); let notify_url = format!("{}/payment/notify", base_url); let return_url = format!("{}/payment/success?order_no={}", base_url, query.order_no); let Some(config) = AlipayConfig::from_env() else { let jwt_for_mock = token.clone(); let mock_html = build_mock_pay_html(&query.order_no, pkg.display_name, &jwt_for_mock); return Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(mock_html)); }; let total_amount_str = format!("{:.2}", pkg.amount as f64 / 100.0); match build_alipay_form_html( &config, &query.order_no, &total_amount_str, pkg.display_name, ¬ify_url, &return_url, ) { Ok(form_html) => Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(form_html)), Err(e) => { tracing::error!("支付宝下单失败: {}", e); Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(format_error_html(&e, &query.order_no))) } } } // ===== Handler: POST /payment/notify — 支付宝异步回调 ===== #[post("/payment/notify")] pub async fn alipay_notify( pool: web::Data, body: web::Form>, ) -> HttpResponse { let body = body.into_inner(); let out_trade_no = body.get("out_trade_no").cloned().unwrap_or_default(); let trade_status = body.get("trade_status").cloned().unwrap_or_default(); tracing::info!( "收到支付宝回调: out_trade_no={}, trade_status={}", out_trade_no, trade_status ); // 1. 检查交易状态 if trade_status != "TRADE_SUCCESS" && trade_status != "TRADE_FINISHED" { return HttpResponse::Ok().body("success"); } // 2. 验证 RSA2 签名(防止伪造回调) let Some(config) = AlipayConfig::from_env() else { tracing::warn!("支付宝配置不存在,无法验证签名"); return HttpResponse::Ok().body("fail"); }; let sign = body.get("sign").cloned().unwrap_or_default(); // BTreeMap 已按 key 排序,直接拼接除 sign 和 sign_type 外的所有参数 let sign_source: String = body .iter() .filter(|(k, _)| *k != "sign" && *k != "sign_type") .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("&"); if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) { tracing::warn!("支付宝签名验证失败: {}", e); return HttpResponse::Ok().body("fail"); } // 3. 确认订单(通过 order_no,不校验 user_id) match db::confirm_payment_order_by_orderno(pool.get_ref(), &out_trade_no).await { Ok(_) => { tracing::info!("订单 {} 支付确认成功", out_trade_no); HttpResponse::Ok().body("success") } Err(e) => { tracing::error!("订单 {} 确认失败: {}", out_trade_no, e); HttpResponse::Ok().body("fail") } } } // ===== Handler: GET /payment/success — 支付成功页面 ===== #[derive(Debug, Deserialize)] pub struct AlipaySuccessQuery { pub order_no: Option, } fn build_success_html(order_no: &str) -> String { let green = "#52c41a"; let white = "white"; let html = format!( r##" 支付成功

支付成功!

恭喜您已成为会员,额度已自动到账

请返回微信小程序查看您的会员状态
订单号: {2}
"##, green, white, order_no ); html } fn format_error_html(msg: &str, order_no: &str) -> String { format!( r##" 支付失败

支付页面生成失败

{}

订单号: {}

返回重试

"##, msg, order_no ) } #[get("/payment/success")] pub async fn payment_success( pool: web::Data, query: web::Query, ) -> HttpResponse { let order_no = query.order_no.as_deref().unwrap_or(""); // 同步确认订单(幂等:已确认的订单会跳过) if !order_no.is_empty() && let Err(e) = db::confirm_payment_order_by_orderno(pool.get_ref(), order_no).await { tracing::warn!("支付成功页同步确认失败(可能是异步回调已处理): {}", e); } let html = build_success_html(order_no); HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(html) } // ===== Handler: POST /payment/refund-notify — 支付宝退款异步回调 ===== #[post("/payment/refund-notify")] pub async fn alipay_refund_notify( pool: web::Data, body: web::Form>, ) -> HttpResponse { let body = body.into_inner(); let out_trade_no = body.get("out_trade_no").cloned().unwrap_or_default(); let refund_status = body.get("refund_status").cloned().unwrap_or_default(); tracing::info!( "收到支付宝退款回调: out_trade_no={}, refund_status={}", out_trade_no, refund_status ); // 只处理成功的退款 if refund_status != "REFUND_SUCCESS" { return HttpResponse::Ok().body("success"); } // 验证 RSA2 签名 let Some(config) = AlipayConfig::from_env() else { tracing::warn!("支付宝配置不存在,无法验证退款签名"); return HttpResponse::Ok().body("fail"); }; let sign = body.get("sign").cloned().unwrap_or_default(); let sign_source: String = body .iter() .filter(|(k, _)| *k != "sign" && *k != "sign_type") .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join("&"); if let Err(e) = rsa2_verify(&sign_source, &sign, &config.alipay_public_key) { tracing::warn!("支付宝退款签名验证失败: {}", e); return HttpResponse::Ok().body("fail"); } // 处理退款:标记订单 + 撤销会员 match db::refund_payment_order(pool.get_ref(), &out_trade_no).await { Ok(_) => { tracing::info!("订单 {} 退款处理成功", out_trade_no); HttpResponse::Ok().body("success") } Err(e) => { tracing::error!("订单 {} 退款处理失败: {}", out_trade_no, e); HttpResponse::Ok().body("fail") } } } // ===== 旧的 API Handler(保持兼容)===== /// POST /api/payment/create-order #[post("/api/payment/create-order")] pub async fn create_order( pool: web::Data, claims: web::ReqData, body: web::Json, ) -> Result { check_payment_maintenance()?; let user_id = claims.user_id; let pkg = match get_package_info(&body.package_type) { Some(p) => p, None => { return Err(AppError::BadRequest("无效的套餐类型".to_string())); } }; let order_no = Uuid::new_v4().to_string(); let expires_at = pkg.days.map(|d| Utc::now() + chrono::Duration::days(d)); db::create_payment_order( pool.get_ref(), user_id, &order_no, &body.package_type, pkg.amount, expires_at, ) .await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "order_id": order_no, "package_type": body.package_type, "amount": pkg.amount, "display_amount": pkg.display_amount, "display_name": pkg.display_name, "expires_at": expires_at, } }))) } /// POST /api/payment/mock-confirm #[post("/api/payment/mock-confirm")] pub async fn mock_confirm( pool: web::Data, claims: web::ReqData, body: web::Json, ) -> Result { check_payment_maintenance()?; let user_id = claims.user_id; let expires_at = db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "is_paid_active": true, "paid_expires_at": expires_at, } }))) } /// POST /api/payment/sync-order — 根据订单号强制同步会员状态 /// /// 用户支付完成后,若前端会员状态未刷新,可调用此接口同步确认订单并获取最新状态。 #[post("/api/payment/sync-order")] pub async fn sync_order( pool: web::Data, claims: web::ReqData, body: web::Json, ) -> Result { check_payment_maintenance()?; let user_id = claims.user_id; // 尝试确认订单(幂等),失败时记录日志 if let Err(e) = db::confirm_payment_order(pool.get_ref(), &body.order_id, user_id).await { tracing::warn!("确认订单失败(可能已被异步回调处理): {}", e); } // 查询最新状态 let user = sqlx::query_as::<_, (bool, Option>)>( r#"SELECT is_paid, paid_expires_at FROM users WHERE id = $1"#, ) .bind(user_id) .fetch_optional(pool.get_ref()) .await .map_err(|e| AppError::Database(format!("查询用户失败: {}", e)))? .ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?; let (is_paid, paid_expires_at) = user; let is_paid_active = is_paid && paid_expires_at.map_or(true, |expires| expires > Utc::now()); Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "is_paid": is_paid, "is_paid_active": is_paid_active, "paid_expires_at": paid_expires_at, } }))) } /// POST /api/payment/cancel-order — 用户主动取消待支付订单 #[derive(Debug, Deserialize)] pub struct CancelOrderRequest { pub order_id: String, } #[post("/api/payment/cancel-order")] pub async fn cancel_order( pool: web::Data, claims: web::ReqData, body: web::Json, ) -> Result { check_payment_maintenance()?; db::cancel_payment_order(pool.get_ref(), &body.order_id, claims.user_id).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "message": "订单已取消" }))) } /// GET /api/payment/orders — 获取当前用户的订单记录 #[get("/api/payment/orders")] pub async fn get_user_orders( pool: web::Data, claims: web::ReqData, ) -> Result { let orders = db::get_user_orders(pool.get_ref(), claims.user_id).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": orders.iter().map(|o| serde_json::json!({ "order_no": o.order_no, "package_type": o.package_type, "amount": o.amount, "status": o.status, "paid_at": o.paid_at, "expires_at": o.expires_at, "created_at": o.created_at, })).collect::>() }))) } /// GET /api/user/quota #[get("/api/user/quota")] pub async fn get_user_quota( pool: web::Data, claims: web::ReqData, ) -> Result { let user_id = claims.user_id; let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string()); let (used, is_paid_active, paid_expires_at, was_paid) = db::get_user_quota(pool.get_ref(), user_id).await?; // 维护模式使用更高的临时限额,但非会员仍然有限制防止滥用 let (limit, unlimited) = if is_maintenance && !is_paid_active { let ml: i64 = std::env::var("MAINTENANCE_MODE_DATA_LIMIT") .ok().and_then(|v| v.parse().ok()).unwrap_or(500); (ml, false) } else if is_paid_active { (0, true) } else { let fl: i64 = std::env::var("FREE_USER_DATA_LIMIT") .ok().and_then(|v| v.parse().ok()).unwrap_or(20); (fl, false) }; let active = if is_maintenance { true } else { is_paid_active }; Ok(HttpResponse::Ok().json(serde_json::json!({ "success": true, "data": { "used": used, "limit": limit, "unlimited": unlimited, "is_paid_active": active, "paid_expires_at": paid_expires_at, "was_paid": was_paid, "maintenance_mode": is_maintenance, } }))) } /// GET /payment/login-status?code=ASD-XXXXX /// 网页端轮询:查询登录码是否已被小程序确认 #[derive(Debug, Deserialize)] pub struct LoginStatusQuery { pub code: String, } #[derive(Debug, serde::Serialize)] pub struct LoginStatusResponse { pub success: bool, pub confirmed: bool, pub token: Option, pub is_paid_active: bool, pub paid_expires_at: Option, } // ===== Handler: GET /payment/generate-code — 网页端生成登录码 ===== #[derive(Debug, serde::Serialize)] pub struct GenerateCodeResponse { pub code: String, pub expires_in: i64, // 秒 } /// 生成随机登录码(网页端专用,无需认证) #[get("/payment/generate-code")] pub async fn generate_code( pool: web::Data, ) -> Result { use rand::Rng; let mut rng = rand::thread_rng(); let suffix: String = (0..6) .map(|_| { let idx = rng.gen_range(0..36); if idx < 10 { (b'0' + idx) as char } else { (b'A' + idx - 10) as char } }) .collect(); let code = format!("ASD-{}", suffix); let expires_at = Utc::now() + chrono::Duration::minutes(10); sqlx::query("INSERT INTO web_login_codes (code, expires_at) VALUES ($1, $2)") .bind(&code) .bind(expires_at) .execute(pool.get_ref()) .await .map_err(|e| AppError::Internal(format!("建码失败: {}", e)))?; Ok(HttpResponse::Ok().json(GenerateCodeResponse { code, expires_in: 600, })) } #[get("/payment/login-status")] pub async fn payment_login_status( pool: web::Data, query: web::Query, _app_state: web::Data, ) -> Result { let code = query.code.trim(); // 查询登录码记录(包含 token 字段用于判断是否已确认) let record: Option<(String, chrono::DateTime, Option, Option)> = sqlx::query_as( "SELECT code, expires_at, token, user_id FROM web_login_codes WHERE code = $1", ) .bind(code) .fetch_optional(pool.get_ref()) .await .map_err(|e| AppError::Internal(format!("数据库查询失败: {}", e)))?; let (db_code, expires_at, token, user_id) = match record { Some(r) => r, None => { return Ok(HttpResponse::Ok().json(LoginStatusResponse { success: false, confirmed: false, token: None, is_paid_active: false, paid_expires_at: None, })); } }; // 检查是否过期 if Utc::now() > expires_at { if let Err(e) = sqlx::query("DELETE FROM web_login_codes WHERE code = $1") .bind(&db_code) .execute(pool.get_ref()) .await { tracing::warn!("清理过期登录码失败: {}", e); } return Ok(HttpResponse::Ok().json(LoginStatusResponse { success: false, confirmed: false, token: None, is_paid_active: false, paid_expires_at: None, })); } // 登录码存在但还没被小程序确认(token 为空 = 刚生成,还没点确认) // 使用 token 字段判断是否已确认(而非 user_id,因为 web_generate_login_code 会设置 user_id) if token.is_none() { return Ok(HttpResponse::Ok().json(LoginStatusResponse { success: true, confirmed: false, token: None, is_paid_active: false, paid_expires_at: None, })); } // 已确认 → 使用已生成的 token let token = token.unwrap(); let user_id = user_id.unwrap(); let (is_paid_active, paid_expires_at): (bool, Option) = match sqlx::query_as::<_, (bool, Option>)>( "SELECT is_paid, paid_expires_at FROM users WHERE id = $1" ) .bind(user_id) .fetch_optional(pool.get_ref()) .await { Ok(Some((is_paid, expires))) => { let active = is_paid && expires.map_or(true, |e| e > Utc::now()); (active, expires.map(|e| e.to_rfc3339())) } _ => (false, None), }; // 清理已使用的登录码 let _ = sqlx::query("DELETE FROM web_login_codes WHERE code = $1") .bind(&db_code) .execute(pool.get_ref()) .await; info!("[LOGIN STATUS] user_id={} confirmed=true", user_id); Ok(HttpResponse::Ok().json(LoginStatusResponse { success: true, confirmed: true, token: Some(token), is_paid_active, paid_expires_at, })) }