package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO; import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.AbstractPayClient; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayConfig; import com.alipay.api.AlipayResponse; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.domain.*; import com.alipay.api.internal.util.AlipaySignature; import com.alipay.api.internal.util.AntCertificationUtil; import com.alipay.api.internal.util.codec.Base64; import com.alipay.api.request.*; import com.alipay.api.response.*; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.time.LocalDateTime; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE; import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY; /** * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) * * @author jason */ @Slf4j public abstract class AbstractAlipayPayClient extends AbstractPayClient { @Getter // 仅用于单测场景 protected DefaultAlipayClient client; public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { super(channelId, channelCode, config); } @Override @SneakyThrows protected void doInit() { AlipayConfig alipayConfig = new AlipayConfig(); BeanUtil.copyProperties(config, alipayConfig, false); this.client = new DefaultAlipayClient(alipayConfig); } // ============ 支付相关 ========== /** * 构造支付关闭的 {@link PayOrderRespDTO} 对象 * * @return 支付关闭的 {@link PayOrderRespDTO} 对象 */ protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) { Assert.isFalse(response.isSuccess()); return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutTradeNo(), response); } @Override public PayOrderRespDTO doParseOrderNotify(Map params, String body, Map headers) throws Throwable { // 1. 校验回调数据 verifyNotifyData(params); // 2. 解析订单的状态 // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); Integer status = parseStatus(bodyObj.get("trade_status")); // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功 if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) { status = PayOrderStatusEnum.REFUND.getStatus(); } Assert.notNull(status, (Supplier) () -> { throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); }); return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), bodyObj.get("out_trade_no"), body); } @Override protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { // 1.1 构建 AlipayTradeRefundModel 请求 AlipayTradeQueryModel model = new AlipayTradeQueryModel(); model.setOutTradeNo(outTradeNo); // 1.2 构建 AlipayTradeQueryRequest 请求 AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); request.setBizModel(model); AlipayTradeQueryResponse response; if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 response = client.certificateExecute(request); } else { response = client.execute(request); } if (!response.isSuccess()) { // 不成功,例如说订单不存在 return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), outTradeNo, response); } // 2.2 解析订单的状态 Integer status = parseStatus(response.getTradeStatus()); Assert.notNull(status, () -> { throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); }); return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), outTradeNo, response); } private static Integer parseStatus(String tradeStatus) { return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusEnum.WAITING.getStatus() : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusEnum.SUCCESS.getStatus() : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusEnum.CLOSED.getStatus() : null; } // ============ 退款相关 ========== /** * 支付宝统一的退款接口 alipay.trade.refund * * @param reqDTO 退款请求 request DTO * @return 退款请求 Response */ @Override protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException { // 1.1 构建 AlipayTradeRefundModel 请求 AlipayTradeRefundModel model = new AlipayTradeRefundModel(); model.setOutTradeNo(reqDTO.getOutTradeNo()); model.setOutRequestNo(reqDTO.getOutRefundNo()); model.setRefundAmount(formatAmount(reqDTO.getRefundPrice())); model.setRefundReason(reqDTO.getReason()); // 1.2 构建 AlipayTradePayRequest 请求 AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); request.setBizModel(model); // 2.1 执行请求 AlipayTradeRefundResponse response; if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 response = client.certificateExecute(request); } else { response = client.execute(request); } if (!response.isSuccess()) { // 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询 if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) { return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response); } return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response); } // 2.2 创建返回结果 // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。 // 另外,支付宝没有退款单号,所以不用设置 return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), reqDTO.getOutRefundNo(), response); } @Override public PayRefundRespDTO doParseRefundNotify(Map params, String body, Map headers) { // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 throw new UnsupportedOperationException("支付宝无退款回调"); } @Override protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException { // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求 AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); model.setOutTradeNo(outTradeNo); model.setOutRequestNo(outRefundNo); model.setQueryOptions(Collections.singletonList("gmt_refund_pay")); // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求 AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); request.setBizModel(model); // 2.1 执行请求 AlipayTradeFastpayRefundQueryResponse response; if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 response = client.certificateExecute(request); } else { response = client.execute(request); } if (!response.isSuccess()) { // 明确不存在的情况,应该就是失败,可进行关闭 if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) { return PayRefundRespDTO.failureOf(outRefundNo, response); } // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待 return PayRefundRespDTO.waitingOf(null, outRefundNo, response); } // 2.2 创建返回结果 if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) { return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), outRefundNo, response); } return PayRefundRespDTO.waitingOf(null, outRefundNo, response); } @Override protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException { // 补充说明:https://opendocs.alipay.com/open/03dcrm?pathHash=4ba3b20b // 沙箱环境:可通过 公钥模式 或 公钥证书模式 加签进行调试 // 生产环境:必须使用 公钥证书模式 加签请求强校验请求 // 1.1 构建 AlipayFundTransUniTransferModel AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel(); // ① 通用的参数 model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额 model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。 model.setOutBizNo(reqDTO.getOutTransferNo()); model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER if (reqDTO.getChannelExtras() != null) { model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras())); } String sceneName = MapUtil.getStr(reqDTO.getChannelExtras(), "sceneName"); if (StrUtil.isNotBlank(sceneName)) { model.setTransferSceneName(sceneName); } // ② 个性化的参数 Participant payeeInfo = new Participant(); payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); // 暂时只考虑转账到支付宝,银行没有权限 https://opendocs.alipay.com/open/02byvc?scene=66dd06f5a923403393b85de68d3c0055 payeeInfo.setIdentity(reqDTO.getUserAccount()); // 支付宝登录号 payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名 model.setPayeeInfo(payeeInfo); // 1.2 构建 AlipayFundTransUniTransferRequest AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); request.setBizModel(model); // 2.1 执行请求 AlipayFundTransUniTransferResponse response; if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 response = client.certificateExecute(request); } else { response = client.execute(request); } if (!response.isSuccess()) { // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账 // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询 if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response); } return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutTransferNo(), response); } // 2.2 处理结果 if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutTransferNo(), response); } if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 return PayTransferRespDTO.processingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response); } return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()), response.getOutBizNo(), response); } @Override protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws Throwable { // 1.1 构建 AlipayFundTransCommonQueryModel AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel(); model.setProductCode("TRANS_ACCOUNT_NO_PWD"); model.setBizScene("DIRECT_TRANSFER"); //业务场景 model.setOutBizNo(outTradeNo); // 1.2 构建 AlipayFundTransCommonQueryRequest AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest(); request.setBizModel(model); // 2.1 执行请求 AlipayFundTransCommonQueryResponse response; if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式 response = client.certificateExecute(request); } else { response = client.execute(request); } if (!response.isSuccess()) { // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账 if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) { return PayTransferRespDTO.waitingOf(null, outTradeNo, response); } return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), outTradeNo, response); } // 2.2 处理返回结果 if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL" return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), outTradeNo, response); } if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中 return PayTransferRespDTO.processingOf(response.getOrderId(), outTradeNo, response); } return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()), response.getOutBizNo(), response); } // TODO @芋艿:由于支付宝一直没触发回调,这个方法暂时没办法测试 @Override protected PayTransferRespDTO doParseTransferNotify(Map params, String body, Map headers) throws Throwable { // 1. 校验回调数据 verifyNotifyData(params); // 2. 解析转账状态 Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); String status = bodyObj.get("status"); String outBizNo = bodyObj.get("out_biz_no"); String orderId = bodyObj.get("order_id"); String payDate = bodyObj.get("pay_date"); // 3. 根据状态返回对应的结果 if (Objects.equals(status, "SUCCESS")) { return PayTransferRespDTO.successOf(orderId, parseTime(payDate), outBizNo, bodyObj); } if (Objects.equals(status, "DEALING")) { return PayTransferRespDTO.processingOf(orderId, outBizNo, bodyObj); } if (ObjectUtils.equalsAny(status, "REFUND", "FAIL")) { return PayTransferRespDTO.closedOf(bodyObj.get("sub_code"), bodyObj.get("sub_msg"), outBizNo, bodyObj); } return PayTransferRespDTO.waitingOf(orderId, outBizNo, bodyObj); } /** * 校验回调数据 * * @param params 回调参数 * @throws Throwable 验签失败时抛出异常 */ protected void verifyNotifyData(Map params) throws Throwable { boolean verify; if (Objects.equals(config.getMode(), MODE_PUBLIC_KEY)) { verify = AlipaySignature.rsaCheckV1(params, config.getAlipayPublicKey(), StandardCharsets.UTF_8.name(), config.getSignType()); } else if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 由于 rsaCertCheckV1 的第二个参数是 path,所以不能这么调用!!!通过阅读源码,发现可以采用如下方式! X509Certificate cert = AntCertificationUtil.getCertFromContent(config.getAlipayPublicCertContent()); String publicKey = Base64.encodeBase64String(cert.getPublicKey().getEncoded()); verify = AlipaySignature.rsaCheckV1(params, publicKey, StandardCharsets.UTF_8.name(), config.getSignType()); } else { throw new IllegalArgumentException("未知的公钥类型:" + config.getMode()); } Assert.isTrue(verify, "验签结果不通过"); } // ========== 各种工具方法 ========== protected String formatAmount(Integer amount) { return String.valueOf(amount / 100.0); } protected String formatTime(LocalDateTime time) { return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER); } protected LocalDateTime parseTime(String str) { return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); } }