前言
近期,我所在团队负责的控制台项目计划新增在线充值功能。为实现此功能,我们首先需完成在线支付环节的对接。初期,我们将优先接入支付宝支付服务。
值得一提的是,我上一次进行支付宝对接,还是在十多年前参与的笨鸟旅行项目,那时我们采用的是 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¬ify_url=https%3A%2F%2Fpaydemo.work.pojian.online%2Fpay%2Fali%2Fnotify&version=1.0&app_id=2021004153624250&sign_type=RSA2×tamp=2024-06-26+18%3A15%3A01&alipay_sdk=alipay-sdk-java-4.39.104.ALL&format=json">
<input type="hidden" name="biz_content" value="{"out_trade_no":"PAY20240626445677","product_code":"FAST_INSTANT_TRADE_PAY","subject":"控制台充值-20240626-0.1元","total_amount":"0.1"}">
<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)