• springboot整合支付宝沙箱支付


    springboot整合支付宝沙箱支付

    1.简介

    支付宝开发平台地址:https://open.alipay.com/develop/sandbox/app

    对于学生来说,目前网上确实没有比较统一而且质量好的支付教程。因为支付对个人开发者尤其是学生来说不太友好。因此,自己折腾两天,算是整理了一篇关于支付宝沙箱支付的文章。况且个人是不能申请支付(wx和alipay)都一样。幸亏有支付宝沙箱这个环境。其实跟正式环境差不多,就换下配置即可。

    整体流程

    在这里插入图片描述

    2.配置说明

    要记住这几个重要的配置

    • appId 这个是appId
    • privateKey 商户私钥
    • publicKey 支付宝公钥, 即对应APPID下的支付宝公钥
    • notifyUrl 支付成功后异步回调地址(注意是必须是公网地址)
    • returnUrl #支付后回调地址
    • signType 签名类型 一般写 RSA2
    • charset utf-8
    • format json
    • #网关地址 在支付宝开发平台复制拷贝下来
    • gatewayUrl: https://openapi.alipaydev.com/gateway.do
    • logPath: F:\ 日志路径
    
    
    • 1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8CwUNj9q-1665450970482)(springboot%E6%95%B4%E5%90%88%E6%94%AF%E4%BB%98%E5%AE%9D%E6%B2%99%E7%AE%B1%E6%94%AF%E4%BB%98.assets/image-20220919171644335.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0OD2JA1-1665450970482)(springboot%E6%95%B4%E5%90%88%E6%94%AF%E4%BB%98%E5%AE%9D%E6%B2%99%E7%AE%B1%E6%94%AF%E4%BB%98.assets/image-20220919172243239.png)]

    ps:如果是正式环境的支付宝支付的时候,公钥私钥处理方案:

    要先下载秘钥工具

    在这里插入图片描述

    点击生成秘钥
    在这里插入图片描述

    生成后打开文件位置
    在这里插入图片描述
    然后把公钥上传到支付宝开发平台

    在这里插入图片描述

    限制与要求
    • 需要使用支付宝开放平台主账号进行配置。
    • 一个应用(APPID)只能配置一种接口加签方式(密钥或证书)

    3.springboot整合支付宝沙箱支付代码

    需要导入依赖

    
        com.alipay.sdk
        alipay-sdk-java
        4.33.39.ALL
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.1 配置类

    package com.hjt.config;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.cloud.context.config.annotation.RefreshScope;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.PropertySource;
    
    
    
    @RefreshScope
    @Configuration
    @ConfigurationProperties(prefix = "alipay")
    @Data
    /***
     * @author: hjt
     * @Date: 2020/11/13/19:19
     * @Description: 支付宝配置类(读取配置文件)
     */
    public class MyAliPayConfig {
        /**
         * APPID
         */
        private String appId;
    
        /**
         * 商户私钥, 即PKCS8格式RSA2私钥
         */
        private String privateKey;
    
        /**
         * 支付宝公钥
         */
        private String publicKey;
    
        /**
         * 服务器异步通知页面路径,需http://格式的完整路径
         * 踩坑:不能加?type=abc这类自定义参数
         */
        private String notifyUrl;
    
        /**
         * 页面跳转同步通知页面路径,需http://格式的完整路径
         * 踩坑:不能加?type=abc这类自定义参数
         */
        private String returnUrl;
    
        /**
         * 签名方式
         */
        private String signType;
    
        /**
         * 字符编码格式
         */
        private String charset;
    
        /***
         * 参数编码格式  json
         */
        private String format;
    
        /**
         * 支付宝网关
         */
        private String gatewayUrl;
    
        /**
         * 日志打印地址
         */
        private String logPath;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    3.2 生成订单信息(以java为例,官网例子)

    public void   doPost (HttpServletRequest httpRequest,
                          HttpServletResponse httpResponse)   throws  ServletException, IOException  {
        AlipayClient alipayClient =  new  DefaultAlipayClient( "https://openapi.alipay.com/gateway.do" , APP_ID, APP_PRIVATE_KEY, FORMAT, CHARSET, ALIPAY_PUBLIC_KEY, SIGN_TYPE);  //获得初始化的AlipayClient 
        AlipayTradePagePayRequest alipayRequest =  new  AlipayTradePagePayRequest(); //创建API对应的request 
        alipayRequest.setReturnUrl( "http://domain.com/CallBack/return_url.jsp" );
        alipayRequest.setNotifyUrl( "http://domain.com/CallBack/notify_url.jsp" ); //在公共参数中设置回跳和通知地址 
        alipayRequest.setBizContent( "{"  +
             "    \"out_trade_no\":\"20150320010101001\","  +
             "    \"product_code\":\"FAST_INSTANT_TRADE_PAY\","  +
             "    \"total_amount\":88.88,"  +
             "    \"subject\":\"Iphone6 16G\","  + 
             "    \"body\":\"Iphone6 16G\","  +
             "    \"passback_params\":\"merchantBizType%3d3C%26merchantBizNo%3d2016010101111\","  +
             "    \"extend_params\":{"  +
             "    \"sys_service_provider_id\":\"2088511833207846\""  +
             "    }" +
             "  }" ); //填充业务参数 
        String form= "" ;
         try  {
            form = alipayClient.pageExecute(alipayRequest).getBody();  //调用SDK生成表单 
        }  catch  (AlipayApiException e) {
            e.printStackTrace();
        }
        httpResponse.setContentType( "text/html;charset="  + CHARSET);
        httpResponse.getWriter().write(form); //直接将完整的表单html输出到页面 
        httpResponse.getWriter().flush();
        httpResponse.getWriter().close();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    以下是我自己业务代码的例子

    @Override
    public void pay(PayInfoDto payInfoDTO, HttpServletResponse httpResponse) throws IOException {
        /*查询订单id是否存在*/
        Long orderId = payInfoDTO.getOrderId();
        /*商品名称*/
        String subject = "";
    
        /*判断订单是否存在*/
        R<Order> orderInfo = remoteOrderService.getOrderInfo(orderId);
        if (orderInfo.getCode() != 200) {
            throw new BaseException(PayException.ORDER_ERROR_PAY_ORDER);
        }
        Order order = orderInfo.getData();
        //订单总金额
        BigDecimal total = order.getTotal();
        String proId = order.getProId();
        /*商品名称以,分割*/
        String[] split = proId.split(",");
        for (int i = 0; i < split.length; i++) {
            R<Product> productInfo = remoteOrderService.getProductInfo(Long.parseLong(split[i]));
            if (productInfo.getCode() != 200) {
                throw new BaseException(PayException.ORDER_ERROR_PAY_PRODUCT);
            }
            Product product = productInfo.getData();
            subject = subject + product.getProTitle() + ",";
        }
        //进行支付宝支付
        AlipayOrder alipayOrder = new AlipayOrder();
        //商户订单号
        alipayOrder.setOut_trade_no(String.valueOf(orderId));
        //订单名称
        alipayOrder.setSubject(subject);
        alipayOrder.setDescription(subject);
        //订单总金额
        alipayOrder.setTotal_amount(total.toString());
        /*进行支付*/
        String payBody = alipayUtil.payByAlipay(alipayOrder);
        log.info("-----------支付信息实体---------{}",payBody);
        //并把消息推送到mq查询是否支付成功
        if(StringUtils.isBlank(payBody)){
            throw new BaseException(PayException.ORDER_ERROR_PAY_BODY);
        }
        /*把支付信息写到html网页中*/
        httpResponse.setContentType("text/html;charset=" + CHARSET);
        // 直接将完整的表单html输出到页面
        httpResponse.getWriter().write(payBody);
        httpResponse.getWriter().flush();
        httpResponse.getWriter().close();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    3.3支付成功后的异步通知

    @Override
    public String payNotifyUrl(HttpServletRequest request) throws AlipayApiException {
        log.info("-----------开始进行支付后的异步通知回调-------");
        /*接收参数*/
        Map<String, String> params = this.exchangeParams(request);
        String payContent = JSONUtil.toJsonStr(params);
        log.info("---------接收的参数--- -----{}",params);
        /*验证签名(支付宝公钥) 调用SDK验证签名*/
        boolean  signVerified = AlipaySignature.rsaCheckV1(params, aliPayConfig.getPublicKey(), aliPayConfig.getCharset(), aliPayConfig.getSignType());
        if (signVerified){
            /*收到支付宝异步通知,返回success,支付宝不再通知 否则会通知你三天三夜*/
            log.info("-----验签成功-----");
            /*应该马上返回"success",另起线程执行自己的业务逻辑*/
            ExecutorService executor = ExecutorBuilder.create()
                    .setCorePoolSize(5)
                    .setMaxPoolSize(10)
                    .setWorkQueue(new LinkedBlockingQueue<>(100))
                    .build();
            executor.execute(new Runnable(){
                @Override
                public void run() {
                    //TODO 幂等性问题后续也要考虑
                    /*支付状态*/
                    String trade_status = params.get("trade_status");
                    /*订单id*/
                    String out_trade_no = params.get("out_trade_no");
                    /*支付成功*/
                    if (PayConstant.TRADE_FINISHED.equals(trade_status) || PayConstant.TRADE_SUCCESS.equals(trade_status)) {
    
                        Long orderId = Long.valueOf(out_trade_no);
                        /*把对于的订单id改为已支付状态*/
                        R<Order> order = remoteOrderService.updateOrderById(orderId);
                        if(order.getCode()!=PayConstant.CODE){
                            throw new BaseException(PayException.ORDER_ERROR_PAY_ORDER);
                        }
                        /*存对于的支付信息*/
                        PayInfo payInfo = null;
                        payInfo = PayInfo.builder()
                                .orderIds(out_trade_no)
                                .callbackContent(payContent)
                                .callbackTime(LocalDateTime.now())
                                .tradeStatus(trade_status)
                                .tradeNo(params.get("trade_no"))
                                .buyerId(params.get("buyer_id"))
                                .totalAmount(params.get("total_amount"))
                                .version(params.get("version"))
                                .sellerId(params.get("seller_id"))
                                .receiptAmount(params.get("receipt_amount"))
                                .gmtCreate(params.get("gmt_create"))
                                .gmtPayment(params.get("gmt_payment"))
                                .fundBillList(params.get("fund_bill_list"))
                                .build();
                        payInfoMapper.insert(payInfo);
                        log.info("-----支付信息插入成功-------");
                    }
                    /* 支付失败 */
                    else{
                        log.error("-------支付失败, 订单id:------{}",out_trade_no);
                        throw new BaseException(PayException.ORDER_ERROR_PAY_BODY);
                    }
    
                }
            });
            return "success";
            // TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商家自身业务处理,校验失败返回failure
        } else {
            log.info("-----验签失败-----");
            return "failure";
            // TODO 验签失败则记录异常日志,并在response中返回failure.
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    注意!!!异步通知的地址必须是公网地址,这里我采用的是frp内网穿透到本地的地址

    需要注意的是,异步通知必须要严格进行验签。

    运行效果图:

    先生成订单

    ​	[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yirrvFNv-1665450970482)(springboot%E6%95%B4%E5%90%88%E6%94%AF%E4%BB%98%E5%AE%9D%E6%B2%99%E7%AE%B1%E6%94%AF%E4%BB%98.assets/image-20220920093254760.png)]

    然后再浏览器直接调用接口

    http://localhost:4401/pay/api/v1/pay-info/pay?Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.YWRtaW4.VaJOloHfQLjacnm6-__pSaeNZ1JbLAdlgeJT3JEptos&orderId=769532559209283584

    会直接跳转到支付支付界面

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eMXbDidW-1665450970483)(springboot%E6%95%B4%E5%90%88%E6%94%AF%E4%BB%98%E5%AE%9D%E6%B2%99%E7%AE%B1%E6%94%AF%E4%BB%98.assets/image-20220920093241376.png)]

    账号密码都是可以在你沙箱账号看得到

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YutI7SJJ-1665450970483)(springboot%E6%95%B4%E5%90%88%E6%94%AF%E4%BB%98%E5%AE%9D%E6%B2%99%E7%AE%B1%E6%94%AF%E4%BB%98.assets/image-20220920093431990.png)]

    支付成功后可见已经回调到我们异步通知自定义的接口了

    即我们在这配置的路径

    notifyUrl 支付成功后异步回调地址(注意是必须是公网地址)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rmjvXjJF-1665450970483)(springboot%E6%95%B4%E5%90%88%E6%94%AF%E4%BB%98%E5%AE%9D%E6%B2%99%E7%AE%B1%E6%94%AF%E4%BB%98.assets/image-20220920093533071.png)]

    3.4退款操作

    参数类型是否必填最大长度描述示例值
    trade_noString特殊可选64支付宝交易号。 和商户订单号不能同时为空2021081722001419121412730660
    out_trade_noString特殊可选64商户订单号。 订单支付时传入的商户订单号,和支付宝交易号不能同时为空。 trade_no,out_trade_no如果同时存在优先取trade_no2014112611001004680073956707
    out_request_noString必选64退款请求号。 请求退款接口时,传入的退款请求号,如果在退款请求时未传入,则该值为创建交易时的商户订单号。HZ01RF001
    query_optionsString[]可选1024查询选项,商户通过上送该参数来定制同步需要额外返回的信息字段,数组格式。枚举支持: refund_detail_item_list:本次退款使用的资金渠道; gmt_refund_pay:退款执行成功的时间; deposit_back_info:银行卡冲退信息;refund_detail_item_list

    商户可使用该接口查询自已通过alipay.trade.refund提交的退款请求是否执行成功。

    注意:1. 该接口的返回码10000,仅代表本次查询操作成功,不代表退款成功,当接口返回的refund_status值为REFUND_SUCCESS时表示退款成功,否则表示退款没有执行成功。
    \2. 如果退款未成功,商户可以调用退款接口重试,重试时请务必保证退款请求号和退款金额一致,防止重复退款。
    \3. 发起退款查询接口的时间不能离退款请求时间太短,建议之间间隔10秒以上。

    个人搭建项目代码地址:
    https://github.com/hongjiatao/spring-boot-anyDemo

    欢迎收藏点赞三连。谢谢!有问题可以留言博主会24小时内无偿回复。

  • 相关阅读:
    RabbitMQ的Windows版安装教程
    C++模板初阶
    python接口自动化封装导出excel方法和读写excel数据
    C++ 内存管理 - primitives - 侯捷
    【C++编程语言】之面向对象的三大特性之一封装
    java高级用法之:无所不能的java,本地方法调用实况
    onehot-词嵌入-图嵌入
    软件测试:功能测试常用的测试用例大全
    Git学习笔记
    Effective C++改善程序与设计的55个具体做法 3. 资源管理
  • 原文地址:https://blog.csdn.net/weixin_40483369/article/details/127256659