告别混乱概念!一文搞懂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> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>26.3.0</version> </dependency> <!-- Spring Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 配置属性支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies>2.2 配置Stripe密钥
在application.properties中添加:
stripe.api-key=sk_test_your_test_key_here stripe.webhook-secret=whsec_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 ResponseEntity<String> 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 | 基本成功支付 |
| 4000000000003220 | 3D 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是否可公开访问
- 订阅不续费:检查客户是否有有效的支付方式
- 货币不匹配:确保所有操作使用相同的货币
在实际项目中,我发现最常遇到的问题是对事件处理的不完整。例如,只处理了订阅创建事件,却忽略了续费失败的情况。建议为所有关键事件都添加日志记录,这样当问题发生时可以快速定位。
