当前位置:   article > 正文

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

springboot对接支付宝支付

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、交互流程

画个下单流程的时序图:

图片

图片

大致流程:

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

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

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

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

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

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

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

查询支付结果流程:

图片

图片

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

3、对接准备

1)加密解密 + 签名验签

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

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

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

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

图片

图片

2)沙箱环境

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

图片

图片

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

图片

图片

图片

图片

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

3)内网穿透

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

图片

图片

双击exe文件,执行认证:

cpolar authtoken xxxx

图片

图片

创建隧道,建立链接:

  1. cpolar http 9527
  2. //返回结果
  3. Forwarding http://maggie.cpolar.io  -> localhost:9527
  4. 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的二次封装),引入依赖:

  1. <dependency>
  2.     <groupId>cn.hutool</groupId>
  3.     <artifactId>hutool-all</artifactId>
  4.     <version>5.7.22</version>
  5. </dependency>
  6. <dependency>
  7.     <groupId>com.google.zxing</groupId>
  8.     <artifactId>core</artifactId>
  9.     <version>3.4.1</version>
  10. </dependency>

调用方式:

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

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

  1. QrConfig config = new QrConfig(300300);
  2. //纠错级别
  3. config.setErrorCorrection(ErrorCorrectionLevel.H);
  4. //二维码颜色
  5. config.setBackColor(Color.BLUE);
  6. 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的依赖:

  1. <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
  2. <dependency>
  3.     <groupId>com.alipay.sdk</groupId>
  4.     <artifactId>alipay-easysdk</artifactId>
  5.     <version>2.2.0</version>
  6. </dependency>

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

  1. alipay:
  2.   easy:
  3.     protocol: https
  4.     gatewayHost: openapi-sandbox.dl.alipaydev.com
  5.     signType: RSA2
  6.     appId: 9021000133624745
  7.     merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0B
  8.     alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOC
  9.     notifyUrl: http://maggie.cpolar.io/notify
  10. server:
  11.   port: 9527

@ConfigurationProperties注解统一读到:

  1. @Configuration
  2. @Data
  3. @ConfigurationProperties(prefix = "alipay.easy")
  4. public class AliPayConfigInfo {
  5.     /**
  6.      * 请求协议
  7.      */
  8.     private String protocol;
  9.     /**
  10.      * 请求网关
  11.      */
  12.     private String gatewayHost;
  13.     /**
  14.      * 签名类型
  15.      */
  16.     private String signType;
  17.     /**
  18.      * 应用ID(来自支付宝申请)
  19.      */
  20.     private String appId;
  21.     /**
  22.      * 应用秘钥
  23.      */
  24.     private String merchantPrivateKey;
  25.     /**
  26.      * 支付宝公钥
  27.      */
  28.     private String alipayPublicKey;
  29.     /**
  30.      * 支付结果异步通知的地址
  31.      */
  32.     private String notifyUrl;
  33.     /**
  34.      * 设施AES秘钥
  35.      */
  36.     private String encryptKey;
  37. }

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

  1. @Configuration
  2. public class AliPayConfig {
  3.     @Bean
  4.     public Config config(AliPayConfigInfo configInfo){
  5.         Config config = new Config();
  6.         config.protocol = configInfo.getProtocol();
  7.         config.gatewayHost = configInfo.getGatewayHost();
  8.         config.signType = configInfo.getSignType();
  9.         config.appId = configInfo.getAppId();
  10.         config.merchantPrivateKey = configInfo.getMerchantPrivateKey();
  11.         config.alipayPublicKey = configInfo.getAlipayPublicKey();
  12.         config.notifyUrl = configInfo.getNotifyUrl();
  13.         config.encryptKey = "";
  14.         return config;
  15.     }
  16. }

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

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

6、异步通知回调

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

  1. @RestController
  2. @Slf4j
  3. public class PayController {
  4.     @Resource
  5.     private Config config;
  6.  
  7.     /**
  8.      * 给支付宝的回调接口
  9.      */
  10.     @PostMapping("/notify")
  11.     public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
  12.         Map<StringString> params = new HashMap<>();
  13.         //获取支付宝POST过来反馈信息,将异步通知中收到的待验证所有参数都存放到map中
  14.         Map<StringString[]> parameterMap = request.getParameterMap();
  15.         for (String name : parameterMap.keySet()) {
  16.             String[] values = parameterMap.get(name);
  17.             String valueStr = "";
  18.             for (int i = 0; i < values.length; i++) {
  19.                 valueStr = (i == values.length - 1) ? valueStr + values[i]
  20.                         : valueStr + values[i] + ",";
  21.             }
  22.             //乱码解决
  23.             valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
  24.             params.put(name, valueStr);
  25.         }
  26.         //验签
  27.         Boolean signResult = Factory.Payment.Common().verifyNotify(params);
  28.         if (signResult) {
  29.             log.info("收到支付宝发送的支付结果通知");
  30.             String out_trade_no = request.getParameter("out_trade_no");
  31.             log.info("交易流水号:{}", out_trade_no);
  32.             //交易状态
  33.             String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
  34.             //交易成功
  35.             switch (trade_status) {
  36.                 case "TRADE_SUCCESS":
  37.                     //支付成功的业务逻辑,比如落库,开vip权限等
  38.                     log.info("订单:{} 交易成功", out_trade_no);
  39.                     break;
  40.                 case "TRADE_FINISHED":
  41.                     log.info("交易结束,不可退款");
  42.                     //其余业务逻辑
  43.                     break;
  44.                 case "TRADE_CLOSED":
  45.                     log.info("超时未支付,交易已关闭,或支付完成后全额退款");
  46.                     //其余业务逻辑
  47.                     break;
  48.                 case "WAIT_BUYER_PAY":
  49.                     log.info("交易创建,等待买家付款");
  50.                     //其余业务逻辑
  51.                     break;
  52.             }
  53.             response.getWriter().write("success");   //返回success给支付宝,表示消息我已收到,不用重调
  54.         } else {
  55.             response.getWriter().write("fail");   ///返回fail给支付宝,表示消息我没收到,请重试
  56.         }
  57.     }
  58. }

到此,看下效果,请求下单接口:

图片

图片

用沙箱版app扫码:

图片

图片

支付,查看余额:

图片

图片

7、查询支付结果

主动查询用户的支付结果,订单编号依然写死:

  1. @RestController
  2. @Slf4j
  3. public class PayController {
  4.     @Resource
  5.     private Config config;
  6.     
  7.     @GetMapping("/query")
  8.     public String query() throws Exception {
  9.         Factory.setOptions(config);
  10.         AlipayTradeQueryResponse result = Factory.Payment.Common().query("LS123qwe123");
  11.         return result.getHttpBody();
  12.     }
  13. }

图片

图片

8、退款

退款操作:

  1. @RestController
  2. @Slf4j
  3. public class PayController {
  4.     @Resource
  5.     private Config config;
  6.     @GetMapping("/refund")
  7.     public String refund() throws Exception {
  8.         Factory.setOptions(config);
  9.         AlipayTradeRefundResponse refundResponse = Factory.Payment.Common().refund("LS123qwe123""19999");
  10.         return refundResponse.getHttpBody();
  11.     }
  12. }

图片

图片

9、通用版SDK

官方文档就是以这个SDK为例的,贴个代码示例:

  1. private static final String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do";
  2. private static final String FORMAT = "JSON";
  3. private static final String CHARSET = "UTF-8";
  4.     //签名方式
  5.     private static final String SIGN_TYPE = "RSA2";
  6. @Resource
  7. private AliPayConfig aliPayConfig;
  8. @Resource
  9. private OrdersMapper ordersMapper;
  10. @GetMapping("/pay"// &subject=xxx&traceNo=xxx&totalAmount=xxx
  11. public void pay(AliPay aliPay, HttpServletResponse httpResponse) throws Exception {
  12.     // 1. 创建Client,通用SDK提供的Client,负责调用支付宝的API
  13.     AlipayClient alipayClient = new DefaultAlipayClient(GATEWAY_URL, aliPayConfig.getAppId(),
  14.             aliPayConfig.getAppPrivateKey(), FORMAT, CHARSET, aliPayConfig.getAlipayPublicKey(), SIGN_TYPE);
  15.     // 2. 创建 Request并设置Request参数
  16.     AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();  // 发送请求的 Request类
  17.     request.setNotifyUrl(aliPayConfig.getNotifyUrl());
  18.     JSONObject bizContent = new JSONObject();
  19.     bizContent.set("out_trade_no", aliPay.getTraceNo());  // 我们自己生成的订单编号
  20.     bizContent.set("total_amount", aliPay.getTotalAmount()); // 订单的总金额
  21.     bizContent.set("subject", aliPay.getSubject());   // 支付的名称
  22.     bizContent.set("product_code""FAST_INSTANT_TRADE_PAY");  // 固定配置
  23.     request.setBizContent(bizContent.toString());
  24.     // 执行请求,拿到响应的结果,返回给浏览器
  25.     String form = "";
  26.     try {
  27.         form = alipayClient.pageExecute(request).getBody(); // 调用SDK生成表单
  28.     } catch (AlipayApiException e) {
  29.         e.printStackTrace();
  30.     }
  31.     httpResponse.setContentType("text/html;charset=" + CHARSET);
  32.     httpResponse.getWriter().write(form);// 直接将完整的表单html输出到页面
  33.     httpResponse.getWriter().flush();
  34.     httpResponse.getWriter().close();
  35. }

具体有业务数据逻辑的对接支付宝接口,可跳转支付宝业务对接:

  • https://llg-notes.blog.csdn.net/article/details/130357977

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/744953
推荐阅读
相关标签
  

闽ICP备14008679号