跳到主要内容

支付宝支付架构

背景说明

  1. Javashop电商系统对接了支付宝支付相关接口,集成了支付宝Java通用版SDK,开发起来更加简单便捷。
  2. 我们还提供了便捷的参数配置功能,只需要在管理平台配置好相关的支付宝参数(如应用私钥、证书等),就可以直接使用支付宝支付。

接口类型

Javashop电商系统一共对接了3种支付宝支付的接口,分别为:电脑网站支付、手机网站支付和APP支付

支付接口对应的客户端类型如下:

微信支付接口客户端类型官方接口文档
电脑网站支付电脑浏览器(PC-Web)端在线文档
手机网站支付手机浏览器(Mobile-Web)端在线文档
APP支付移动端APP应用在线文档

SDK接入

支付宝Java通用版SDK库地址:GitHub 项目主页 (需要科学上网)

在系统中,我们在javashop-core工程下的pom.xml文件中加入了以下maven依赖,这样在程序中我们就可以直接使用支付宝SDK中已经集成好的方法了。

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

经监测,开源的Java开发组件Fastjson存在远程代码执行漏洞,攻击者可利用上述漏洞远程执行任意代码。Java SDK(alipay-sdk-java)在4.34.0版本之前使用了存在漏洞的Fastjson版本,因此我们使用的版本为4.35.150.ALL。

初始化配置

在调用支付宝相关接口之前,都要先初始化配置信息,接入SDK后就不需要手动编写计算请求签名和验证应答签名的代码了,直接使用SDK中集成好的方法即可。

支付宝提供了2种加签模式:公钥模式加签和公钥证书模式加签。我们选择的是公钥证书模式加签

    /**
* 根据支付宝配置参数创建AlipayClient实例
*
* @param config 支付宝配置参数
* @return
*/
protected AlipayClient buildClient(Map<String, String> config) {
try {
//获取应用ID
String appId = config.get("app_id");
//获取应用私钥
String appPrivateKey = config.get("app_private_key");
//获取应用公钥证书文件内容
String appPublicCert = this.getFileContent(config.get("app_public_cert_id"));
//获取支付宝公钥证书文件内容
String alipayPublicCert = this.getFileContent(config.get("alipay_public_cert_id"));
//获取支付宝根证书文件内容
String alipayRootCert = this.getFileContent(config.get("alipay_root_cert_id"));

debugger.log("使用如下参数创建AlipayClient实例:");
debugger.log(config.toString());

CertAlipayRequest alipayConfig = new CertAlipayRequest();
//设置商户私钥
alipayConfig.setPrivateKey(appPrivateKey);
//设置支付宝网关
alipayConfig.setServerUrl(AlipayConfig.gatewayUrl);
//设置应用ID
alipayConfig.setAppId(appId);
//设置编码集
alipayConfig.setCharset(AlipayConfig.charset);
//设置签名方式
alipayConfig.setSignType(AlipayConfig.signType);
//设置参数返回格式
alipayConfig.setFormat(AlipayConfig.format);
//设置应用公钥证书
alipayConfig.setCertContent(appPublicCert);
//设置支付宝公钥证书
alipayConfig.setAlipayPublicCertContent(alipayPublicCert);
//设置支付宝根证书
alipayConfig.setRootCertContent(alipayRootCert);
//创建AlipayClient实例
AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);

return alipayClient;
} catch (AlipayApiException e) {
this.logger.error("创建AlipayClient实例失败", e);
}
return null;
}

通过以下方法,我们可以获取到支付宝在平台管理端配置的参数信息:

/**
* 读取支付方式在平台端配置的参数信息
* @param clientType 客户端类型 {@link com.enation.app.javashop.model.base.ClientTypeEnum}
* @param pluginId 支付方式唯一标识 支付宝:alipayDirectPlugin
* @return
*/
Map<String, String> ThirdPlatformManager.getConfig(clientType, pluginId);

注意:在Javashop电商系统平台管理端配置微信支付参数时,我们是直接上传的相关证书,而不是配置证书内容(具体可参考支付宝参数配置),但是在代码中加载配置时我们用到的是相关证书的内容,所以在代码中我们需要先取出存放在数据库中的证书信息,再获取证书内容,代码如下:

    /**
* 获取证书内容
* @param id 证书主键ID
* @return 证书内容
*/
protected String getFileContent(String id) {
try {
//获取证书信息
FileStreamDO fileStreamDO = this.fileStreamManager.get(Long.parseLong(id));
byte[] buffer = fileStreamDO.getStream();
return new String(buffer, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

接口调用

支付调用流程图

image-20230719160609315

重点步骤说明:

  • 步骤1:调用的API包含:交易支付、订单支付、会员充值和店铺升级等等

  • 步骤4:要根据支付插件ID来获取到支付插件信息,代码如下:

        /**
    * 查找支付插件
    *
    * @param pluginId
    * @return
    */
    private PaymentPluginManager findPlugin(String pluginId) {
    for (PaymentPluginManager plugin : paymentPluginList) {
    if (plugin.getPluginId().equals(pluginId)) {
    return plugin;
    }
    }
    return null;
    }
  • 步骤6:在调用支付插件时,还要根据不同的客户端类型来判断具体调用哪个支付宝支付下单接口

电脑网站支付

时序图

image-20230720114616944

重点步骤说明:

  • 步骤4:调用电脑网站支付的支付接口 alipay.trade.page.pay(统一收单下单并支付页面接口)发起支付请求

  • 步骤5-6:接口返回的是支付宝扫码支付页面地址,格式如下:

    https://openapi.alipay.com/gateway.do?charset=UTF-8&method=alipay.trade.page.pay&sign=k0w1DePFqNMQWyGBwOaEsZEJuaIEQufjoPLtwYBYgiX%2FRSkBFY38VuhrNumXpoPY9KgLKtm4nwWz4DEQpGXOOLaqRZg4nDOGOyCmwHmVSV5qWKDgWMiW%2BLC2f9Buil%2BEUdE8CFnWhM8uWBZLGUiCrAJA14hTjVt4BiEyiPrtrMZu0o6%2FXsBu%2Fi6y4xPR%2BvJ3KWU8gQe82dIQbowLYVBuebUMc79Iavr7XlhQEFf%2F7WQcWgdmo2pnF4tu0CieUS7Jb0FfCwV%2F8UyrqFXzmCzCdI2P5FlMIMJ4zQp%2BTBYsoTVK6tg12stpJQGa2u3%2BzZy1r0KNzxcGLHL%2BwWRTx%2FCU%2Fg%3D%3D&notify_url=http%3A%2F%2F114.55.81.185%2Fopendevtools%2Fnotify%2Fdo%2Fbf70dcb4-13c9-4458-a547-3a5a1e8ead04&version=1.0&app_id=2014100900013222&sign_type=RSA&timestamp=2021-02-02+14%3A11%3A40&alipay_sdk=alipay-sdk-java-dynamicVersionNo&format=json

    前端接收到此地址后,进行页面跳转

  • 步骤7:支付宝扫码支付页面是由支付宝提供,不属于JavashopPC客户端页面

代码展示

    /**
* PC端下单并支付
*
* @param bill 下单支付参数信息
* @return
*/
public Map pcPay(PayBill bill) {
try {
//根据客户端类型创建AlipayClient实例
AlipayClient alipayClient = this.buildClient(bill.getClientType());

//设置请求参数
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();

//设置同步跳转地址
String returnUrl = this.getReturnUrl(bill);
request.setReturnUrl(returnUrl);
logger.info("支付宝支付下单支付同步跳转地址:" + returnUrl);

//设置异步接收地址(接收支付宝支付结果通知的地址)
String notifyUrl = this.getCallBackUrl(bill.getTradeType(), bill.getClientType());
request.setNotifyUrl(notifyUrl);
logger.info("支付宝支付下单支付结果异步接收地址:" + returnUrl);

debugger.log("支付宝支付下单支付结果异步接收地址:" + returnUrl);

//设置请求参数的集合(最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递)
String bizContent = this.buildBizContent(bill.getBillSn(), bill.getOrderPrice(), bill.getSubSn(), FAST_INSTANT_TRADE_PAY);
request.setBizContent(bizContent);

//调用统一收单下单并支付页面接口
AlipayResponse response = alipayClient.pageExecute(request, "GET");
logger.info("调用统一收单下单并支付页面接口返回结果:" + response.getBody());

//返回结果
Map map = new HashMap(1);
map.put("gateway_url", response.getBody());
return map;
} catch (Exception e) {
logger.error("调用支付宝PC端统一收单下单并支付页面接口报错:", e);
}
return null;
}

重点代码说明:

  • 接口请求参数分为公共请求参数和其它请求参数 公共请求参数是可以直接set进AlipayTradePagePayRequest中的 其它请求参数,需要以json的形式组装起来,通过公共参数中的biz_content来进行传递
  • 通过alipayClient.pageExecute()请求接口时,默认请求类型为POST,即生成form表单,因此我们设置请求类型为GET,这样返回的response.getBody()就是url链接了(可参考官方文档pageExecute()方法如何生成url形式的请求链接

手机网站支付

时序图

image-20230720153306833

重点步骤说明:

  • 步骤4:调用手机网站支付的支付接口 alipay.trade.wap.pay(手机网站支付接口2.0)发起支付请求

  • 步骤7:支付宝支付页面有两种支付方式:打开支付宝APP付款和继续浏览器付款

    如果手机安装了支付宝应用,默认会优先打开支付宝APP进行付款

    如果未安装支付宝应用,可以选择继续浏览器付款或者下载支付宝APP付款

    image-20230720154131683

  • 步骤8-13:此流程内所有页面均由支付宝提供,不属于JavashopH5客户端页面

代码展示

    /**
* WAP端下单并支付
*
* @param bill 下单支付参数信息
* @return
*/
public Map wapPay(PayBill bill) {
try {
//根据客户端类型创建AlipayClient实例
AlipayClient alipayClient = this.buildClient(bill.getClientType());

//设置请求参数
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();

//设置同步跳转地址
String returnUrl = this.getReturnUrl(bill);
request.setReturnUrl(returnUrl);
logger.info("支付宝支付下单支付同步跳转地址:" + returnUrl);

//设置异步接收地址(接收支付宝支付结果通知的地址)
String notifyUrl = this.getCallBackUrl(bill.getTradeType(), bill.getClientType());
request.setNotifyUrl(notifyUrl);
logger.info("支付宝支付下单支付结果异步接收地址:" + returnUrl);

//设置请求参数的集合(最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递)
String bizContent = this.buildBizContent(bill.getBillSn(), bill.getOrderPrice(), bill.getSubSn(), QUICK_WAP_WAY);
request.setBizContent(bizContent);

//调用支付宝手机网站支付接口
AlipayTradeWapPayResponse response = alipayClient.pageExecute(request, "GET");
logger.info("调用支付宝手机网站支付接口返回结果:" + response.getBody());

//返回结果
Map map = new HashMap(1);
map.put("gateway_url", response.getBody());
return map;

} catch (Exception e) {
logger.error("调用支付宝手机网站支付接口报错:", e);
}
return null;
}

重点代码说明:

  • 接口请求参数分为公共请求参数和其它请求参数 公共请求参数是可以直接set进AlipayTradeWapPayRequest中的 其它请求参数,需要以json的形式组装起来,通过公共参数中的biz_content来进行传递
  • 通过alipayClient.pageExecute()请求接口时,默认请求类型为POST,即生成form表单,因此我们设置请求类型为GET,这样返回的response.getBody()就是url链接了(可参考官方文档pageExecute()方法如何生成url形式的请求链接

APP支付

时序图

image-20230720163150599

重点步骤说明:

  • 步骤4:调用APP支付的支付接口 alipay.trade.app.pay(app支付接口2.0)发起支付请求

  • 步骤5-6:获取支付参数格式如下:

    {
    "alipay_trade_app_pay_response": {
    "code": "10000",
    "msg": "Success",
    "app_id": "2014072300007148",
    "auth_app_id": "2014072300007148",
    "charset": "utf-8",
    "timestamp": "2016-10-11 17:43:36",
    "out_trade_no": "081622560194853",
    "total_amount": "9.00",
    "trade_no": "2016081621001004400236957647",
    "seller_id": "2088702849871851"
    },
    "sign": "NGfStJf3i3ooWBuCDIQSumOpaGBcQz+aoAqyGh3W6EqA/gmyPYwLJ********",
    "sign_type": "RSA2"
    }
  • 步骤8:正常场景下,会唤起支付宝收银台等待用户核身;异常场景下,会返回异常信息

  • 步骤17:用户在支付宝 App 完成支付后,会跳转回商家页面,并返回最终的支付结果(即同步通知),可查看 同步通知参数说明

代码展示

    /**
* APP端下单并支付
*
* @param bill 下单支付参数信息
* @return
*/
public Map appPay(PayBill bill) {
try {
//根据客户端类型创建AlipayClient实例
AlipayClient alipayClient = this.buildClient(bill.getClientType());

//设置请求参数
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();

//设置同步跳转地址
String returnUrl = this.getReturnUrl(bill);
request.setReturnUrl(returnUrl);
logger.info("支付宝支付下单支付同步跳转地址:" + returnUrl);

//设置异步接收地址(接收支付宝支付结果通知的地址)
String notifyUrl = this.getCallBackUrl(bill.getTradeType(), bill.getClientType());
request.setNotifyUrl(notifyUrl);
logger.info("支付宝支付下单支付结果异步接收地址:" + returnUrl);

//设置请求参数的集合(最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递)
String bizContent = this.buildBizContent(bill.getBillSn(), bill.getOrderPrice(), bill.getSubSn(), QUICK_MSECURITY_PAY);
request.setBizContent(bizContent);

//调用支付宝APP支付接口
AlipayTradeAppPayResponse response = alipayClient.certificateExecute(request);

logger.info("调用支付宝APP支付接口返回结果:" + response.getBody());
return JsonUtil.toMap(response.getBody());

} catch (Exception e) {
e.printStackTrace();
}
return null;
}

重点代码说明:

  • 接口请求参数分为公共请求参数和其它请求参数 公共请求参数是可以直接set进AlipayTradeWapPayRequest中的 其它请求参数,需要以json的形式组装起来,通过公共参数中的biz_content来进行传递

支付异步通知

支付宝通过支付通知接口将用户支付成功消息通知给商户

可参考官方文档:支付宝异步通知说明

设置回调URL

在调用支付宝支付接口时,我们就要把异步通知回调地址作为参数放入到请求中,在AbstractPaymentPlugin.java中我们定义了获取异步通知回调地址的方法

    /**
* 获取异步通知url
*
* @param tradeType 交易类型
* @param clientTypeEnum 客户端类型
* @return
*/
protected String getCallBackUrl(TradeTypeEnum tradeType, ClientTypeEnum clientTypeEnum) {
return domainHelper.getCallback() + "/payment/callback/" + tradeType + "/" + this.getPluginId() + "/" + clientTypeEnum;
}

支付宝支付回调地址格式:https://buyer-api域名/payment/callback/交易类型/alipayDirectPlugin/客户端类型。

以电脑完整支付为例

回调地址为:https://api-v730.javamall.com.cn/buyer-api/buyer/payment/callback/TRADE/alipayDirectPlugin/PC

异步通知特性

1、在进行异步通知交互时,如果支付宝收到的应答不是 success ,支付宝会认为通知失败,会通过一定的策略定期重新发起通知。通知的间隔频率为:4m、10m、10m、1h、2h、6h、15h

2、商家设置的异步地址(notify_url)需保证无任何字符,如空格、HTML 标签,且不能重定向。(如果重定向,支付宝会收不到 success 字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知)

3、支付宝是用 POST 方式发送通知信息,商户获取参数的方式如下:

String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");

4、支付宝针对同一条异步通知重试时,异步通知参数中的 notify_id 是不变的。

异步返回结果验签

1、在通知返回参数列表中,除去 signsign_type 两个参数外,通知返回的参数均为待验签的参数。

2、将剩下参数进行 URLDecode,然后进行字典排序,组成字符串,得到待签名字符串。

3、将签名参数(sign)使用 base64 解码为字节码串。

4、使用 RSA 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名。

我们可以使用支付宝SDK提供的AlipaySignature.java工具类进行验签操作,具体代码如下:

    /**
* 支付宝支付结果回调
* @param clientTypeEnum 支付客户端类型 {@link ClientTypeEnum}
* @return
*/
public String aliPayCallback(ClientTypeEnum clientTypeEnum) {
try {
debugger.log("进入支付宝异步通知回调");

// 获取回调请求参数
HttpServletRequest request = ThreadContextHolder.getHttpRequest();
// 交易状态
String tradeStatus = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");

//如果交易状态不为支付成功,直接返回通知失败即可
if (!TRADE_SUCCESS.equals(tradeStatus)) {
return CALLBACK_FAIL;
}

// 商户订单号
String outTradeNo = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
// 支付宝交易号
String returnTradeNo = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"), "UTF-8");
// 付款金额
String totalAmount = request.getParameter("total_amount");
this.logger.info("获取的支付宝异步回调通知请求参数包括:outTradeNo["+outTradeNo+"],returnTradeNo["+returnTradeNo+"],tradeStatus["+tradeStatus+"],totalAmount["+totalAmount+"]");

// 根据商户订单号查询交易账单信息
PaymentBillDO paymentBill = paymentBillManager.getModel(outTradeNo);
this.logger.info("根据商户订单号查询到的交易账单信息为:" + paymentBill);

// 判断交易账单是否已经支付过了 如果已经支付,则直接返回success即可
if (paymentBill != null && paymentBill.getIsPay() == 1) {
return CALLBACK_SUCCESS;
}

// 获取支付宝配置参数
Map<String, String> configParams = this.getConfig(clientTypeEnum);
/**
* 通过支付宝公钥证书来获取支付宝公钥信息
* 管理端上传的支付宝公钥证书内容并不是支付宝公钥,需要
* 按照以下步骤获取真正的公钥
* 1、先获取公钥证书内容
* 2、通过公钥证书内容获取X509的证书信息
* 3、通过X509证书获取到公钥信息
*/
String alipayPublicCertContent = this.getFileContent(configParams.get("alipay_public_cert_id"));
X509Certificate cert = AntCertificationUtil.getCertFromContent(alipayPublicCertContent);
PublicKey publicKey = cert.getPublicKey();
String alipayPublicKey = Base64.encodeBase64String(publicKey.getEncoded());

//获取支付宝异步通知回调参数(将异步通知中收到的所有参数都存放到map中)
Map<String, String> params = new HashMap<>(16);
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = iter.next();
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] + ",";
}
params.put(name, valueStr);
}
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayPublicKey, AlipayConfig.charset, AlipayConfig.signType);

// 对支付宝异步回调参数进行验签,验签通过,则修改订单相关状态;验签失败直接返回失败信息
if (signVerified) {

debugger.log("支付宝异步返回结果验签成功");

//将通知中的付款金额字段类型转为double类型
double payPrice = StringUtil.toDouble(totalAmount, 0d);
//调用支付成功后的执行方法
this.paySuccess(outTradeNo, returnTradeNo, payPrice);
//返回成功的应答消息(此处不要进行修改,如果支付宝收到的应答不是 success ,支付宝会认为通知失败,会通过一定的策略定期重新发起通知)
return CALLBACK_SUCCESS;
}

} catch (Exception e) {
this.logger.error("支付宝异步返回结果通知失败", e);
}

return CALLBACK_FAIL;
}

通知应答

image-20230720165812337

支付宝退款

退款流程时序图

image-20230720170608096

重点步骤说明:

  • 步骤5:调用支付宝统一收单交易退款接口申请退款

  • 步骤6:退款是否成功可以根据同步响应的 fund_change 参数来判断,fund_change 表示本次退款是否发生了资金变化,返回 Y 表示退款成功。

    可参考:如何判断退款成功

    如果fund_change不等于Y,程序会先将退款状态设置为REFUNDING(退款处理中),然后通过调用支付宝统一收单交易退款查询接口来查询退款状态

  • 步骤9-12:当退款状态为REFUNDING(退款处理中)时,系统会每隔10分钟定时查询一次退款结果(调用支付宝统一收单交易退款查询),然后根据查询的退款结果来判定退款是否成功

退款代码展示

申请退款代码展示

    /**
* 支付宝支付申请退款
* @param bill 申请退款参数
* @return
*/
public Map aliPayRefund(RefundBill bill) {
//退款结果
Map refundResult = new HashMap<>();
try {
//根据客户端类型创建AlipayClient实例(支付宝各个客户端配置都是一样的,此处默认用PC)
AlipayClient alipayClient = this.buildClient(ClientTypeEnum.PC);

//设置请求参数
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();

JSONObject bizContent = new JSONObject();
//设置支付宝交易号
bizContent.put("trade_no", bill.getReturnTradeNo());
//设置退款金额
bizContent.put("refund_amount", bill.getRefundPrice());
//设置退款请求号
bizContent.put("out_request_no", bill.getRefundSn());

//设置请求参数的集合(最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递)
request.setBizContent(bizContent.toString());

//调用支付宝统一收单交易退款接口
AlipayTradeRefundResponse response = alipayClient.certificateExecute(request);
this.logger.info("调用支付宝统一收单交易退款接口结果为:" + response.isSuccess());

if(response.isSuccess()){
/**
* 退款是否成功可以根据同步响应的 fund_change 参数来判断,fund_change 表示本次退款是否发生了资金变化,返回 Y 表示退款成功
* 参考文档:https://opendocs.alipay.com/support/01rawa
*/
this.logger.info("退款是否成功标识【fund_change】为:" + response.getFundChange());
if (response.getFundChange() != null && "Y".equals(response.getFundChange())) {
refundResult.put("refund_status", RefundStatusEnum.COMPLETED.value());
refundResult.put("refund_time", DateUtil.getDateline());
} else {
refundResult.put("refund_status", RefundStatusEnum.REFUNDING.value());
}
} else {
this.logger.error("调用支付宝统一收单交易退款接口失败,失败信息为:" + "code["+response.getCode()+"],msg["+response.getMsg()+"]," +
"sub_code["+response.getSubCode()+"],sub_msg["+response.getSubMsg()+"]");

refundResult.put("refund_status", RefundStatusEnum.REFUNDFAIL.value());
}
return refundResult;
} catch (Exception e) {
this.logger.error("支付宝申请退款失败", e);
}

refundResult.put("refund_status", RefundStatusEnum.REFUNDFAIL.value());
return refundResult;
}

退款结果查询代码展示

    /**
* 查询支付宝申请退款结果
*
* @param bill 申请退款参数
* @return
*/
public Map queryAliPayRefundResult(RefundBill bill) {
//退款结果
Map refundResult = new HashMap<>();

try {
//根据客户端类型创建AlipayClient实例(支付宝各个客户端配置都是一样的,此处默认用PC)
AlipayClient alipayClient = this.buildClient(ClientTypeEnum.PC);

//设置请求参数
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();

JSONObject bizContent = new JSONObject();
//设置支付宝交易号
bizContent.put("trade_no", bill.getReturnTradeNo());
//设置退款请求号
bizContent.put("out_request_no", bill.getRefundSn());
//设置退款执行成功的时间
JSONArray queryOptions = new JSONArray();
queryOptions.add("gmt_refund_pay");
bizContent.put("query_options", queryOptions);

//设置请求参数的集合(最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递)
request.setBizContent(bizContent.toString());

//调用支付宝统一收单交易退款查询接口
AlipayTradeFastpayRefundQueryResponse response = alipayClient.certificateExecute(request);
this.logger.info("调用支付宝统一收单交易退款查询接口返回结果:" + response.toString());

if (response.isSuccess()) {
//拼接退款查询次数缓存key,超过3次查询失败或者没有结果,直接判定当前退款失败
String cacheKey = CachePrefix.REFUND_RESULT_QUERY_TIMES.getPrefix() + bill.getReturnTradeNo() + "_" + bill.getRefundSn();
/**
* 根据官方文档对refund_status返回值的解释:
* 如果该字段不为空并且等于REFUND_SUCCESS时,证明退款处理成功;
* 如果未返回该字段表示退款请求未收到或者退款失败,由于无明确依据可以判断是退款失败还是接口调用失败,
* 因此我们设置3次查询机会,如果3次查询后refund_status还是不为REFUND_SUCCESS,我们直接就判定退款失败
*/
if (response.getRefundStatus() != null && REFUND_SUCCESS.equals(response.getRefundStatus())){
refundResult.put("refund_status", RefundStatusEnum.COMPLETED.value());
refundResult.put("refund_time", DateUtil.getDateline(response.getGmtRefundPay().toString(), "yyyy-MM-dd HH:mm:ss"));

//退款成功要删除缓存中的退款查询次数
cache.remove(cacheKey);
} else {
//获取缓存中存放的退款查询次数
Integer times = cache.get(cacheKey) == null ? 0 : StringUtil.toInt(cache.get(cacheKey), false);
times = times + 1;
if (times > 3) {
refundResult.put("refund_status", RefundStatusEnum.REFUNDFAIL.value());
//退款成功要删除缓存中的退款查询次数
cache.remove(cacheKey);
} else {
refundResult.put("refund_status", RefundStatusEnum.REFUNDING.value());
}
}
return refundResult;
}
} catch (Exception e) {
e.printStackTrace();
}

refundResult.put("refund_status", RefundStatusEnum.REFUNDING.value());
return refundResult;
}
tip

在查询退款结果时,根据官方文档对refund_status返回值的解释: 如果该字段不为空并且等于REFUND_SUCCESS时,证明退款处理成功; 如果未返回该字段表示退款请求未收到或者退款失败,由于无明确依据可以判断是退款失败还是接口调用失败,因此我们设置3次查询机会,如果3次查询后refund_status还是不为REFUND_SUCCESS,我们直接就判定退款失败