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

如何使用Dart的dart_frog库实现Stripe订阅及Webhook?

完整Stripe订阅与Webhook实现指南(Dart Frog)

你的现有代码已经完成了Stripe初始化和客户查询的基础工作,但还缺少实际创建订阅、用户数据关联和Webhook事件处理的核心逻辑。以下是完整的实现步骤和代码优化方案:


一、优化订阅创建逻辑

1. 修复Stripe API调用方式

官方Stripe Dart SDK已经封装了所有API方法,无需手动拼接URL,直接使用SDK提供的customers.search()、subscriptions.create()等方法更可靠。同时要移除硬编码的用户信息,关联当前登录用户的真实数据。

2. 完整订阅流程实现

更新user_service.dart,实现从客户创建到订阅生成的完整流程:

import 'package:stripe/stripe.dart';
import 'package:your_project/models/user_model.dart'; // 导入你的用户模型
import 'package:your_project/services/database_service.dart'; // 导入数据库服务

Future<void> userSubscription(
  UserSubscriptionModel userSubscriptionModel,
  String userId,
) async {
  Logger().info('<---------------------------stripe-subscription-----------------------'>);
  
  // 推荐在应用启动时全局初始化Stripe,避免重复创建实例
  final stripe = Stripe(environment['STRIPE_PRIVATE_KEY']!);

  // 1. 获取当前用户的真实信息(从数据库或Auth服务)
  final UserModel user = await DatabaseService().getUserById(userId);
  if (user.email == null) throw Exception('用户邮箱不能为空');

  // 2. 查询或创建Stripe客户
  Customer? stripeCustomer;
  final searchResponse = await stripe.customers.search(
    SearchCustomersParams(query: "email:'${user.email}'"),
  );
  if (searchResponse.data.isEmpty) {
    stripeCustomer = await stripe.customers.create(
      CustomerCreateParams(
        name: user.name,
        email: user.email,
        metadata: {'app_user_id': userId}, // 关联应用内用户ID
      ),
    );
    Logger().info('创建新Stripe客户: ${stripeCustomer.id}');
    // 更新用户数据库,保存Stripe客户ID
    await DatabaseService().updateUser(userId, {'stripe_customer_id': stripeCustomer.id});
  } else {
    stripeCustomer = searchResponse.data.first;
    Logger().info('找到已存在的Stripe客户: ${stripeCustomer.id}');
  }

  // 3. 根据brandIds获取对应的Stripe价格ID(需提前在Stripe后台创建价格,或从数据库映射)
  final List<String> priceIds = await DatabaseService().getPriceIdsByBrandIds(userSubscriptionModel.brandIds);
  if (priceIds.isEmpty) throw Exception('未找到对应品牌的订阅价格');

  // 4. 创建Stripe订阅
  final subscription = await stripe.subscriptions.create(
    SubscriptionCreateParams(
      customer: stripeCustomer.id,
      items: priceIds.map((priceId) => SubscriptionItemParams(price: priceId)).toList(),
      paymentBehavior: PaymentBehavior.defaultBehavior,
      expand: ['latest_invoice.payment_intent'], // 展开支付意图信息
    ),
  );
  Logger().info('创建订阅成功: ${subscription.id}, 状态: ${subscription.status}');

  // 5. 更新用户数据库,保存订阅信息
  await DatabaseService().updateUser(
    userId,
    {
      'stripe_subscription_id': subscription.id,
      'brand_subscriptions': userSubscriptionModel.brandIds,
      'subscription_status': subscription.status,
      'current_period_end': subscription.currentPeriodEnd?.toIso8601String(),
    },
  );

  Logger().info('---------------------------stripe-subscription------------------------');
}

3. 优化控制器逻辑

更新user_controller.dart,确保参数校验和错误处理更严谨:

static Future<Response> userSubscription(RequestContext context) async {
  try {
    final request = context.request;
    final authenticator = context.read<Authenticator>();
    final userModuleServices = context.read<UserModuleService>();

    // 验证用户身份
    final userId = await authenticator.getUserId(request.headers);
    if (userId == null) return Response.json(statusCode: 401, body: {'message': '未授权'});

    // 解析请求体
    final requestBody = await request.json() as Map<String, dynamic>;
    final subscriptionModel = UserSubscriptionModel.fromJson(requestBody);
    // 校验brandIds非空
    if (subscriptionModel.brandIds.isEmpty) {
      return Response.json(statusCode: 400, body: {'message': '请选择订阅品牌'});
    }

    await userModuleServices.userSubscription(subscriptionModel, userId.toString());
    return successResponse(ResponseKey.subscribtionSuccessHintAppMsg);
  } catch (e) {
    Logger().info('订阅创建失败: ${e.toString()}');
    if (e is ExceptionMsg) {
      return Response.json(
        statusCode: e.statusCode,
        body: GeneralResponse(status: e.status, message: e.message),
      );
    } else {
      return internalServerError();
    }
  }
}

二、实现Stripe Webhook

Webhook用于接收Stripe的异步事件通知(如付款成功、订阅取消、逾期付款等),必须验证签名确保请求来自Stripe。

1. 配置Webhook密钥

在环境变量中添加Stripe Webhook签名密钥(从Stripe后台获取):

STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx

2. 创建Webhook控制器

在dart_frog中创建routes/stripe/webhook.dart:

import 'dart:convert';
import 'dart:io';
import 'package:dart_frog/dart_frog.dart';
import 'package:stripe/stripe.dart';
import 'package:your_project/services/database_service.dart';

Future<Response> onRequest(RequestContext context) async {
  if (context.request.method != HttpMethod.post) {
    return Response(statusCode: HttpStatus.methodNotAllowed);
  }

  final stripeWebhookSecret = environment['STRIPE_WEBHOOK_SECRET']!;
  final stripe = Stripe(environment['STRIPE_PRIVATE_KEY']!);

  // 读取原始请求体(Stripe签名验证需要原始字节)
  final rawBody = await context.request.bodyBytes;
  final signature = context.request.headers['stripe-signature'];

  try {
    // 验证Stripe签名
    final event = stripe.webhooks.constructEvent(
      rawBody,
      signature!,
      stripeWebhookSecret,
    );

    // 处理不同类型的事件
    switch (event.type) {
      case 'customer.subscription.created':
        await _handleSubscriptionCreated(event.data.object as Subscription);
        break;
      case 'invoice.paid':
        await _handleInvoicePaid(event.data.object as Invoice);
        break;
      case 'customer.subscription.deleted':
        await _handleSubscriptionDeleted(event.data.object as Subscription);
        break;
      case 'invoice.payment_failed':
        await _handlePaymentFailed(event.data.object as Invoice);
        break;
      default:
        Logger().info('未处理的Stripe事件类型: ${event.type}');
    }

    return Response(statusCode: HttpStatus.ok);
  } on StripeException catch (e) {
    Logger().info('Stripe签名验证失败: ${e.message}');
    return Response(statusCode: HttpStatus.badRequest, body: {'message': e.message});
  } catch (e) {
    Logger().info('Webhook处理失败: ${e.toString()}');
    return Response(statusCode: HttpStatus.internalServerError);
  }
}

// 处理订阅创建事件
Future<void> _handleSubscriptionCreated(Subscription subscription) async {
  final appUserId = subscription.metadata['app_user_id'];
  if (appUserId == null) return;

  await DatabaseService().updateUser(
    appUserId,
    {
      'stripe_subscription_id': subscription.id,
      'subscription_status': subscription.status,
      'current_period_end': subscription.currentPeriodEnd?.toIso8601String(),
    },
  );
}

// 处理付款成功事件(更新订阅状态为活跃)
Future<void> _handleInvoicePaid(Invoice invoice) async {
  final subscriptionId = invoice.subscription;
  if (subscriptionId == null) return;

  final subscription = await Stripe(environment['STRIPE_PRIVATE_KEY']!).subscriptions.retrieve(subscriptionId);
  final appUserId = subscription.metadata['app_user_id'];
  if (appUserId == null) return;

  await DatabaseService().updateUser(
    appUserId,
    {
      'subscription_status': subscription.status,
      'current_period_end': subscription.currentPeriodEnd?.toIso8601String(),
    },
  );
}

// 处理订阅取消事件
Future<void> _handleSubscriptionDeleted(Subscription subscription) async {
  final appUserId = subscription.metadata['app_user_id'];
  if (appUserId == null) return;

  await DatabaseService().updateUser(
    appUserId,
    {
      'stripe_subscription_id': null,
      'subscription_status': 'canceled',
      'brand_subscriptions': [],
    },
  );
}

// 处理付款失败事件(标记订阅状态为逾期)
Future<void> _handlePaymentFailed(Invoice invoice) async {
  final subscriptionId = invoice.subscription;
  if (subscriptionId == null) return;

  final subscription = await Stripe(environment['STRIPE_PRIVATE_KEY']!).subscriptions.retrieve(subscriptionId);
  final appUserId = subscription.metadata['app_user_id'];
  if (appUserId == null) return;

  await DatabaseService().updateUser(
    appUserId,
    {
      'subscription_status': subscription.status,
    },
  );
}

三、关键注意事项

  1. Stripe SDK初始化:建议在应用启动时全局初始化一次Stripe,避免重复创建实例。
  2. 价格管理:提前在Stripe后台创建订阅价格,并在应用数据库中维护品牌与价格ID的映射关系。
  3. Webhook测试:使用stripe listen --forward-to localhost:8080/stripe/webhook命令将Stripe事件转发到本地服务进行测试。
  4. 错误处理:针对Stripe API的异常(如支付失败、客户不存在等)添加更细粒度的错误捕获和用户提示。
  5. 数据一致性:确保应用数据库中的订阅状态与Stripe后台保持同步,依赖Webhook事件进行更新。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 19:44:54