Stripe Web 购买集成

1. 准备事项

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

2. 配置产品以及价格

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

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

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

3. 设计一下流程

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 pageEmbedded form

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

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

文档:Stripe Checkout | Stripe 文档

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

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

后端创建收银台

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]

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;
}

Ref

Documentation | Stripe 文档

Stripe-hosted page | Stripe 文档

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

Stripe API Reference

相关推荐
小扳4 分钟前
微服务篇-深入了解使用 RestTemplate 远程调用、Nacos 注册中心基本原理与使用、OpenFeign 的基本使用
java·运维·分布式·后端·spring·微服务·架构
阿乾之铭5 分钟前
Spring Framework的常用工具类
java
北巷!!9 分钟前
宇信科技JAVA笔试(2024-11-26日 全部AK)
java·开发语言·科技
ᝰꫝꪉꪯꫀ36110 分钟前
JavaWeb——SpringBoot原理
java·开发语言·后端·springboot
HaiFan.14 分钟前
Spring日志
java·spring boot
爬山算法16 分钟前
Tomcat(36)Tomcat的静态资源缓存
java·缓存·tomcat
TPBoreas17 分钟前
手搓一个不用中间件的分表策略
java
、十一、18 分钟前
Tomcat的工作模式是什么?
java·tomcat
killsime20 分钟前
JavaWeb开发 : tomcat+Servlet+JSP
java·servlet·tomcat·javaweb
刽子手发艺40 分钟前
云服务器部署springboot项目、云服务器配置JDK、Tomcat
java·后端·部署