【支付】Stripe支付通道Java对接(产品 价格 支付 查询 退款 回调)

news2024/9/21 0:51:42

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

参考

  • https://docs.stripe.com/api
  • https://docs.stripe.com/js
  • https://docs.stripe.com/webhooks
  • https://docs.stripe.com/search#query-fields-for-products

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2125492.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微深节能 天车无人抓渣系统 格雷母线定位系统

微深节能的天车无人抓渣系统结合格雷母线定位系统&#xff0c;为工业自动化领域带来了显著的技术提升。 一、系统概述 微深节能的天车无人抓渣系统是一种高度自动化的解决方案&#xff0c;它集成了格雷母线定位系统&#xff0c;实现了天车在无人操作下的精准定位与高效作业。该…

新颖的团建分组方式

如何实现男女比例平均分组&#xff1f; 在组织团建活动时&#xff0c;采用新颖的分组方式可以增加团队的互动性和参与感。本文将介绍一种基于云分组小程序的男女比例平均分组方法&#xff0c;以及如何高效地邀请成员加入和管理分组。 步骤一&#xff1a;创建分组 1. 进入云分组…

JAVAWeb---JavaScript

第三章 JavaScript 一 JS简介 1.1 JS起源 Javascript是一种由Netscape(网景)的LiveScript发展而来的原型化继承的面向对象的动态类型的区分大小写的客户端脚本语言&#xff0c;主要目的是为了解决服务器端语言&#xff0c;遗留的速度问题&#xff0c;为客户提供更流畅的浏览效…

【English】长难句翻译

这里写目录标题 技巧知识点1. 定语从句 和 状从区别2. 定从 修饰词3. who 和 whom 区别4. 除了定从、状从,还有啥?5. 怎么在长难句快速定位到主谓宾而不被各种从句中的动词影响判断6. 没有,的那种一大堆从句连起来的长难句怎么办7. 时态怎么放在翻译里总结技巧 知识点 1. 定语…

游戏论坛网站|基于Springboot+vue的游戏论坛网站系统游戏分享网站(源码+数据库+文档)

游戏论坛|游戏论坛系统|游戏分享网站 目录 基于Springbootvue的游戏论坛网站系统游戏分享网站 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大…

[数据集][目标检测]汽车头部尾部检测数据集VOC+YOLO格式5319张3类别

数据集制作单位&#xff1a;未来自主研究中心(FIRC) 版权单位&#xff1a;未来自主研究中心(FIRC) 版权声明&#xff1a;数据集仅仅供个人使用&#xff0c;不得在未授权情况下挂淘宝、咸鱼等交易网站公开售卖,由此引发的法律责任需自行承担 数据集格式&#xff1a;Pascal VOC格…

DWS=业务库创建

数据库对象命名 数据库对象命名需要满足约束&#xff1a;长度不超过63个字符&#xff0c;以字母或下划线开头&#xff0c;中间字符可以是字母、数字、下划线、$、#。 【建议】避免使用保留或者非保留关键字命名数据库对象。可以使用select * from pg_get_keywords()查询GaussDB…

有什么好用的视频倒放软件?2024帮助你快速进行视频倒放软件

有什么好用的视频倒放软件&#xff1f;2024帮助你快速进行视频倒放软件 视频倒放是一种常见的视频编辑效果&#xff0c;能够为视频增添趣味或实现特殊的视觉效果。以下是5款好用的视频倒放软件&#xff0c;帮助你快速进行视频倒放操作&#xff1a; 口袋视频转换器 这是专业的…

会议音频设备行业调研:未来几年年复合增长率CAGR为3.5%

本报告中的会议音频设备主要包括扬声器和会议电话、麦克风&#xff08;桌面麦克风、吸顶麦克风、悬挂麦克风、鹅颈麦克风等&#xff09;、扬声器、视频吧、发送器和接收器、放大器和处理器以及其他配件。 据QYResearch调研团队最新报告“全球会议音频设备市场报告2024-2030”显…

QMT软件怎么申请开通?QMT软件到底是谁在用啊?QMT量化软件K线驱动介绍

QMT提供了三种运行机制&#xff0c;其中K线驱动是最为重要的一种。它主要通过handlebar函数来实现&#xff0c;具体表现为逐K线驱动。 历史K线驱动&#xff1a;在运行开始时&#xff0c;QMT量化软件会遍历所选周期的历史K线。从左向右&#xff0c;每根K线都会触发一次handlebar…

java微信机器人制作教程

Java实现微信小号做机器人 随着人工智能技术的发展&#xff0c;机器人在各行各业扮演着越来越重要的角色。在社交领域&#xff0c;微信机器人也逐渐受到人们的关注。本文将介绍如何使用Java实现一个简单的微信小号做机器人的功能。 常见开发功能&#xff1a; 好友管理&#…

王牌功能 | 法大大“用户角色权限管理”,合同数据分流、管理权限分层都搞定!

合同签了还得管&#xff0c; 按业务、按项目、按岗位级别… 关于法大大“用户角色权限管理”功能的使用&#xff1a;企业操作人员在电子合同平台创建企业后&#xff0c;需添加企业组织架构&#xff0c;主要添加合同操作相关的人员&#xff0c;即可实现合同操作与数据权限的管理…

springboot+vue+mybatis计算机毕业设计老年人健康管理系统+PPT+论文+讲解+售后

近些年来&#xff0c;随着科技的飞速发展&#xff0c;互联网的普及逐渐延伸到各行各业中&#xff0c;给人们生活带来了十分的便利&#xff0c;老年人健康管理系统利用计算机网络实现信息化管理&#xff0c;使整个老年人健康管理的发展和服务水平有显著提升。 本文拟采用Eclips…

基于MATLAB的全景图像拼接系统实现

简要的论文框架和技术思路 摘要 本文深入探讨了基于MATLAB平台的块匹配全景图像拼接系统的设计与实现。通过详细解析SIFT/SURF特征提取、RANSAC变换估计、APAP局部对齐、图割算法拼接缝选择及multi-band blending图像融合等关键技术&#xff0c;构建了高效且高质量的全景图像…

python学习第七节:正则表达式

python学习第七节&#xff1a;正则表达式 正则表达式基本上在所有开发语言中都会使用到&#xff0c;在python中尤为重要。当我们使用python开发爬虫程序将目标网页扒下来之后我们要从网页中解析出我们想要的信息&#xff0c;这个时候就需要正则表达式去进行匹配。 import rere…

由于找不到steam_api.dll,无法继续执行代码的科学修复方法分析,靠谱实用!

当你尝试运行依赖Steam平台的游戏或应用程序时&#xff0c;可能会遭遇一个很让人无奈的错误信息&#xff1a;“由于找不到steam_api.dll,无法继续执行代码”。通常&#xff0c;这是由于系统中缺失或损坏了steam_api.dll文件所致。以下是五种详细的解决方案&#xff0c;助你迅速…

word文档的读入(4)

刚刚我们获取到的是段落和样式块对象&#xff0c;要想读取到真正的文本内容&#xff0c;需要通过.text属性来访问。不同的需求对应不同的操作。比如&#xff1a;如果想读取某一段落中的所有文本&#xff0c;就使用.paragraphs[{段落索引}]获取到段落对象后&#xff0c;再访问它…

tensor 的运算(加法、点乘、矩阵乘法)

文章目录 案例准备加法点乘矩阵乘法注意 案例准备 import torcha torch.tensor([[1,2,3],[4,5,6],[7,8,9]], dtypetorch.float) b torch.tensor([[10,10,10,],[10,10,10],[10,10,10,]], dtypetorch.float)此时a,b为 加法 a 100a b a.add(b)点乘 a * b矩阵乘法 a b a…

牛客周赛 Round 58(上)

会赢吗&#xff1f; 题目描述 登录—专业IT笔试面试备考平台_牛客网 运行代码 #include <iostream> using namespace std;int main() {double w;int h;cin >> w >> h;if (h - w > 0) {cout << "YES" << endl;} else {cout <&…

Java中SringBoot服务连接多个MySQL数据源案例实战

Java中SringBoot服务连接多个MySQL数据源案例实战 1.场景2.取消默认的单数据源配置3.自定义多数据源配置文件4.自定义多数据源配置类1.DB1Config.java2.DB2Config.java 5.启动项目&#xff0c;测试操作多数据源 1.场景 A服务&#xff08;供应商&#xff09;有一套自己的数据库…