SpringBoot集成支付宝沙箱支付

引言

由于在毕业设计中有缴费功能,所以想到集成支付宝的沙箱支付,但以前没有涉及过相关功能。

经过百度查阅相关资料,整理相关集成资料如下

流程概述

本流程为集成支付宝沙箱电脑网站支付流程

image-20230217135026494

具体流程如下:

  1. 商家系统调用 alipay.trade.page.pay(统一收单下单并支付页面接口)向支付宝发起支付请求,支付宝对商家请求参数进行校验,而后重新定向至用户登录页面。
  2. 用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
  3. 交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
  4. 若由于网络等原因,导致商家系统没有收到异步通知,商家可自行调用 alipay.trade.query(统一收单线下交易查询接口)查询交易以及支付信息(商家也可以直接调用该查询接口,不需要依赖异步通知)。

注意

  • 由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
  • 商家系统接收到异步通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。详细验签规则可查看 异步通知验签
  • 接收到异步通知并验签通过后,请务必核对通知中的 app_id、out_trade_no、total_amount 等参数值是否与请求中的一致,并根据 trade_status 进行后续业务处理。
  • 在支付宝端,partnerId 与 out_trade_no 唯一对应一笔单据,商家端保证不同次支付 out_trade_no 不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。

前期准备

支付宝开发平台中心

支付宝开放中心:https://openhome.alipay.com/

前期准备就是在支付宝开放中心创建沙箱应用

  1. 访问支付宝开放中心,登录账号后点击控制台

    image-20230217141308803

  2. 进入控制台后,将页面往下滑,会看见沙箱

    image-20230217141629152

  3. 点进沙箱后就是如下界面,可以看到appid

    image-20230217141727133

  4. 然后我们需要设置接口加签方式,这里建议选择自定义密钥中的公钥模式

    点击设置并查看时,在界面中会出现相关文档链接

    https://opendocs.alipay.com/common/02kipl

    image-20230217161929751

  5. 设置应用网关地址

    用于接收支付宝沙箱异步通知消息(例如 From蚂蚁消息等),需要传入http(s)公网可访问的网页地址。选填,若不设置,则无法接收相应的异步通知消息。

    如果你要接收用户支付完成后的信息,那么这个url是必须要设置的

    **注意:**设置的url地址一定要是外网可以访问的,如果是在本机进行测试,那么本机相关端口需要进行内网穿透处理,否则支付宝无法访问该url

    就比如我本地的8888端口经过内网穿透处理后的异步通知url就如下所示:

    image-20230217164018980

  6. 设置授权回调地址

    外部通过访问该网关来将请求分发给对应的沙箱内部业务系统

    授权回调地址是用于第三方登录的,如果你不需要使用到支付宝用户登录功能,则可以不用设置

  7. 再往下划可以看见开发相关产品的文档

    image-20230217162807413

  8. 然后左侧点击沙箱账号,可以看见支付宝沙箱提供的商家账号和卖家账号

    image-20230217162918117

  9. 然后我们在沙箱工具中去下载支付宝客户端沙箱版(仅安卓)

    然后在支付宝沙箱版中去登录买家账号,到时候测试网站支付即可

内网穿透

https://natapp.cn/

内网穿透我使用的是NATAPP,主要功能就是让本地端口通过一个代理url可以让外网进行访问

**在这里的作用就是:**使用内网穿透工具,代理本机后端端口,然后支付宝的异步通知才能发送到对应接口上,否则支付宝访问不到我们本地的接口

image-20230219004957114

如下是我电脑的8888端口进行内网穿透,那么我使用http://4get9t.natappfree.cc来替代这个端口的访问即可

image-20230217164340086

代码示例

本代码示例为电脑网站支付示例

有疑问的位置可以查阅官方文档:https://opendocs.alipay.com/open/repo-0038oa

集成支付

  1. 引入依赖

    <dependency>
        <groupId>com.alipay.sdk</groupId>
        <artifactId>alipay-sdk-java</artifactId>
        <version>4.34.0.ALL</version>
    </dependency>
    
  2. 编写application.yml

    • gatewayUrl:支付宝网关,沙箱环境网关与生产环境网关不一致,下面的示例是沙箱环境的
    • appId:在支付宝开放平台创建应用后,每个应用都有自己的appid
    • appPrivateKey:应用私钥,这个需要填自己的
    • format:报文格式,这个不用改
    • charset:字符串编码格式,这个不用改
    • signType:签名方式,这个一般不用改
    • alipayPublicKey:支付宝公钥,这个需要填自己的
    • returnUrl:支付完成后同步跳转的路径,这个需要填自己的,我填的是我前端页面路径
    • notifyUrl:异步通知url,用来处理支付完成后业务,注意,这个路径在本地测试的话需要进行内网穿透,否则支付宝无法访问该路径
    alipay:
      # 支付宝网关
      gatewayUrl: https://openapi.alipaydev.com/gateway.do
      # appid
      appId: 
      # 应用私钥
      appPrivateKey: 
      # 报文格式
      format: JSON
      # 字符串编码格式
      charset: utf-8
      # 签名方式
      signType: RSA2
      # 支付宝公钥
      alipayPublicKey: 
      # 支付完成后同步跳转的路径(一般写支付完成后想要跳转的路径)
      returnUrl: http://localhost:9999/#/personal/illegalInfo
      # 异步通知url
      notifyUrl: http://4get9t.natappfree.cc/aliPay/notify
    
  3. 在config包下新建AliPayProperties

    package com.lzj.config;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    
    /**
     * <p>
     *
     * </p>
     *
     * @author:雷子杰
     * @date:2023/2/15
     */
    @Data
    @Component
    @ConfigurationProperties(prefix = "alipay")
    public class AliPayProperties {
          
          
        /**
         * 支付宝网关
         */
        private String gatewayUrl;
        /**
         * 支付宝应用的appid
         */
        private String appId;
        /**
         * 应用私钥
         */
        private String appPrivateKey;
        /**
         * 报文格式
         */
        private String format;
        /**
         * 字符串编码格式
         */
        private String charset;
        /**
         * 签名方式
         */
        private String signType;
        /**
         * 支付宝公钥
         */
        private String alipayPublicKey;
        /**
         * 支付完成后同步跳转的路径(一般写支付完成后想要跳转的路径)
         */
        private String returnUrl;
        /**
         * 异步通知url
         */
        private String notifyUrl;
    
    
    }
    
  4. 在entity包下新建AlipayOrder

    package com.lzj.entity;
    
    import io.swagger.annotations.ApiModel;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.experimental.Accessors;
    
    /**
     * <p>
     *
     * </p>
     *
     * @author:雷子杰
     * @date:2023/2/15
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Accessors(chain = true)
    @ApiModel("支付订单类")
    public class AlipayOrder {
          
          
    
        /**
         * 用户订单号,必填
         */
        private String out_trade_no;
    
        /**
         * 订单名称,必填
         */
        private String subject;
    
        /**
         * 订单总金额,必填
         */
        private String total_amount;
    
        /**
         * 销售产品码,必填,注:目前电脑支付场景下仅支持FAST_INSTANT_TRADE_PAY,所以可以写死
         */
        private final String product_code = "FAST_INSTANT_TRADE_PAY";
    
    }
    
  5. 在service包下新建一个AliPayService

    package com.lzj.service;
    
    import com.alipay.api.AlipayApiException;
    import com.alipay.api.AlipayResponse;
    import com.alipay.api.response.AlipayTradePagePayResponse;
    import com.lzj.entity.AlipayOrder;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * <p>
     *
     * </p>
     *
     * @author:雷子杰
     * @date:2023/2/15
     */
    public interface AliPayService {
          
          
    
        AlipayResponse pay(AlipayOrder order) throws AlipayApiException;
    
        /**
         * 异步通知签名验证
         * @param request
         * @return
         */
        boolean notifySignVerified(HttpServletRequest request);
    }
    
    
  6. 新建AliPayServiceImpl

    package com.lzj.service.impl;
    
    import com.alibaba.fastjson.JSON;
    import com.alipay.api.*;
    import com.alipay.api.internal.util.AlipaySignature;
    import com.alipay.api.request.AlipayOpenPublicTemplateMessageIndustryModifyRequest;
    import com.alipay.api.request.AlipayTradePagePayRequest;
    import com.alipay.api.response.AlipayOpenPublicTemplateMessageIndustryModifyResponse;
    import com.alipay.api.response.AlipayTradePagePayResponse;
    import com.lzj.config.AliPayProperties;
    import com.lzj.entity.AlipayOrder;
    import com.lzj.service.AliPayService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    
    /**
     * <p>
     *
     * </p>
     *
     * @author:雷子杰
     * @date:2023/2/15
     */
    @Service("aliPayService")
    @Transactional
    @Slf4j
    public class AliPayServiceImpl implements AliPayService {
          
          
    
        @Autowired
        private AliPayProperties aliPayProperties;
    
    
        @Override
        public AlipayResponse pay(AlipayOrder order) throws AlipayApiException {
          
          
            //初始化AlipayConfig
            AlipayConfig alipayConfig = new AlipayConfig();
            alipayConfig.setServerUrl(aliPayProperties.getGatewayUrl());
            alipayConfig.setAppId(aliPayProperties.getAppId());
            alipayConfig.setSignType(aliPayProperties.getSignType());
            alipayConfig.setAlipayPublicKey(aliPayProperties.getAlipayPublicKey());
            alipayConfig.setPrivateKey(aliPayProperties.getAppPrivateKey());
            alipayConfig.setFormat(aliPayProperties.getFormat());
            alipayConfig.setCharset(aliPayProperties.getCharset());
    
            //实例化客户端
            AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
    
            //设置请求参数
            //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称
            AlipayTradePagePayRequest  request = new AlipayTradePagePayRequest ();
            //设置同步返回url
            request.setReturnUrl(aliPayProperties.getReturnUrl());
            //设置异步通知url
            request.setNotifyUrl(aliPayProperties.getNotifyUrl());
            log.debug("JSON.toJSONString(order):" + JSON.toJSONString(order));
            //转换为json字符串后传入
            request.setBizContent(JSON.toJSONString(order));
    
            AlipayTradePagePayResponse  response = alipayClient.pageExecute(request);
    
            //调用成功,则处理业务逻辑
            return response;
        }
    
        @Override
        public boolean notifySignVerified(HttpServletRequest request) {
          
          
    
            log.debug("进入了异步通知");
            Map<String, String> params = new HashMap<String, String>();
            Map<String, String[]> requestParams = request.getParameterMap();
    
            for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
          
          
                String name = (String) iter.next();
                String[] values = (String[]) requestParams.get(name);
                String valueStr = "";
                for (int i = 0; i < values.length; i++) {
          
          
                    valueStr = (i == values.length - 1) ? valueStr + values[i]
                            : valueStr + values[i] + ",";
                }
                params.put(name, valueStr);
            }
    
            //调用SDK验证签名
            System.out.println(params.toString());
            System.out.println(aliPayProperties.toString());
            boolean signVerified = false;
            try {
          
          
                signVerified = AlipaySignature.rsaCheckV1(params, aliPayProperties.getAlipayPublicKey(), aliPayProperties.getCharset(), aliPayProperties.getSignType());
            } catch (AlipayApiException e) {
          
          
                e.printStackTrace();
            }
            System.out.println("SDK验证签名结果1:" + signVerified);
    
            return signVerified;
        }
    }
    
  7. 新建AliPayController

    /notify接口是用来接收支付宝支付成功后返回的异步通知

    /**
     * <p>
     *
     * </p>
     *
     * @author:雷子杰
     * @date:2023/2/16
     */
    @RestController
    @RequestMapping("aliPay")
    @Slf4j
    @Api(tags = "支付宝支付模块")
    public class AliPayController {
          
          
        @Autowired
        private AliPayService aliPayService;
        @Autowired
        private OrderService orderService;
        @Autowired
        private IllegalInfoService illegalInfoService;
        @Autowired
        private RedisClient redisClient;
    
    
    
        @GetMapping("/return")
        public JsonResult<Void> getReturnMsg() {
          
          
    
            return JsonResult.ok();
        }
    
        @PostMapping("/notify")
        public JsonResult<Void> getNotifyMsg(HttpServletRequest request) throws UnsupportedEncodingException {
          
          
    
            boolean signVerified = aliPayService.notifySignVerified(request);
    
            if (signVerified) {
          
          
                // 商户订单号
                String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
    
                // 支付宝交易号
                String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
    
                // 交易状态
                String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
    
                if (trade_status.equals("TRADE_FINISHED")) {
          
          
                    // 判断该笔订单是否在商户网站中已经做过处理
                    // 如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
                    // 如果有做过处理,不执行商户的业务程序
    
                    log.debug("成功:TRADE_FINISHED");
                    // 注意:
                    // 退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
                    return JsonResult.ok();
                } else if (trade_status.equals("TRADE_SUCCESS")) {
          
          
                    // 判断该笔订单是否在商户网站中已经做过处理
                    // 如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
                    // 如果有做过处理,不执行商户的业务程序
                    log.debug("成功:TRADE_SUCCESS");
    
                    // 注意:
                    // 付款完成后,支付宝系统发送该交易状态通知
                    return JsonResult.ok();
                } else {
          
          //验证失败
    
                    // 调试用,写文本函数记录程序运行情况是否正常
                    return JsonResult.fail(StatusCodeEnum.ALI_PAY_ERROR.getCode(), StatusCodeEnum.ALI_PAY_ERROR.getDesc());
                }
    
            }
    
            return JsonResult.ok();
        }
    }
    

业务调用

  1. 前端页面请求相关接口

    image-20230217195908324

  2. 接口中调用AliPayService的pay()方法返回alipayResponse,根据alipayResponse的isSuccess()方法来判断是否请求成功,然后来处理业务逻辑,然后返回alipayResponse的getBody()方法

    示例:

    image-20230217194639564

  3. 接口返回alipayResponse的getBody()方法返回的参数(是一个表单字符串)

  4. 前端页面接收到返回的字符串后对其进行操作,新开窗口跳转

    **注意:**新开窗口跳转后原页面会出现空白是正常情况

    payIllegalInfoMoney(row){
          
          
      payIllegalInfoMoney({
          
          
        illegalInfoId: row.illegalInfoId,
        illegalActCode: row.illegalActCode,
        amount: row.amount,
        userId: this.$store.getters.userInfo.userId
      }).then(res =>{
          
          
        //下面的操作不可删减,删减了后可能因为缓存导致第二次无法跳转窗口的问题
        const divForm = document.getElementsByTagName("div");
        if (divForm.length) {
          
          
          document.body.removeChild(divForm[0]);
        }
        const div = document.createElement("div");
        div.innerHTML = res.data; // data就是接口返回的form 表单字符串
        document.body.appendChild(div);
        document.forms[0].setAttribute("target", "_blank"); // 新开窗口跳转
        document.forms[0].submit();
    
      })
    }
    
  5. 跳转后的界面如下

    **注意:**周日中午12点-周一中午12点支付宝沙箱版可能进行维护,这个时候访问的话页面可能会报502异常,如果出现这个问题了我们等第二天再进行测试就可以了

    image-20230217200410870

  6. 然后使用沙箱版支付宝扫码或者直接网站登录付款就可以了

  7. 扫码支付完成后,如果你在后端写明了returnUrl的,那么该页面会跳转至该链接,同时扫码支付完成后支付宝会向你设置的异步通知url发送相关请求

  8. 在接收支付宝异步通知的接口中,我们可以进行支付完成后的一个业务处理,示例如下

    image-20230217200711014

总结

只是做一个简单的电脑网站支付宝沙箱支付并不困难,基本上就是调一调api就可以实现。

希望以后在工作中能够遇到相关知识让我在实践中学习

猜你喜欢

转载自blog.csdn.net/qq_49137582/article/details/129134691