当前位置: 首页 > article >正文

【开发心得】SpringBoot对接Stripe支付

概述

    应用出海,需要对接国外的支付,之一的选择就是Stripe。这个介绍下Java对接Stripe的实现。

官网: Stripe | Financial Infrastructure to Grow Your Revenue

API文档:
https://docs.stripe.com/apiicon-default.png?t=O83Ahttps://docs.stripe.com/api

Stripe支付方式和概念比较多。

  1. 付款方式: 一次性付款和周期订阅付款。
  2. 支付方式: Paylink、charge、pay intent多种概念。
  3. 对接方式: 前端对接,后端对接两种。

Stripe在认证开通之前,支付属于沙箱模式。支付的时候,卡号填 4242 4242 4242 4242即可(在外网资料中看到的,暂时不知道其他特殊卡号是否支持)。

国内正式环境测试可以通过一些双币卡,比如东方航空与中信银行的联名信用卡。(吐槽下,Stripe有一个最低消费额,换算成人民币,3块左右每次)

对接步骤

1. 引入maven/gradle依赖

这里以maven为例。(2024年5月份对接的时候,我这里选型26.3,网上一些资料基于更老的版本)

<!-- https://mvnrepository.com/artifact/com.stripe/stripe-java -->
<dependency>
	<groupId>com.stripe</groupId>
	<artifactId>stripe-java</artifactId>
	<!--                <version>24.16.0</version>-->
	<version>26.3.0</version>
</dependency>
2. webhook监听

设置webhook监听地址:

(1) 页面方式

入口比较隐蔽,可以在全局搜

地址填写实现webhook的地址。事件根据需要选择。

创建完后点击对应链接,获得密钥签名webHookSecret

(2) 代码方式创建

import com.stripe.Stripe;
import com.stripe.model.Event;
import com.stripe.model.WebhookEndpoint;
import com.stripe.net.WebhookEndpointCollection;
import com.stripe.exception.*;
 
public class CreateWebhookEndpoint {
    public static void main(String[] args) {
        Stripe.apiKey = "your_stripe_api_key";
 
        try {
            WebhookEndpoint endpoint = WebhookEndpoint.create(
                "https://your-webhook-handler-domain.com",
                Arrays.asList("charge.succeeded", "charge.failed")
            );
 
            System.out.println(endpoint.getId());
        } catch (StripeException e) {
            e.printStackTrace();
        }
    }
}

Stripe有个回调机制,在一定时间内,不断重试知道请求成功。它依赖于http的状态码。

关于Stripe的Java服务端监听,官方文档参考: Stripe Login | Sign in to the Stripe DashboardSign in to the Stripe Dashboard to manage business payments and operations in your account. Manage payments and refunds, respond to disputes and more.icon-default.png?t=O83Ahttps://dashboard.stripe.com/webhooks/create?endpoint_location=hosted

  private static final String PAYLOAD_CHARSET = "UTF-8";

  private static final String STRIPE_SIGNATURE_FIELD = "Stripe-Signature";

  private static final String REMOTE_SUCCEED_FIELD = "succeeded";

stripeApiKey与webHookSecret  这里是我从配置文件中读取出来的的,@Value方式,可自行实现。

stripeApiKey 是Stripe的支付sk

webHookSecret是回调页面配置获取的key

webhook 监听的事件包含如下几个常用,其实比较多,可以通过页面设置webhook,或者代码设置webhook的时候指定。

* charge.succeed
* charge.canceled
* payment_intent.succeeded
* payment_intent.failed
* payment_intent.canceled
    @ResponseBody
    @PostMapping(value = "/callback")
    public void postEventsWebhook(HttpServletRequest request, HttpServletResponse response) {
        Stripe.apiKey = stripeApiKey;
        try {
//            String payload = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
            InputStream inputStream = request.getInputStream();
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024 * 4];
            int n = 0;
            while (-1 != (n = inputStream.read(buffer))) {
                output.write(buffer, 0, n);
            }
            byte[] bytes = output.toByteArray();
            String payload = new String(bytes, PAYLOAD_CHARSET);
            if (!StringUtils.isEmpty(payload)) {
                String sigHeader = request.getHeader(STRIPE_SIGNATURE_FIELD);
                String endpointSecret = webHookSecret;
                Event event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
                Optional<StripeObject> stripeObject = event.getDataObjectDeserializer().getObject();
                String remoteStatus = null;
                Map<String, String> metaData = null;
                if (stripeObject.isPresent()) {
                    PaymentIntent intent = (PaymentIntent) stripeObject.get();
                    remoteStatus = intent.getStatus();
                    metaData = intent.getMetadata();
                } else {
                    StripeObject sobj = event.getData().getObject();
                    if (sobj instanceof Charge) {
                        Charge object = (Charge) sobj;
                        remoteStatus = object.getStatus();
                        metaData = object.getMetadata();
                    } else {
                        PaymentIntent intent = (PaymentIntent) sobj;
                        remoteStatus = intent.getStatus();
                        metaData = intent.getMetadata();
                    }
                }

                ProcessStatus processStatus = null;
                StripePayResultEnum payResultEnum = StripePayResultEnum.getByName(event.getType());
                switch (payResultEnum) {
                    case PI_CREATED: //创建订单
                        // 这种先不处理
                        break;
                    case PI_CANCELED: // 取消订单
                        processStatus = ProcessStatus.CANCELLED;
                        break;
                    case PI_SUCCEED: // 支付成功
                    case CH_SUCCEED: // 结算成功
                        if (REMOTE_SUCCEED_FIELD.equals(remoteStatus)) {
                            processStatus = ProcessStatus.COMPLETED;
                        }
                        break;
                    case PI_FAILED: // 支付失败
                    case CH_FAILED: // 结算失败
                        processStatus = ProcessStatus.ERROR;
                        break;
                    default:
                        break;
                }
                if (processStatus != null) {
                    //自定义传入的参数
                    String orderNo = metaData.get(GatewayConstant.PAY_ORDER_NO);
                    log.info("finished, orderNo:{}, webhook event:{}, status:{}", orderNo, payResultEnum, processStatus);
                    Boolean finish = orderService.finish(orderNo, processStatus);
                    log.info("orderNo:{} deal finished, result:{}", orderNo, finish);
                }
                response.setStatus(200);
            }

        } catch (Exception e) {
            response.setStatus(500);
            log.error(e.getMessage(), e);
        }
    }

可以通过内网穿透,或者把应用放到云服务器上测试。

也可以通过本地webhook模拟测试,参考:

Stripe Login | Sign in to the Stripe DashboardSign in to the Stripe Dashboard to manage business payments and operations in your account. Manage payments and refunds, respond to disputes and more.icon-default.png?t=O83Ahttps://dashboard.stripe.com/webhooks/create?endpoint_location=local

3. 业务支付

(1) 创建PriceId

XxxPrice(自己命名):

    private String name;     // 名称
    private Long amount;     // 金额
    private String currency; // 货币单位(如usd)

checkPrice是简单判断了一下,param参数的货币,金额,调用该代码之后,我们会得到一个price_开头的价格id。将其记录下来,与自定义的商品关联。不用每次创建,这个在商品创建与价格更新的时候处理一次即可。

    public String createPrice(XxxPrice param) {
        this.checkPrice(param);
        Stripe.apiKey = privateKey;
        try {
            PriceCreateParams params =
                    PriceCreateParams.builder()
                            .setCurrency(param.getCurrency())
                            .setUnitAmount(param.getAmount())
                            .setProductData(
                                    PriceCreateParams.ProductData.builder().setName(param.getName()).build()
                            )
                            .build();
            Price price = Price.create(params);
            if (price != null) {
                return price.getId();
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }

        return null;
    }

 

(2) 创建PayIntent(支付意图,这是Stripe较新的概念)

这里SuccessUrl和CancelUrl自定设置,作用是支付页面支付完成后,重定向到对应的地址。

Quantity是数量,PriceId是上一步获得的id

/**
     * @param priceId
     * @return
     * @throws StripeException
     * @description: 获取支付链接
     */
    public Session createPayment(String priceId, String orderNo) throws StripeException {
        String payStatusUrl = this.getPayStatusUrl(orderNo);
        Stripe.apiKey = privateKey;
        SessionCreateParams.Builder builder = SessionCreateParams.builder();

        builder.setMode(SessionCreateParams.Mode.PAYMENT);
        builder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder()
                .putMetadata(GatewayConstant.PAY_ORDER_NO, orderNo)
                .build());
        SessionCreateParams params =
                builder
                        // 支付成功跳转
                        .setSuccessUrl(payStatusUrl)
                        // 支付取消跳转
                        .setCancelUrl(payStatusUrl)
                        .addLineItem(
                                SessionCreateParams.LineItem.builder()
                                        .setQuantity(1L)
                                        // Provide the exact Price ID (for example, pr_1234) of the product you want to sell
                                        // 传入价格ID
                                        .setPrice(priceId)
                                        .build())
                        .build();
        Session session = Session.create(params);
        return session;
    }

这个会得到一个 Session,id为一个pi_开头的字符串,代表付款唯一标识,url是支付界面的url,这个需要前端/客户端展示的。页面类似如下,其中商品名称,描述,货币单位,价格等都可以代码控制。(截图是正式的样例,测试的会有提示测试环境的字样,测试环境可以使用4242 4242 4242 4242来测试)

关于签名: SDK可以自动验证签名:

 String sigHeader = request.getHeader(STRIPE_SIGNATURE_FIELD);
 String endpointSecret = webHookSecret;
 Event event = Webhook.constructEvent(payload, sigHeader, endpointSecret);

关于携带自定义字段:

SessionCreateParams的putMetadata方法实际是一个map,可以设置自定义的键值对来传递。比如我这里传递的orderNo。

结语:

虽然概念多,但实际Stripe的文档也足够详细了。多看文档,多看sdk的源码,对接本身还是简单的。


http://www.kler.cn/a/459552.html

相关文章:

  • springCloud 脚手架项目功能模块:Java分布式锁
  • 如何判断状态:停留还是移动。【计算加速度de方案】
  • canvas+fabric实现时间刻度尺+长方形数据展示
  • RabbitMQ基础篇之Java客户端快速入门
  • 【模型】Qwen2-VL 服务端UI
  • Rabbitmq追问1
  • 国产数据库TiDB从入门到放弃教程
  • python -【es】基本使用
  • 什么是自治系统和非自治系统
  • Android ActionBar 技术深度解析
  • 上海人工智能方案引领,CES Asia 2025共筑机器人未来
  • PHP语言的编程范式
  • HAL库STM32硬件IIC驱动数字电位器MCP4017
  • Linux(Centos 7.6)基础命令/常用命令说明
  • Python爬虫(二)- Requests 高级使用教程
  • 25 go语言(golang) - 内存分配机制原理
  • 鱼眼相机模型与去畸变实现
  • MySQL数据导出导出的三种办法(1316)
  • JAVA毕业设计205—基于Java+Springboot+vue3的民宿酒店管理系统(源代码+数据库)
  • 基于FPGA的RLC电阻电容电感测试仪设计(论文+源码)
  • XDMA IP
  • React 性能优化十大总结
  • springboot511基于SpringBoot视频点播系统的设计与实现(论文+源码)_kaic
  • php 静态变量
  • linux 配置端口转发
  • 前端Python应用指南(四)Django实战:创建一个简单的博客系统