Stripe是一家美国科技公司,成立于2010年,由爱尔兰兄弟Patrick Collison和John Collison共同创立。该公司致力于提供高效、简洁的互联网支付收款服务,为开发者或商家提供支付API接口或代码,使商家的网站、移动APP支持信用卡付款。Stripe被誉为“移动时代的PayPal”,因其简便的支付方式而受到广泛欢迎。

Stripe 总共有三种支付方式:
1、Stripe Checkout,pay links 创建支付链接,
2、payment intent,后端预下单,返回秘钥,前端确定订单
3、前端创建支付token ,后端创建Charge,返回支付结果链接

Stripe接口调用时序图

后台服务 Stripe 【初始化】客户端 StripeClient,使用从Stripe平台获取的私钥 初始化成功,创建 StripeClient 实例 【创建产品】 client.products().create() 创建产品成功,返回产品信息 【创建产品价格】 client.prices().create(),给产品创建指定币种的价格 创建产品价格成功,返回价格信息 【下单】 client.checkout().sessions().create() 返回订单信息,包含支付链接 session.getUrl() 和订单id 浏览器打开支付链接【完成支付】 支付完成,跳转到完成页面 【支付完成回调】webhook请求 checkout.session.completed 事件,返回 Session 【支付成功回调】webhook请求 charge.succeeded 事件,返回 Charge 【查询订单】client.charges().retrieve(chargeId) 返回订单信息 【退款】client.refunds().create(),传入 chargeId 返回退款订单信息(退款ID refundId) 【查询退款订单】 client.refunds().retrieve(refundId) 或 client.refunds().list() 返回退款订单信息 后台服务 Stripe

0、初始化客户端

StripeClient client = StripeClient.builder()
             .setConnectTimeout(30 * 1000)
             .setReadTimeout(80 * 1000)
             .setApiKey("sk_test_51PtO7DC6XhwanSnNvGezNPc4hsL2F****")
             .build();

1、产品

查询或创建新产品。

每次交易传入产品名称或描述,自动查询是否已经存在,如果存在则直接使用,如果不存在则新建产品。

private Product getProduct(StripeOrder stripeOrder) throws StripeException {
     ProductSearchParams searchParams =
             ProductSearchParams.builder()
                     .setQuery("active:'true' AND name:'" + stripeOrder.getSubject()
                             + "' AND description:'" + stripeOrder.getBody() + "'")
                     .setLimit(1L)
                     .build();
     StripeSearchResult<Product> result = client.products().search(searchParams);
     Product product;
     if (result != null && !result.getData().isEmpty()) {
         product = result.getData().stream().findFirst().get();
     } else {
         //创建产品 https://stripe.com/docs/api/products/create
         ProductCreateParams params = ProductCreateParams.builder()
                 .setDescription(stripeOrder.getBody())
                 .setName(stripeOrder.getSubject()).build();
         product = client.products().create(params);
     }
     return product;
 }

2、价格

根据产品ID创建对应币种的价格,指定 lookupKey = 价格+币种+产品ID,作为价格关键字,用于查询是否已经存在。如果价格存在则直接使用,如果不存在则新增新的价格。

private Price getPrice(StripeOrder stripeOrder, String productId) throws StripeException {
    Long unitAmount = Util.conversionCentAmount(stripeOrder.getPrice());
    String lookupKey = unitAmount + stripeOrder.getCurrencyCode() + productId;
    PriceSearchParams params =
            PriceSearchParams.builder()
                    .setQuery("active:'true' AND product:'" + productId
                            + "' AND currency:'" + stripeOrder.getCurrencyCode()
                            + "' AND lookup_key:'" + lookupKey + "'")
                    .build();
    StripeSearchResult<Price> result = client.prices().search(params);
    Price price;
    if (result != null && !result.getData().isEmpty()) {
        price = result.getData().stream().findFirst().get();
    } else {
        //创建价格 https://stripe.com/docs/api/prices/create
//          PriceCreateParams.Recurring recurring = PriceCreateParams.Recurring.builder()
//                    .setInterval(PriceCreateParams.Recurring.Interval.MONTH).build();
        PriceCreateParams priceCreateParams = PriceCreateParams.builder()
                .setCurrency(stripeOrder.getCurrencyCode())
                .setProduct(productId)
                .setLookupKey(lookupKey)
                .setUnitAmount(unitAmount)
//                    .setRecurring(recurring)
                .build();
        price = client.prices().create(priceCreateParams);
    }
    return price;
}

3、创建支付

根据上次返回的价格信息,创建新的支付对象,这里指定银行卡支付,也可以指定别的支付方式;Stripe支持几十种支付方式,可以根据不同国家选择,具体可以在这里查看

下面通过 client.checkout().sessions().create() 创建支付,取得支付链接,在浏览器直接打开即可支付。

public Map<String, Object> orderInfo(PayOrder order) {
    StripeOrder stripeOrder = (StripeOrder) order;
    try {
        Product product = getProduct(stripeOrder);
        Price price = getPrice(stripeOrder, product.getId());
        //创建支付信息 得到url
        SessionCreateParams sessionCreateParams = SessionCreateParams.builder()
                .setMode(SessionCreateParams.Mode.PAYMENT)
                .addPaymentMethodType(SessionCreateParams.PaymentMethodType.CARD)
//                    .addPaymentMethodType(SessionCreateParams.PaymentMethodType.ALIPAY)
                .setSuccessUrl(payConfigStorage.getReturnUrl())
                .setCancelUrl(payConfigStorage.getCancelUrl())
                .setCustomer(stripeOrder.getCustomer())
                .setClientReferenceId(stripeOrder.getClientReferenceId())
                .setCustomerEmail(stripeOrder.getCustomerEmail())
                .addLineItem(
                        SessionCreateParams.LineItem.builder()
                                .setQuantity(1L)
                                .setPrice(price.getId())
                                .build())
                .putMetadata("outTradeNo", stripeOrder.getOutTradeNo())
                .build();
        Session session = client.checkout().sessions().create(sessionCreateParams);
        LOG.info("session:{}", JSON.toJSONString(session));
        return preOrderHandler(Collections.singletonMap("paymentLink", session.getUrl()), order);

    } catch (StripeException e) {
        throw new RuntimeException(e);
    }
}

4、打开支付链接,完成支付

输入Stripe平台提供的测试卡信息,完成支付

test@example.com
4242 4242 4242 4242
12/34
567
Zhang San
United States
12345

在这里插入图片描述

5、支付订单查询

传入回调信息中得到的chargeId,查询订单状态。在返回的json数据了包含支付平台receiptUrl

public Map<String, Object> query(AssistOrder assistOrder) {
    try {
        Charge charge = client.charges().retrieve(assistOrder.getTradeNo());
        // 使用Hutool的BeanUtil将User对象转换为Map
        return BeanUtil.beanToMap(charge);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

支付凭证

在这里插入图片描述

6、退款

传入回调信息中得到的chargeId,提交退款请求;返回退款订单信息,包含退款订单Id(可用于查询退款订单)

public RefundResult refund(RefundOrder refundOrder) {
    RefundCreateParams params = RefundCreateParams.builder()
            .setCharge(refundOrder.getTradeNo())
            .build();
    try {
        Refund refund = client.refunds().create(params);
        LOG.info("refund:{}", JSON.toJSONString(refund));
        StripeRefundResult refundResult = new StripeRefundResult(refund, refundOrder.getTradeNo());
        refundOrder.setRefundNo(refundResult.getRefundNo());
        return refundResult;
    } catch (StripeException e) {
        throw new RuntimeException(e);
    }
}

7、退款查询

传入回调信息中得到的chargeId,查询退款订单;也可以通过退款订单Id查询。

public Map<String, Object> refundquery(RefundOrder refundOrder) {
    RefundListParams params = RefundListParams.builder()
            .setCharge(refundOrder.getTradeNo())
            .build();
    try {
        StripeCollection<Refund> result = client.refunds().list(params);
        if (!result.getData().isEmpty()) {
            Refund refund = result.getData().stream().findFirst().get();
            // 使用Hutool的BeanUtil将User对象转换为Map
            return BeanUtil.beanToMap(refund);
        }
    } catch (StripeException e) {
        throw new RuntimeException(e);
    }
    return null;
}

8、回调通知

登录Stripe平台,在https://dashboard.stripe.com/workbench/webhooks中配置Webhook;菜单地址“开发人员-Webhook”。
在这里插入图片描述

Webhook回调代码示例

public class StripePayMessageHandler implements PayMessageHandler<PayMessage, StripePayService> {
    private final Logger LOG = LoggerFactory.getLogger(getClass());
    private final String endpointSecret = "whsec_HlzC2omyh4V4X3BCgMx5PScTCmwZpvAC";//webhook秘钥签名

    @Override
    public PayOutMessage handle(PayMessage payMessage, Map<String, Object> context, StripePayService payService) throws PayErrorException {
        Map<String, Object> message = payMessage.getPayMessage();
        NoticeParams noticeParams = (NoticeParams) context.get("noticeParams");
        try {
            String sigHeader = noticeParams.getHeader("Stripe-Signature");
            Event event = Webhook.constructEvent(noticeParams.getBodyStr(), sigHeader, endpointSecret);//验签,并获取事件
            StripeObject eventObj = event
                    .getDataObjectDeserializer()
                    .getObject()
                    .get();
            LOG.info("EventType:{}, Event:{}", event.getType(), JSON.toJSONString(eventObj));
            PaymentIntent intent;
            Charge charge;
            String outTradeNo;
            String chargeId;
            String receiptUrl;
            switch (event.getType()) {
                case "charge.succeeded"://支付成功
                    //TODO 支付成功,处理业务逻辑
                    charge = (Charge) eventObj;
                    // 取得 chargeId ,在退款时使用
                    chargeId = charge.getId();
                    receiptUrl = charge.getReceiptUrl();
                    message.put("trade_no", chargeId);
                    LOG.info("支付成功 Charge, chargeId:{}, receiptUrl:{}", chargeId, receiptUrl);
                    break;
                case "checkout.session.completed":// 通过支付链接 支付完成
                    //TODO 支付完成,处理业务逻辑
                    Session session = (Session) eventObj;
                    outTradeNo = session.getMetadata().get("outTradeNo");//自定义订单号
                    LOG.info("支付完成 Session, 订单号为:{}", outTradeNo);
                    message.put("out_trade_no", outTradeNo);
                    break;
                case "charge.refunded"://退款成功
                    charge = (Charge) eventObj;
                    if (charge.getStatus().equals("succeeded")) {
                        //TODO 退款成功,处理业务逻辑
                        chargeId = charge.getId();
                        receiptUrl = charge.getReceiptUrl();
                        message.put("trade_no", chargeId);
                        LOG.info("退款成功, chargeId:{}, receiptUrl:{}", chargeId, receiptUrl);
                    }
                    break;
                case "checkout.session.expired"://过期
                    break;
                case "payment_intent.created"://创建订单 这里事件就是图二选着的事件
                    break;
                case "payment_intent.canceled"://取消订单
                    break;
                case "payment_intent.succeeded"://支付成功
                    intent = (PaymentIntent) eventObj;
                    Map<String, String> metaData = intent.getMetadata();//自定义传入的参数
                    outTradeNo = metaData.get("outTradeNo");//自定义订单号
                    message.put("out_trade_no", outTradeNo);
                    LOG.info("支付成功 payment_intent, 订单号为:{}", outTradeNo);
                    //*********** 根据订单号从数据库中找到订单,并将状态置为成功 *********//*
                    break;
                case "payment_intent.payment_failed"://支付失败
                    intent = (PaymentIntent) eventObj;
                    LOG.info("Failed: " + intent.getId());
                    break;
                default:
                    break;
            }
        } catch (Exception e) {
            LOG.error("stripe异步通知(webhook事件)", e);
        }
        // TODO 支付确认逻辑处理
        return payService.successPayOutMessage(payMessage);
    }
}

参考

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐