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

相关推荐
雾月559 分钟前
LeetCode 1292 元素和小于等于阈值的正方形的最大边长
java·数据结构·算法·leetcode·职场和发展
24k小善1 小时前
Flink TaskManager详解
java·大数据·flink·云计算
想不明白的过度思考者1 小时前
Java从入门到“放弃”(精通)之旅——JavaSE终篇(异常)
java·开发语言
.生产的驴2 小时前
SpringBoot 封装统一API返回格式对象 标准化开发 请求封装 统一格式处理
java·数据库·spring boot·后端·spring·eclipse·maven
猿周LV2 小时前
JMeter 安装及使用 [软件测试工具]
java·测试工具·jmeter·单元测试·压力测试
晨集2 小时前
Uni-App 多端电子合同开源项目介绍
java·spring boot·uni-app·电子合同
时间之城2 小时前
笔记:记一次使用EasyExcel重写convertToExcelData方法无法读取@ExcelDictFormat注解的问题(已解决)
java·spring boot·笔记·spring·excel
椰羊~王小美2 小时前
LeetCode -- Flora -- edit 2025-04-25
java·开发语言
凯酱2 小时前
MyBatis-Plus分页插件的使用
java·tomcat·mybatis
程序员总部2 小时前
如何在IDEA中高效使用Test注解进行单元测试?
java·单元测试·intellij-idea