图片被吞了可以来这里看:https://juejin.cn/post/7346388511338381364
1. 准备事项
- Stripe 账号
- 域名以及配套的网站
- Stripe 账号付款信息
- 公钥和私钥
2. 配置产品以及价格
可以通过 API 或者 Stripe 管理后台来进行配置
产品:就是商品,只需要配置一个名称和一个类型(用于计算税额)
价格:价格有定期和一次性两种收费方式,定期其实就是订阅。价格实体非常灵活,适合多种场景,一般就使用固定费率的一次性付款和定期付款。
3. 设计一下流程
4. 代码集成
4.1 依赖导入
stripe/stripe-java: Java library for the Stripe API. (github.com)
<dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>23.3.0</version>
</dependency>
4.2 配置
# 公钥
stripe.key=pk_test_51Nxxxx
# 私钥
stripe.secret=sk_test_51xxxx
# webhook 密钥签名
stripe.endpoint_secret=whsec_Tcxxxx
@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/quickstart
Web 端一般使用 Stripe-hosted page
来简化开发,像 ChatGPT
也是使用这种方式。
后端创建收银台
public CheckoutCreateResult create(CheckoutCreateRequest request) {// 查询或者创建客户String customerId = queryOrCreateCustomer();// 查询价格idString priceId = queryPrice();// 构建成功URL和取消URLUriComponents 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 回调接口
本地测试的方式不是很友好,可以使用内网穿透工具将请求转到本地来进行调试
@RestController
@Slf4j
public class WebhookController {@Resourceprivate StripeConfig stripeConfig;@Resourceprivate 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 事件枚举
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 处理器
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> {@Overridepublic boolean supports(StripeEventType stripeEventType) {return stripeEventType.equals(StripeEventType.CHECKOUT_SESSION_COMPLETED);}@Overridepublic void handle(Session session) {// 完成收银}
}@Component
@Slf4j
public class WebhookHandlerChargeRefundImpl extends WebhookHandlerBase<Charge> {@Overridepublic boolean supports(StripeEventType stripeEventType) {return stripeEventType.equals(StripeEventType.CHARGE_REFUNDED);}@Overridepublic void handle(Charge charge) {// 订单退款}
}
配置 Stripe Webhook
管理平台 – FeloTranslator – Stripe [Test]
4.4.3 完成收银
步骤流程:
- 判断对应的订单是否存在
- 订单所有者
- 对应的Stripe checkout session 状态是否正常
- 订单完成
- 发送订单完成事件
- 事件订阅者处理后续流程
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_statusString status = session.getPaymentStatus();if (StringUtils.notEquals(status, "paid")) {throw new RuntimeException("Checkout has no completed: " + status);}return session;
}
Ref
Documentation | Stripe 文档
Stripe-hosted page | Stripe 文档
stripe/stripe-java: Java library for the Stripe API. (github.com)
Stripe API Reference