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

相关推荐
star-yp3 分钟前
vibe coding 博客管理系统
java·spring boot·spring·ai·ai编程
小江的记录本4 分钟前
【JEECG Boot】JEECG Boot 系统性知识体系全方位结构化总结
java·前端·spring boot·后端·python·spring·spring cloud
Mr.wangh5 分钟前
Spring原理(Bean的生命周期)
java·前端·spring
派大星酷8 分钟前
Java 多线程创建方式
java·开发语言·多线程
棉花骑士9 小时前
【AI Agent】面向 Java 工程师的Claude Code Harness 学习指南
java·开发语言
爱敲代码的小鱼9 小时前
springboot(2)从基础到项目创建:
java·spring boot·spring
迈巴赫车主10 小时前
蓝桥杯19724食堂
java·数据结构·算法·职场和发展·蓝桥杯
i220818 Faiz Ul11 小时前
动漫商城|基于springboot + vue动漫商城系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·动漫商城系统
海兰11 小时前
【实战】MCP 服务在 Nacos 中注册状态分析与优化
android·java·github·银行系统·银行ai
Makoto_Kimur12 小时前
Java 打印模板大全
java·开发语言·排序算法