You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Stripe订阅的用户创建健壮方案问询(Rust/Actix+async-stripe)

优化的订阅制服务用户创建+支付验证方案

针对你当前流程中注册邮箱与Stripe支付邮箱不一致导致的账户激活失败问题,核心改进思路是通过唯一客户端关联ID替代邮箱作为后端与Stripe的匹配标识,以下是基于Rust Actix框架和async-stripe库的具体实现方案:

核心流程说明

  1. 用户提交注册表单(邮箱+密码+订阅层级)后,后端生成唯一client_reference_id并创建未激活用户,将该ID与用户关联存储
  2. 后端调用Stripe API创建Checkout Session,传入client_reference_id、订阅价格ID及回调URL,返回Stripe支付页面地址供用户跳转
  3. 用户完成支付后,Stripe触发checkout.session.completed事件,后端Webhook通过client_reference_id直接匹配到对应未激活用户,完成账户激活并关联Stripe订阅ID
  4. 后续退订、退款等操作通过用户记录中的Stripe订阅ID关联处理,无需依赖邮箱

关键代码实现

1. 用户注册接口(生成关联ID并创建Stripe Checkout Session)

use actix_web::{post, web, HttpResponse};
use async_stripe::{
    checkout::{Session, SessionCreateParams, SessionCreateParamsLineItem},
    client::Client,
};
use bcrypt::{hash, DEFAULT_COST};
use sqlx::PgPool;
use uuid::Uuid;

#[derive(serde::Deserialize)]
struct RegisterRequest {
    email: String,
    password: String,
    price_id: String, // 对应Stripe后台配置的订阅价格ID
}

#[post("/api/register")]
async fn register(
    req: web::Json<RegisterRequest>,
    stripe_client: web::Data<Client>,
    db_pool: web::Data<PgPool>,
) -> HttpResponse {
    // 生成唯一客户端关联ID
    let client_ref_id = Uuid::new_v4().to_string();

    // 哈希密码(生产环境需处理哈希错误)
    let hashed_pwd = hash(&req.password, DEFAULT_COST).expect("Failed to hash password");

    // 插入未激活用户到数据库(生产环境需处理数据库操作错误)
    sqlx::query!(
        r#"
        INSERT INTO users (email, password_hash, client_reference_id, is_active, subscription_id)
        VALUES ($1, $2, $3, $4, $5)
        "#,
        req.email,
        hashed_pwd,
        client_ref_id,
        false,
        Option::<String>::None
    )
    .execute(&**db_pool)
    .await
    .expect("Failed to create user");

    // 创建Stripe Checkout Session
    let session_params = SessionCreateParams::new()
        .mode(async_stripe::checkout::SessionMode::Subscription)
        .success_url("https://your-domain.com/api/payment-success".to_string())
        .cancel_url("https://your-domain.com/api/payment-cancel".to_string())
        .client_reference_id(client_ref_id)
        .line_item(
            SessionCreateParamsLineItem::new()
                .price(req.price_id)
                .quantity(1),
        );

    let session = Session::create(&*stripe_client, session_params)
        .await
        .expect("Failed to create Stripe checkout session");

    // 重定向到Stripe支付页面
    HttpResponse::Found()
        .append_header(("Location", session.url.expect("Checkout session has no URL")))
        .finish()
}

2. Stripe Webhook处理(激活用户及订阅管理)

use actix_web::{post, web, HttpResponse};
use async_stripe::{
    webhook::WebhookEvent,
    client::Client,
};
use sqlx::PgPool;

#[derive(sqlx::FromRow)]
struct User {
    id: Uuid,
    is_active: bool,
    subscription_id: Option<String>,
}

#[post("/api/stripe-webhook")]
async fn stripe_webhook(
    payload: web::Bytes,
    headers: web::HttpRequest,
    stripe_client: web::Data<Client>,
    db_pool: web::Data<PgPool>,
) -> HttpResponse {
    // 验证Stripe Webhook签名(必须做,防止伪造请求)
    let sig_header = headers
        .headers()
        .get("Stripe-Signature")
        .expect("Missing Stripe-Signature header")
        .to_str()
        .expect("Invalid Stripe-Signature header");
    let webhook_secret = std::env::var("STRIPE_WEBHOOK_SECRET")
        .expect("STRIPE_WEBHOOK_SECRET not set");

    let event = WebhookEvent::construct(&payload, sig_header, &webhook_secret)
        .expect("Invalid webhook signature");

    match event {
        WebhookEvent::CheckoutSessionCompleted(session) => {
            // 通过client_reference_id找到未激活用户
            let client_ref_id = session.client_reference_id.expect("Missing client_reference_id");
            let mut user = sqlx::query_as!(
                User,
                r#"
                SELECT id, is_active, subscription_id FROM users
                WHERE client_reference_id = $1 AND is_active = $2
                "#,
                client_ref_id,
                false
            )
            .fetch_one(&**db_pool)
            .await
            .expect("No matching inactive user found");

            // 获取Stripe订阅ID并激活用户
            let subscription_id = session.subscription.expect("Missing subscription ID").to_string();
            sqlx::query!(
                r#"
                UPDATE users SET is_active = $1, subscription_id = $2 WHERE id = $3
                "#,
                true,
                subscription_id,
                user.id
            )
            .execute(&**db_pool)
            .await
            .expect("Failed to activate user");

            // 可选:发送账户激活通知邮件
        }
        WebhookEvent::CustomerSubscriptionDeleted(subscription) => {
            // 取消用户订阅权限
            sqlx::query!(
                r#"
                UPDATE users SET is_active = $1 WHERE subscription_id = $2
                "#,
                false,
                subscription.id
            )
            .execute(&**db_pool)
            .await
            .expect("Failed to deactivate user");
        }
        // 处理支付失败、订阅更新等其他事件
        WebhookEvent::PaymentIntentPaymentFailed(_) => {
            // 可选:发送支付失败提醒
        }
        _ => {}
    }

    HttpResponse::Ok().finish()
}

额外健壮性优化措施

  • 强制Webhook签名验证:绝不信任未验证的Webhook请求,async-stripe的WebhookEvent::construct方法已封装验证逻辑
  • 数据库事务:在创建用户、激活用户等关键操作中使用事务,避免数据不一致
  • 错误处理:示例中使用expect仅为简化,生产环境需捕获并处理所有可能的错误(如数据库连接失败、Stripe API调用超时等),并记录详细日志
  • 支付失败处理:监听payment_intent.payment_failed事件,给用户发送支付重试提醒,或标记用户状态为待支付
  • Checkout取消处理:监听checkout.session.canceled事件,允许用户重新发起支付,或清理超过有效期的未激活用户记录
  • 密码安全:始终使用bcrypt等慢哈希算法存储密码,禁止明文存储

内容的提问来源于stack exchange,提问作者jacobi_matrix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 19:45:59