如何使用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, }, ); }
三、关键注意事项
- Stripe SDK初始化:建议在应用启动时全局初始化一次Stripe,避免重复创建实例。
- 价格管理:提前在Stripe后台创建订阅价格,并在应用数据库中维护品牌与价格ID的映射关系。
- Webhook测试:使用
stripe listen --forward-to localhost:8080/stripe/webhook命令将Stripe事件转发到本地服务进行测试。 - 错误处理:针对Stripe API的异常(如支付失败、客户不存在等)添加更细粒度的错误捕获和用户提示。
- 数据一致性:确保应用数据库中的订阅状态与Stripe后台保持同步,依赖Webhook事件进行更新。
内容的提问来源于stack exchange,提问作者Harsh Jani
相关产品推荐
相关产品推荐

