保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理!

2024-06-04 4603阅读

1、支付方式选择 2、交互流程 3、1. 对接准备 2.加密解密 + 签名验签 3.沙箱环境 4、内网穿透 5、二维码 6、下单 7、异步通知回调 8、查询支付结果 9、退款 10、通用版SDK

需求:系统A对接支付宝,实现支持用户扫码支付

1、支付方式选择

对接的API文档:

  • https://open.alipay.com/api

    可选的支付方式有:

    • 扫码付:出示付款码或者用户扫码付款

    • APP支付:在APP中唤起支付宝

    • 手机网站支付:在移动端网页中唤起支付宝 App 或支付宝网页

    • 电脑网站支付:在PC端唤起支付宝App或者网页登录支付宝账户

    • 刷脸付:需硬件支持

    • 商家扣款:类似每月会员扣款

    • 预授权支付:冻结对应额度,交易完成后给商家

    • JSAPI支付:小程序

      这里选择扫码付的方式,点击下单后,返回支付二维码,用户扫码支付。

      2、交互流程

      画个下单流程的时序图:

      保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第1张

      图片

      大致流程:

      • 用户下单,系统A组装信息后(订单信息、回调地址、签名),调用支付宝预下单接口,返回二维码链接

      • 系统A将二维码链接转二维码图片

      • 用户扫码,唤醒本地支付宝,完成支付

      • 支付宝返回支付成功信息给用户

      • 支付宝异步通知系统A支付成功的消息(回调地址),如果用户支付成功,支付宝就调用回调地址的API,回调接口中自然是系统A收到用户支付成功消息后的动作

      • 上一步如果通知失败,比如网络异常或支付宝调用异步通知接口时系统A正好挂了 ⇒ 可主动调支付宝提供的查询支付结果接口,或者加定时任务轮询来查询交易状态,如3s-5s

      • 还可以考虑在第一步请求支付宝接口时加上二维码的有效时间,过期就重新发起

        查询支付结果流程:

        保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第2张

        图片

        退款流程同上查询支付结果。PS:注意下单、退款过程中,相关订单的业务数据落库到系统A。

        3、对接准备

        1)加密解密 + 签名验签

        支付信息不能在网络上明文传输,以防被篡改。系统A到支付宝的方向,采用:

        • 支付宝公钥加密 + 系统A的私钥签名(系统A做的事)

        • 支付宝私钥解密 + 系统A的公钥验签(收到信息后,支付宝做的事)

          同理,支付宝返回支付结果时,就是在支付宝中用系统A的公钥加密+支付宝的私钥签名,传输到系统A后,则是先用支付宝的公钥验签,再用系统A的私钥解密支付结果

          保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第3张

          图片

          2)沙箱环境

          调试过程中,可采用支付宝提供的沙箱环境,点击右上角控制台,登录后选择沙箱:

          保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第4张

          图片

          这里有一套可调试的APPID、系统A的公钥、密钥、支付宝的公钥、支付宝的网关地址,以及商家账户和用户账户(用于后续登录沙箱版本支付宝APP完成支付)

          保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第5张

          图片

          保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第6张

          图片

          点击【沙箱工具】侧边栏,下载沙箱版支付宝APP,等于上面的买家账户。

          3)内网穿透

          前面提到,用户支付成功后,支付宝需要回调系统A接口来通知系统A,但我的开发环境在内网,支付宝访问不到,考虑做内网穿透,让支付宝通知到一个中转地址,再由中转地址到我的内网。穿透工具选择cpolar,下载地址 https://dashboard.cpolar.com/get-started,下载后,解压并安装msi包

          保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第7张

          图片

          双击exe文件,执行认证:

          cpolar authtoken xxxx
          

          保姆级教程:SpringBoot 对接支付宝完成扫码支付,完整流程梳理! 第8张

          图片

          创建隧道,建立链接:

          cpolar http 9527
          //返回结果
          Forwarding http://maggie.cpolar.io  -> localhost:9527
          Forwarding https://maggie.cpolar.io  -> localhost:9527
          

          转发成功。此时,给支付宝访问forward的地址即可,比如系统A的异步通知接口:

          localhost:9527/notify
          

          那就是:

          http://maggie.cpolar.io/notify
          

          4、二维码

          二维码是消息的载体。平时玩可直接在草料二维码UI页面,这里需要给系统A的订单服务用代码生成二维码。二维码中的信息自然是支付宝预下单返回的url。

          Java生成二维码可集成zxing库,但这样得自己两层for填充方格子,这里选择hutool工具类库(对zxing的二次封装),引入依赖:

              cn.hutool
              hutool-all
              5.7.22
          
          
              com.google.zxing
              core
              3.4.1
          
          

          调用方式:

          //生成直到url对应的二维码,宽高均300像素,可到路径,也可到Http响应
          QrCodeUtil.generate("https://url/path", 300, 300, "png", httpServletResponse.getOutPutStream());
          

          也可引入QrConfig对象,设置其他属性:

          QrConfig config = new QrConfig(300, 300);
          //纠错级别
          config.setErrorCorrection(ErrorCorrectionLevel.H);
          //二维码颜色
          config.setBackColor(Color.BLUE);
          QrCodeUtil.generate("https://www.baidu.com", config, new File("D:\code.png"));
          

          5、下单

          支付宝提供的SDK 中已经对加签验签逻辑做了封装,使用 SDK 时传入支付宝公钥等内容可直接通过 SDK 自动进行加验签。SDK文档地址:https://opendocs.alipay.com/open/54/103419?pathHash=d6bc7c2b 。支付宝提供了两种SDK:

          • 通用版SDK

          • 简易版SDK

            官网有通用版的API代码示例,这里走简易版的。引入简易版SDK的依赖:

            
                com.alipay.sdk
                alipay-easysdk
                2.2.0
            
            

            在application.yml配置文件中统一写密钥、通知地址等(生产环境不要将私钥信息配置在源码中,例如配置为常量或储存在配置文件中,这样源码一丢,这些保密信息都泄漏了,放安全区域或服务器,运行时读取即可)

            alipay:
              easy:
                protocol: https
                gatewayHost: openapi-sandbox.dl.alipaydev.com
                signType: RSA2
                appId: 9021000133624745
                merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0B
                alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOC
                notifyUrl: http://maggie.cpolar.io/notify
            server:
              port: 9527
            

            @ConfigurationProperties注解统一读到:

            @Configuration
            @Data
            @ConfigurationProperties(prefix = "alipay.easy")
            public class AliPayConfigInfo {
                /**
                 * 请求协议
                 */
                private String protocol;
                /**
                 * 请求网关
                 */
                private String gatewayHost;
                /**
                 * 签名类型
                 */
                private String signType;
                /**
                 * 应用ID(来自支付宝申请)
                 */
                private String appId;
                /**
                 * 应用秘钥
                 */
                private String merchantPrivateKey;
                /**
                 * 支付宝公钥
                 */
                private String alipayPublicKey;
                /**
                 * 支付结果异步通知的地址
                 */
                private String notifyUrl;
                /**
                 * 设施AES秘钥
                 */
                private String encryptKey;
            }
            

            将配置处理成Config类型的Bean,方便后面传入Config对象:

            @Configuration
            public class AliPayConfig {
                @Bean
                public Config config(AliPayConfigInfo configInfo){
                    Config config = new Config();
                    config.protocol = configInfo.getProtocol();
                    config.gatewayHost = configInfo.getGatewayHost();
                    config.signType = configInfo.getSignType();
                    config.appId = configInfo.getAppId();
                    config.merchantPrivateKey = configInfo.getMerchantPrivateKey();
                    config.alipayPublicKey = configInfo.getAlipayPublicKey();
                    config.notifyUrl = configInfo.getNotifyUrl();
                    config.encryptKey = "";
                    return config;
                }
            }
            

            写下单接口,响应一个二维码给前端,这里业务数据、订单编号直接写***,只做示意:

            @RestController
            @Slf4j
            public class PayController {
                @Resource
                private Config config;
                /**
                 * 收银台点击结账
                 * 发起下单请求
                 */
                @GetMapping("/pay")
                public void pay(HttpServletRequest request, HttpServletResponse response) throws Exception {
                    Factory.setOptions(config);
                    //调用支付宝的接口
                    AlipayTradePrecreateResponse payResponse = Factory.Payment.FaceToFace().preCreate("订单主题:Mac笔记本", "LS123qwe123", "19999");
                    //参照官方文档响应示例,解析返回结果
                    String httpBodyStr = payResponse.getHttpBody();
                    JSONObject jsonObject = JSONObject.parseObject(httpBodyStr);
                    String qrUrl = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString();
                    QrCodeUtil.generate(qrUrl, 300, 300, "png", response.getOutputStream());
                }
            }
            

            6、异步通知回调

            异步回调参考文档:https://opendocs.alipay.com/open/194/103296?pathHash=e43f422e&ref=api,实现先全放Controller层了:

            @RestController
            @Slf4j
            public class PayController {
                @Resource
                private Config config;
             
                /**
                 * 给支付宝的回调接口
                 */
                @PostMapping("/notify")
                public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
                    Map params = new HashMap();
                    //获取支付宝POST过来反馈信息,将异步通知中收到的待验证所有参数都存放到map中
                    Map parameterMap = request.getParameterMap();
                    for (String name : parameterMap.keySet()) {
                        String[] values = parameterMap.get(name);
                        String valueStr = "";
                        for (int i = 0; i 

    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]