对接支付宝实现门户网站在线支付功能

对接支付宝实现门户网站在线支付功能

朱治龙
2024-06-29 / 0 评论 / 57 阅读 / 正在检测是否收录...

前言

近期,我所在团队负责的控制台项目计划新增在线充值功能。为实现此功能,我们首先需完成在线支付环节的对接。初期,我们将优先接入支付宝支付服务。
值得一提的是,我上一次进行支付宝对接,还是在十多年前参与的笨鸟旅行项目,那时我们采用的是 jsp + mybatis 技术栈。与现在便捷的SDK调用相比,当时的开发条件相对简陋,整个项目甚至连 maven 包管理工具都未使用,前端工程化也仅处于起步阶段,CI/CD流程更是无从谈起。回顾过去,不禁让人感叹技术发展带来的日新月异。

支付宝支付流程

支付宝支付流程大致如下:

1、 注册商家并绑定开发人员 :支付宝商家平台( https://b.alipay.com/ )注册商家,多说一嘴,注册成功后,可以在商家平台的账号中心 → 员工列表 添加员工为子账号,这样后续就不需要老是麻烦Boss帮忙扫码或提供验证码了:
支付宝商家平台-员工列表

2、 申请开通支付产品: 商家平台的产品中心根据业务场景需要,申请开通相关支付产品。如我们申请开通了「电脑网站支付」、「手机网站支付」两款支付产品,申请后一般几分钟就开通了。
开通的支付产品

3、 创建应用: 开通产品后就可以创建应用了,单击对应的产品可以进入关联应用页面创建应用并关联,这个界面需要超级管理员将开发人员添加为开放平台的管理员,这样开发人员就可以在开放平台进行应用相关配置了。
关联应用

4、 开发配置: 进入支付宝开放平台,进入应用详情,可在「开发设置」模块中根据流程配置加密证书等信息

5、发布上线:申请完的应用都是开发中状态,前期验证阶段踩了些小坑,以为配置好并生成证书就可以着手开发对接了,后面才发现状态为开发中的应用,是无法调用线上正式接口的,需要应用上线才行,所以我们在「开发设置」中配置好以后就直接提交审核就好,一般提交审核一天左右就可以审核通过,审核通过就可以正式开发对接了。

6、开发对接:开发对接根据支付宝官网的文档来就好,现在对接支付宝可以直接使用高度封装的 SDK,开发速度上快了不少。电脑网站支付产品的文档链接为:https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667 。前期尝试了使用 v3 版本的对接方式(文档链接为: https://opendocs.alipay.com/open-v3/05w3qc ),但一直报证书方面的错误,后改用旧版本的 SDK 没发现问题。

直接上代码

码了那么多字,总算到最简单的环节了,我们的后端服务核心技术栈:JDK17 + SpringBoot 2.7.x + lombok + Mybatis-plus + MySQL + Hutool,使用 Maven 构建项目,所以我们这个示例工程也是基于上面的技术栈。以下是支付相关的核心代码:

一、准备工作

1、引入 Alipay SDK

在项目 pom.xml 文件 的 dependencies 节点中加入 Alipay SDK 的依赖:

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.39.104.ALL</version>
</dependency>

由于 V3 版本没有验证通过,这里我们没有根据文档站中的建议采用 V3 版本。

2、定义配置类

跟支付宝对接相关的信息我们统一写到 application.yml 配置文件中, 使用一个配置类来快速加载配置文件中的数据。

package com.paratera.console.pay.config;

import com.alipay.api.AlipayConfig;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;


@Configuration
@ConfigurationProperties(prefix = "alipay")
@Data
public class AlipayProperties extends AlipayConfig {
    /**
     * 支付成功通知URL
     */
    private String notifyUrl;
}

然后,我们在application.yml 中添加如下配置信息:

alipay:
  appId: 2021004153624250
  serverUrl: https://openapi.alipay.com/gateway.do
  notifyUrl: https://paydemo.work.pojian.online/pay/ali/notify
  privateKey: MIIEvgIBADANBgkqhkiG9w0BAQEF....RMXJeS
  alipayPublicCertPath: /alipay/alipayCertPublicKey_RSA2.crt
  rootCertPath: /alipay/alipayRootCert.crt
  appCertPath: /alipay/appCertPublicKey_2021004153624250.crt
  encryptKey: EfaaDErfTRvO2islsjk3thAQ==

3、定义签名校验方法

为避免一些没必要的安全隐患,后端接收到支付宝推送的通知内容需要做验签操作,验签通过后再进行核心业务操作。

/**
     * 验签
     * @param paramMap
     * @return
     */
    public boolean checkSign(Map<String, String> paramMap) {
        try {
            String alipayPublicKey = alipayProperties.getAlipayPublicKey();
            if (ObjectUtil.isEmpty(alipayPublicKey)) {
                String alipayPublicCertPath = alipayProperties.getAlipayPublicCertPath();
                alipayPublicKey = AlipaySignature.getAlipayPublicKey(alipayPublicCertPath);
                alipayProperties.setAlipayPublicKey(alipayPublicKey);
            }
            return AlipaySignature.rsaCheckV1(paramMap, alipayPublicKey, alipayProperties.getCharset(), "RSA2");
        } catch (AlipayApiException e) {
            throw new RuntimeException(e);
        }
    }

二、对接「统一收单下单并支付页面接口」

1、controller 定义支付申请入口

@PostMapping(path="/pay-prepare")
@ResponseBody
public String pay(PayForm payForm) throws AlipayApiException {
    String payHtml = alipayService.payPrepare(payForm);
    if (ObjectUtil.isEmpty(payHtml)) {
        return "支付失败";
    }
    return payHtml;
}

PayForm 主要定义充值相关的字段,如充值金额、用户Id、邮箱、备注等,根据业务自己来就好,我这里采用最简的数据:

package com.paratera.console.pay.model.form;

import lombok.Data;
import javax.validation.constraints.NotBlank;

/**
 * 支付表单
 */
@Data
public class PayForm {
    /**0
     * 用户Id
     */
    @NotBlank(message = "用户Id不能为空")
    private String userId;
    /**
     * 支付金额
     */
    @NotBlank(message = "支付金额不能为空")
    private String money;
}

2、业务实现 AlipayService.payPrepare()

public String payPrepare(PayForm payForm) throws AlipayApiException {
      Faker faker = new Faker(Locale.CHINA);
      CertAlipayRequest alipayConfig = new CertAlipayRequest();
      alipayConfig.setServerUrl(alipayProperties.getServerUrl());
      alipayConfig.setAppId(alipayProperties.getAppId());
      alipayConfig.setPrivateKey(alipayProperties.getPrivateKey());

      alipayConfig.setAlipayPublicCertPath(alipayProperties.getAlipayPublicCertPath());
      alipayConfig.setCertPath(alipayProperties.getAppCertPath());
      alipayConfig.setSignType("RSA2");
      alipayConfig.setCharset("UTF-8");
      alipayConfig.setFormat("json");
      alipayConfig.setRootCertPath(alipayProperties.getRootCertPath());
      alipayConfig.setEncryptor(alipayProperties.getEncryptKey());

      AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
      
      // 实例化客户端
      AlipayTradePagePayRequest payRequest = new AlipayTradePagePayRequest();
      payRequest.setReturnUrl("https://paydemo.work.pojian.online/payResult.html");
      payRequest.setNotifyUrl(alipayProperties.getNotifyUrl());
      AlipayTradePagePayModel alipayTradePayModel = new AlipayTradePagePayModel();
      // 调用 alipay.trade.pay
      LocalDateTime payTime = LocalDateTimeUtil.now();
      String orderNo = "PAY" + DateUtil.format(payTime, DatePattern.PURE_DATE_PATTERN)
              + faker.number().digits(6);
      alipayTradePayModel.setOutTradeNo(orderNo);
      alipayTradePayModel.setTotalAmount(payForm.getMoney());
      alipayTradePayModel.setSubject("控制台充值-" + DateUtil.format(payTime, DatePattern.PURE_DATE_PATTERN) + "-" + payForm.getMoney() + "元");
      alipayTradePayModel.setProductCode("FAST_INSTANT_TRADE_PAY");
      payRequest.setBizModel(alipayTradePayModel);
      AlipayTradePagePayResponse response = alipayClient.pageExecute(payRequest, "POST");
      // 发起调用
      String pageRedirectionData = response.getBody();
//        log.info(pageRedirectionData);
      if (response.isSuccess()) {
//            log.info("调用成功");
          // 保存充值记录
          AddRechargePayForm addForm = new AddRechargePayForm();
          addForm.setOrderNo(orderNo);
          addForm.setSubject(alipayTradePayModel.getSubject());
          addForm.setPayMethod("ALIPAY");
          addForm.setUserId(payForm.getUserId());
          // 原单位是元,需要转换为分
          BigDecimal money = new BigDecimal(payForm.getMoney()).multiply(BigDecimal.valueOf(100));
          addForm.setPayMoney(money);
          rechargePayService.addRechargePay(addForm);
          return pageRedirectionData;
      } else {
          log.error("调用失败");
          // sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
           String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
          log.error(diagnosisUrl);
      }
      return null;
}

上面的过程主要是生成如下所示的包含签名数据的html代码,在网页里通过自动提交表单的方式跳转到支付宝的付款页面:

<form name="punchout_form" method="post" action="https://openapi.alipay.com/gateway.do?app_cert_sn=653da2afe61d430fcc9376b008245959&charset=UTF-8&alipay_root_cert_sn=687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6&method=alipay.trade.page.pay&sign=gQOZ2g8xU9lyA2k55FJNSTbleinvaFA%2FjrfuUInElycpytnWweBVDECnC0q7d2tZ%2FOQvxQc5%2Fyxm2OBTc7ERa%2Fs5%2Fi4vTAD2QyukrlKLN1SleGZP5%2FihTpcB%2BM5WkpHyQjsRrnHeFHB3i8Nzfc%2B9ICdz5xJCx8dlu%2BVN9qb51hx9y4eyTvWd2XY4%2BfmW3RHsE8PYQ6VU9YtMP8wjFonaPMExY9WnpJn74fNVj8wLHmxYemxkW80qXhbOxGik2qHY9NWfkWZsYhzMbBu5%2BUBXWdprlbakThSPqXjNpagUMmdPNiGjvLWy3XYtFHfYsOQdbIIPrPlhuQIYmmUfJF9M3w%3D%3D&return_url=https%3A%2F%2Fpaydemo.work.pojian.online%2FpayResult.html&notify_url=https%3A%2F%2Fpaydemo.work.pojian.online%2Fpay%2Fali%2Fnotify&version=1.0&app_id=2021004153624250&sign_type=RSA2&timestamp=2024-06-26+18%3A15%3A01&alipay_sdk=alipay-sdk-java-4.39.104.ALL&format=json">
<input type="hidden" name="biz_content" value="{&quot;out_trade_no&quot;:&quot;PAY20240626445677&quot;,&quot;product_code&quot;:&quot;FAST_INSTANT_TRADE_PAY&quot;,&quot;subject&quot;:&quot;控制台充值-20240626-0.1元&quot;,&quot;total_amount&quot;:&quot;0.1&quot;}">
<input type="submit" value="立即支付" style="display:none" >
</form>
<script>document.forms[0].submit();</script>

二、对接支付结果的异步通知

发起支付操作的时候我们有指定两个URL:

  • return_url:用于支付完成后跳转到我们的页面给用户展示支付结果
  • notify_url:用于支付完成后通知后端更新支付结果,由于return_url给的支付结果一般不可信,所以我们一般采用这个地址告诉支付宝,用户支付完后异步通知我们哪个接口

1、定义接收通知的接口

/**
 * 支付宝付款通知
 *
 * https://opendocs.alipay.com/open/270/105902?pathHash=d5cd617e&ref=api
 * @return
 */
@RequestMapping("/notify")
public String payNotify(HttpServletRequest request) throws UnsupportedEncodingException {
    log.info("====== 开始接收支付宝支付回调通知 ======");
    Map<String, String> paramMap = new HashMap<>();
    Map<String, String[]> requestParams = request.getParameterMap();
    log.info("获取支付宝POST过来反馈信息");
    for (String name : requestParams.keySet()) {
        String[] values = requestParams.get(name);
        String valueStr = "";
        for (int i = 0; i < values.length; i++) {
            valueStr = (i == values.length - 1) ? valueStr + values[i]
                    : valueStr + values[i] + ",";
        }
        paramMap.put(name, valueStr);
    }
    log.info("通知请求数据:{}", JSONUtil.toJsonStr(paramMap));
    if(ObjectUtil.isNotEmpty(paramMap)) {
        log.info("支付宝回调URL参数:{}", JSONUtil.toJsonStr(paramMap));
        boolean signVerified = alipayService.checkSign(paramMap);
        if (signVerified) {
            if ("TRADE_SUCCESS".equals(paramMap.get("trade_status"))) {
                rechargePayService.paid(paramMap.get("out_trade_no"), JSONUtil.toJsonStr(paramMap));
            }
        }
        // {"gmt_create":["2024-06-25 15:18:10"],"charset":["UTF-8"],"gmt_payment":["2024-06-25 15:18:22"],"notify_time":["2024-06-25 15:18:23"],"subject":["Jacob Have I Loved"],"sign":["CM2qWtZ4rCmvqFyRucgANQJAz2k0SsroaU3uDEK28kgxkcHC29a64jquy4/qRfEiX4VuJKdmxkHG3+hEAC/9/qOgl8mG/GGSPlq+B90fg25aymYlKoM6wYOzEqooOLj2LCtj8Nq1WKaw9pTwYK/GlDdVLsUB8bSpkpjZ6vrhhzjeXK38wDgZuNKle9JdFCQLO/f1vcyM+h4SQpkurg/jTYB7oQQWk/ZsQAqC/XM/laKrPMPrA4SYb8hcC0UIQD8BLlCZiXaEj/+Q6CTJQffGyW2q6trTEhySgb4I3OwTjqtbn1BlafFCTm1OYgMlVLZu5U+iwvhPW2Xmz8A2v3RWIQ=="],"merchant_app_id":["2021004153624250"],"buyer_open_id":["037QDGdQKn6R2z7On4z_OJrBOzaNN0z5Obtjr_T4aftVdU9"],"invoice_amount":["0.10"],"version":["1.0"],"notify_id":["2024062501222151823038371452266751"],"fund_bill_list":["[{\"amount\":\"0.10\",\"fundChannel\":\"ALIPAYACCOUNT\"}]"],"notify_type":["trade_status_sync"],"out_trade_no":["PAY20240625635868"],"total_amount":["0.10"],"trade_status":["TRADE_SUCCESS"],"trade_no":["2024062522001438371427121571"],"auth_app_id":["2021004153624250"],"receipt_amount":["0.10"],"point_amount":["0.00"],"buyer_pay_amount":["0.10"],"app_id":["2021004153624250"],"sign_type":["RSA2"],"seller_id":["2088641848623112"]}
    }
    return "success";
}

2、核心逻辑说明

alipayService.checkSign()这个验签方法在前面的章节已经定义过,这里就不着重介绍了。rechargePayService.paid()是内部业务逻辑,本示例中主要是存储支付宝推送的数据并改变订单状态,在这里就不做代码罗列了。

示例工程的几个核心页面效果

为更好的展示示例效果,在示例工程开发过程中做了些界面展示,可通过如下链接体验完整功能:https://paydemo.work.pojian.online/

相关核心界面截图如下:

0

评论 (0)

取消