告别混乱概念!一文搞懂Stripe的Payment Intent、Session与Charge,并用SpringBoot 3实现订阅支付
告别混乱概念一文搞懂Stripe的Payment Intent、Session与Charge并用SpringBoot 3实现订阅支付第一次接触Stripe的开发者往往会被Payment Intent、Checkout Session、Charge、Price等概念搞得晕头转向。这些术语看似相似实则各司其职共同构成了Stripe强大的支付生态系统。本文将用通俗易懂的方式梳理这些核心概念的关系并通过一个完整的SpringBoot 3.x项目演示如何实现订阅支付功能。1. Stripe支付核心概念解析1.1 支付流程中的关键角色想象Stripe的支付系统就像一家餐厅Customer顾客在Stripe中代表支付方可以保存支付方式信息Price菜单上的价格定义商品或服务的定价Product菜单项代表你销售的商品或服务PaymentIntent顾客的支付意图记录支付状态和金额Checkout Session服务员处理整个支付流程Charge实际的资金转移相当于完成交易1.2 概念关系图Customer → 创建 Subscription → 使用 Price ↘ 创建 PaymentIntent → 通过 Checkout Session 完成支付 → 生成 Charge1.3 何时使用哪种方式场景推荐方式特点一次性支付PaymentIntent简单直接适合单次交易复杂支付流程Checkout Session提供完整支付页面支持多种支付方式订阅服务Subscription自动周期性收费管理生命周期直接扣款Charge已获得客户授权时的直接扣款2. SpringBoot 3.x集成Stripe基础配置2.1 项目初始化首先创建一个新的SpringBoot项目添加必要的依赖dependencies !-- Stripe Java SDK -- dependency groupIdcom.stripe/groupId artifactIdstripe-java/artifactId version26.3.0/version /dependency !-- Spring Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 配置属性支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency /dependencies2.2 配置Stripe密钥在application.properties中添加stripe.api-keysk_test_your_test_key_here stripe.webhook-secretwhsec_your_webhook_secret创建配置类读取这些属性Configuration ConfigurationProperties(prefix stripe) public class StripeConfig { private String apiKey; private String webhookSecret; PostConstruct public void init() { Stripe.apiKey this.apiKey; } // getters and setters }3. 实现订阅支付全流程3.1 创建订阅产品首先需要定义订阅产品和价格public String createSubscriptionProduct(String productName, String currency, Long unitAmount, String interval) { try { // 1. 创建产品 Product product Product.builder() .setName(productName) .build() .create(); // 2. 创建价格 Price price Price.builder() .setProduct(product.getId()) .setCurrency(currency) .setUnitAmount(unitAmount) .setRecurring(Price.Recurring.builder() .setInterval(Price.Recurring.Interval.valueOf(interval)) .build()) .build() .create(); return price.getId(); } catch (StripeException e) { throw new RuntimeException(创建订阅产品失败, e); } }3.2 创建订阅Checkout Session这是订阅流程的核心部分public String createSubscriptionCheckoutSession(String customerEmail, String priceId, String successUrl, String cancelUrl) { try { SessionCreateParams params SessionCreateParams.builder() .setCustomerEmail(customerEmail) .setSuccessUrl(successUrl) .setCancelUrl(cancelUrl) .setMode(SessionCreateParams.Mode.SUBSCRIPTION) .addLineItem( SessionCreateParams.LineItem.builder() .setPrice(priceId) .setQuantity(1L) .build()) .build(); Session session Session.create(params); return session.getUrl(); } catch (StripeException e) { throw new RuntimeException(创建订阅会话失败, e); } }3.3 处理Webhook事件订阅支付需要处理多种Webhook事件RestController RequestMapping(/webhook) public class StripeWebhookController { Autowired private StripeConfig stripeConfig; PostMapping public ResponseEntityString handleWebhook( RequestBody String payload, RequestHeader(Stripe-Signature) String sigHeader) { try { Event event Webhook.constructEvent(payload, sigHeader, stripeConfig.getWebhookSecret()); switch (event.getType()) { case customer.subscription.created: handleSubscriptionCreated(event); break; case customer.subscription.updated: handleSubscriptionUpdated(event); break; case customer.subscription.deleted: handleSubscriptionDeleted(event); break; case invoice.payment_succeeded: handleInvoicePaid(event); break; case invoice.payment_failed: handleInvoiceFailed(event); break; default: log.info(未处理的事件类型: {}, event.getType()); } return ResponseEntity.ok().build(); } catch (Exception e) { return ResponseEntity.badRequest().body(e.getMessage()); } } private void handleSubscriptionCreated(Event event) { Subscription subscription (Subscription) event.getData().getObject(); log.info(新订阅创建: {}, subscription.getId()); // 业务逻辑激活用户订阅状态 } // 其他事件处理方法类似... }4. 高级订阅管理功能4.1 订阅升级与降级允许用户更改订阅计划public Subscription changeSubscriptionPlan(String subscriptionId, String newPriceId) { try { Subscription subscription Subscription.retrieve(subscriptionId); SubscriptionUpdateParams params SubscriptionUpdateParams.builder() .addItem(SubscriptionUpdateParams.Item.builder() .setId(subscription.getItems().getData().get(0).getId()) .setPrice(newPriceId) .build()) .setProrationBehavior(SubscriptionUpdateParams.ProrationBehavior.CREATE_PRORATIONS) .build(); return subscription.update(params); } catch (StripeException e) { throw new RuntimeException(更改订阅计划失败, e); } }4.2 订阅暂停与恢复public Subscription pauseSubscription(String subscriptionId) { try { SubscriptionUpdateParams params SubscriptionUpdateParams.builder() .setPauseCollection(SubscriptionUpdateParams.PauseCollection.builder() .setBehavior(SubscriptionUpdateParams.PauseCollection.Behavior.VOID) .build()) .build(); return Subscription.retrieve(subscriptionId).update(params); } catch (StripeException e) { throw new RuntimeException(暂停订阅失败, e); } } public Subscription resumeSubscription(String subscriptionId) { try { SubscriptionUpdateParams params SubscriptionUpdateParams.builder() .setPauseCollection(SubscriptionUpdateParams.PauseCollection.builder() .setBehavior(SubscriptionUpdateParams.PauseCollection.Behavior.UNPAUSE) .build()) .build(); return Subscription.retrieve(subscriptionId).update(params); } catch (StripeException e) { throw new RuntimeException(恢复订阅失败, e); } }4.3 优惠券与折扣public Subscription applyCouponToSubscription(String subscriptionId, String couponId) { try { SubscriptionUpdateParams params SubscriptionUpdateParams.builder() .setCoupon(couponId) .build(); return Subscription.retrieve(subscriptionId).update(params); } catch (StripeException e) { throw new RuntimeException(应用优惠券失败, e); } }5. 测试与调试技巧5.1 测试信用卡号Stripe提供了一系列测试卡号卡号场景4242424242424242基本成功支付40000000000032203D Secure验证4000000000000002支付失败5555555555554444国际信用卡(Master)5.2 Webhook本地测试使用Stripe CLI进行本地测试stripe listen --forward-to localhost:8080/webhook stripe trigger payment_intent.succeeded5.3 常见问题排查提示当Webhook无法正常工作时首先检查签名验证和事件类型过滤是否正确支付失败检查是否设置了正确的支付方式Webhook未触发验证端点URL是否可公开访问订阅不续费检查客户是否有有效的支付方式货币不匹配确保所有操作使用相同的货币在实际项目中我发现最常遇到的问题是对事件处理的不完整。例如只处理了订阅创建事件却忽略了续费失败的情况。建议为所有关键事件都添加日志记录这样当问题发生时可以快速定位。