Stripe Web 购买集成

1. 准备事项

  • Stripe 账号
  • 域名以及配套的网站
  • Stripe 账号付款信息
  • 公钥和私钥

2. 配置产品以及价格

可以通过 API 或者 Stripe 管理后台来进行配置

产品:就是商品,只需要配置一个名称和一个类型(用于计算税额)

image.png

价格:价格有定期和一次性两种收费方式,定期其实就是订阅。价格实体非常灵活,适合多种场景,一般就使用固定费率的一次性付款和定期付款。

image.png

3. 设计一下流程

image.png

4. 代码集成

4.1 依赖导入

stripe/stripe-java: Java library for the Stripe API. (github.com)

 

xml

复制代码

<dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>23.3.0</version> </dependency>

4.2 配置

 

properties

复制代码

# 公钥 stripe.key=pk_test_51Nxxxx # 私钥 stripe.secret=sk_test_51xxxx # webhook 密钥签名 stripe.endpoint_secret=whsec_Tcxxxx

 

java

复制代码

@Data @Configuration @ConfigurationProperties(prefix = "stripe") public class StripeConfig { private String key; private String secret; private String endpointSecret; @Bean public StripeClient stripeClient() { return new StripeClient(secret); } }

4.3 创建收银

Stripe 中有两种方式能进行收款,Stripe-hosted page 和 Embedded form

Stripe-hosted page:指的是收费的时候跳转到 Stripe 提供的一个收银台页面进行付款。

Embedded form:则是需要高度自定义页面的产品使用,或者是客户端。

文档:Stripe Checkout | Stripe 文档

Demo: docs.stripe.com/checkout/qu…

Web 端一般使用 Stripe-hosted page 来简化开发,像 ChatGPT 也是使用这种方式。

image.png

后端创建收银台

 

java

复制代码

public CheckoutCreateResult create(CheckoutCreateRequest request) { // 查询或者创建客户 String customerId = queryOrCreateCustomer(); // 查询价格id String priceId = queryPrice(); // 构建成功URL和取消URL UriComponents successUrl = UriComponentsBuilder.fromHttpUrl(request.getSuccessUrl()) .queryParam("checkout_id", checkoutId) .queryParam("receipt", "{CHECKOUT_SESSION_ID}") // 模板变量 https://stripe.com/docs/payments/checkout/custom-success-page#modify-success-url .build(); UriComponents cancelUrl = UriComponentsBuilder.fromHttpUrl(request.getCancelUrl()) .queryParam("checkout_id", checkoutId) .build(); // 创建checkout 收银台 SessionCreateParams.Builder builder = SessionCreateParams.builder() .setSuccessUrl(successUrl.toUriString()) .setCancelUrl(cancelUrl.toUriString()) // 指定付款用户 .setCustomer(customerId) // 自动扣税 .setAutomaticTax( SessionCreateParams.AutomaticTax.builder() .setEnabled(false) .build()) // 购买项目:和订单明细类似 .addLineItem( SessionCreateParams.LineItem.builder() // 数量 .setQuantity(request.getCount().longValue()) // 价格 .setPrice(priceId) .build()) // 元数据:额外附加的数据。 webhook 通知的时候可以取出来 .putAllMetadata(ImmutableMap.of( MetaDataKey.CHECKOUT_ID, checkoutId, MetaDataKey.APP_ID, request.getAppId() )) // 是否允许优惠码 .setAllowPromotionCodes(Boolean.TRUE); if (productPrice.getPriceType() == PriceTypeEnum.RECURRING) { // 定期价格,最后会创建订阅对象。可以为付款成功后生成的订阅对象设置一些数据 builder.setMode(SessionCreateParams.Mode.SUBSCRIPTION) .setSubscriptionData(// 试用期 SessionCreateParams.SubscriptionData.builder() .putMetadata(MetaDataKey.APP_ID, request.getAppId()) .build()); } else { // 一次性价格,最后会创建付款对象。可以为付款成功后生成的付款对象设置一些数据 builder.setMode(SessionCreateParams.Mode.PAYMENT) .setPaymentIntentData( SessionCreateParams.PaymentIntentData.builder() .putMetadata(MetaDataKey.APP_ID, request.getAppId()) .build()); } SessionCreateParams params = builder.build(); /*.addDiscount( // 优惠券 SessionCreateParams.Discount.builder() .setCoupon("bBfCjIMt") .build())*/ Session session = null; try { session = stripeClient.checkout().sessions().create(params); } catch (StripeException e) { log.error("failed to create checkout session. {}, {}, {}", request, customerId, priceId, e); throw new RuntimeException("failed to create checkout session: "+ e.getMessage()); } return new CheckoutCreateThirdResult() // checkout session 的 id .setId(session.getId()) // 可供用户进行付款的页面链接,前端直接打开即可跳转到Stripe .setTokenThird(session.getUrl()); }

4.4 完成收银

4.4.1 前端提交

用户付款完成后,Stripe 会将页面重定向到创建 Checkout Session 时设置的 success_url。 页面可以从URL中获取到订单id和sessionId来进一步调用后端接口完成收银。

4.4.2 接收 Webhook

用户付款完成后,Stripe 的后台还会将对应的事件通过 WebHook 的方式 POST 我们预先提供的接口。

第一步,先提供一个 Webhook 回调接口

本地测试的方式不是很友好,可以使用内网穿透工具将请求转到本地来进行调试

 

java

复制代码

@RestController @Slf4j public class WebhookController { @Resource private StripeConfig stripeConfig; @Resource private List<WebhookHandler> webhookHandlers; @PostMapping("/webhook") public Object handle(@RequestHeader("Stripe-Signature") String sigHeader, @RequestBody String payload) { log.info("stripe webhook payload: {}", payload); return webhook(payload, sigHeader, stripeConfig.endpointSecret()); } private Object webhook(String payload, String sigHeader, String endpointSecret) { Event event; try { event = Webhook.constructEvent(payload, sigHeader, endpointSecret); StripeEventType stripeEventType = StripeEventType.convert(event.getType()); webhookHandlers.stream() .filter(webhookHandler -> webhookHandler.supports(stripeEventType)) .findFirst() .get().handle(event); } catch (Exception e) { log.error("failed to handle webhook event. {}, {}", sigHeader, payload, e); return ResponseEntity.status(500).body(e.getMessage()); } return ResponseEntity.ok().body("OK"); } }

Stripe 事件枚举

 

java

复制代码

public enum StripeEventType implements EnumBase { // 收银完成 CHECKOUT_SESSION_COMPLETED("checkout.session.completed"), // 退款 CHARGE_REFUNDED("charge.refunded"), IGNORED(""); private final String message; StripeEventType(String message) { this.message = message; } public static StripeEventType convert(String message) { for (StripeEventType value : StripeEventType.values()) { if (StringUtils.equals(value.message(), message)) { return value; } } return IGNORED; } @Override public String message() { return this.message; } @Override public Number value() { return null; } }

Webhook 处理器

 

java

复制代码

public interface WebhookHandler { boolean supports(StripeEventType stripeEventType); void handle(Event event) throws EventDataObjectDeserializationException; } public abstract class WebhookHandlerBase<T> implements WebhookHandler { @SuppressWarnings("unchecked") @Override public void handle(Event event) throws EventDataObjectDeserializationException { EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); StripeObject stripeObject = dataObjectDeserializer.deserializeUnsafe(); handle((T) stripeObject); } public abstract void handle(T stripeObject); } @Component @Slf4j public class WebhookHandlerDefaultImpl implements WebhookHandler { @Override public boolean supports(StripeEventType stripeEventType) { return stripeEventType.equals(StripeEventType.IGNORED); } @Override public void handle(Event event) { log.info("ignored event: {} {}", event.getType(), event.toJson()); } } @Component @Slf4j public class WebhookHandlerCheckoutSessionCompletedImpl extends WebhookHandlerBase<Session> { @Override public boolean supports(StripeEventType stripeEventType) { return stripeEventType.equals(StripeEventType.CHECKOUT_SESSION_COMPLETED); } @Override public void handle(Session session) { // 完成收银 } } @Component @Slf4j public class WebhookHandlerChargeRefundImpl extends WebhookHandlerBase<Charge> { @Override public boolean supports(StripeEventType stripeEventType) { return stripeEventType.equals(StripeEventType.CHARGE_REFUNDED); } @Override public void handle(Charge charge) { // 订单退款 } }

配置 Stripe Webhook

管理平台 – FeloTranslator – Stripe [Test]

image.png

4.4.3 完成收银

步骤流程:

  1. 判断对应的订单是否存在
  2. 订单所有者
  3. 对应的Stripe checkout session 状态是否正常
  4. 订单完成
  5. 发送订单完成事件
  6. 事件订阅者处理后续流程
 

java

复制代码

protected Session checkCheckoutSession(String sessionId) { // 查询是否完成 Session session = null; try { session = stripeClient.checkout().sessions().retrieve(sessionId); } catch (StripeException e) { log.error("failed to query checkout session. {}", sessionId, e); throw new RuntimeException("failed to query checkout session:" + sessionId); } // https://stripe.com/docs/api/checkout/sessions/object#checkout_session_object-payment_status String status = session.getPaymentStatus(); if (StringUtils.notEquals(status, "paid")) { throw new RuntimeException("Checkout has no completed: " + status); } return session; }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值