【同步】BOOT 和 CLOUD 的功能(PAY 相关功能)
This commit is contained in:
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.pay.config;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -15,7 +14,7 @@ import java.util.Map;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface PayClient {
|
||||
public interface PayClient<Config> {
|
||||
|
||||
/**
|
||||
* 获得渠道编号
|
||||
@@ -24,6 +23,13 @@ public interface PayClient {
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 获得渠道配置
|
||||
*
|
||||
* @return 渠道配置
|
||||
*/
|
||||
Config getConfig();
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
@@ -95,10 +101,9 @@ public interface PayClient {
|
||||
* 获得转账订单信息
|
||||
*
|
||||
* @param outTradeNo 外部订单号
|
||||
* @param type 转账类型
|
||||
* @return 转账信息
|
||||
*/
|
||||
PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
|
||||
PayTransferRespDTO getTransfer(String outTradeNo);
|
||||
|
||||
/**
|
||||
* 解析 transfer 回调数据
|
||||
|
||||
@@ -22,7 +22,6 @@ public class PayTransferRespDTO {
|
||||
|
||||
/**
|
||||
* 外部转账单号
|
||||
*
|
||||
*/
|
||||
private String outTransferNo;
|
||||
|
||||
@@ -50,11 +49,19 @@ public class PayTransferRespDTO {
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
/**
|
||||
* 渠道 package 信息
|
||||
*
|
||||
* 特殊:目前只有微信转账有这个东西!!!
|
||||
* @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012716430">JSAPI 调起用户确认收款</a>
|
||||
*/
|
||||
private String channelPackageInfo;
|
||||
|
||||
/**
|
||||
* 创建【WAITING】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO waitingOf(String channelTransferNo,
|
||||
String outTransferNo, Object rawData) {
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
@@ -66,10 +73,10 @@ public class PayTransferRespDTO {
|
||||
/**
|
||||
* 创建【IN_PROGRESS】状态的转账返回
|
||||
*/
|
||||
public static PayTransferRespDTO dealingOf(String channelTransferNo,
|
||||
String outTransferNo, Object rawData) {
|
||||
public static PayTransferRespDTO processingOf(String channelTransferNo,
|
||||
String outTransferNo, Object rawData) {
|
||||
PayTransferRespDTO respDTO = new PayTransferRespDTO();
|
||||
respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
|
||||
respDTO.status = PayTransferStatusRespEnum.PROCESSING.getStatus();
|
||||
respDTO.channelTransferNo = channelTransferNo;
|
||||
respDTO.outTransferNo = outTransferNo;
|
||||
respDTO.rawData = rawData;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
@@ -12,9 +9,6 @@ import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.Alipay;
|
||||
import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum.WxPay;
|
||||
|
||||
/**
|
||||
* 统一转账 Request DTO
|
||||
*
|
||||
@@ -23,21 +17,15 @@ import static cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferType
|
||||
@Data
|
||||
public class PayTransferUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 转账类型
|
||||
*
|
||||
* 关联 {@link PayTransferTypeEnum#getType()}
|
||||
*/
|
||||
@NotNull(message = "转账类型不能为空")
|
||||
@InEnum(PayTransferTypeEnum.class)
|
||||
private Integer type;
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotEmpty(message = "用户 IP 不能为空")
|
||||
private String userIp;
|
||||
|
||||
/**
|
||||
* 外部转账单编号
|
||||
*/
|
||||
@NotEmpty(message = "外部转账单编号不能为空")
|
||||
private String outTransferNo;
|
||||
|
||||
@@ -55,26 +43,23 @@ public class PayTransferUnifiedReqDTO {
|
||||
@Length(max = 128, message = "转账标题不能超过 128")
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 收款人账号
|
||||
*
|
||||
* 微信场景下:openid
|
||||
* 支付宝场景下:支付宝账号
|
||||
*/
|
||||
@NotEmpty(message = "收款人账号不能为空")
|
||||
private String userAccount;
|
||||
/**
|
||||
* 收款人姓名
|
||||
*/
|
||||
@NotBlank(message = "收款人姓名不能为空", groups = {Alipay.class})
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
* 支付宝登录号
|
||||
*/
|
||||
@NotBlank(message = "支付宝登录号不能为空", groups = {Alipay.class})
|
||||
private String alipayLogonId;
|
||||
|
||||
/**
|
||||
* 微信 openId
|
||||
*/
|
||||
@NotBlank(message = "微信 openId 不能为空", groups = {WxPay.class})
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
*
|
||||
* 微信支付:sceneId 和 scene_report_infos 字段,必须传递;参考 <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012711988#%EF%BC%883%EF%BC%89%E6%8C%89%E8%BD%AC%E8%B4%A6%E5%9C%BA%E6%99%AF%E6%8A%A5%E5%A4%87%E8%83%8C%E6%99%AF%E4%BF%A1%E6%81%AF">按转账场景报备背景信息</>
|
||||
*/
|
||||
private Map<String, String> channelExtras;
|
||||
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.dto.transfer;
|
||||
|
||||
import com.github.binarywang.wxpay.bean.notify.OriginNotifyResponse;
|
||||
import com.github.binarywang.wxpay.bean.notify.WxPayBaseNotifyV3Result;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
// TODO @luchi:这个可以复用 wxjava 里的类么?
|
||||
@NoArgsConstructor
|
||||
public class WxPayTransferPartnerNotifyV3Result implements Serializable, WxPayBaseNotifyV3Result<WxPayTransferPartnerNotifyV3Result.TransferNotifyResult> {
|
||||
|
||||
private static final long serialVersionUID = -1L;
|
||||
|
||||
/**
|
||||
* 源数据
|
||||
*/
|
||||
private OriginNotifyResponse rawData;
|
||||
|
||||
/**
|
||||
* 解密后的数据
|
||||
*/
|
||||
private TransferNotifyResult result;
|
||||
|
||||
@Override
|
||||
public void setRawData(OriginNotifyResponse rawData) {
|
||||
this.rawData = rawData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResult(TransferNotifyResult data) {
|
||||
this.result = data;
|
||||
}
|
||||
|
||||
public TransferNotifyResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public OriginNotifyResponse getRawData() {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class TransferNotifyResult implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/*********************** 公共字段 ********************
|
||||
|
||||
/**
|
||||
* 商家批次单号
|
||||
*/
|
||||
@SerializedName(value = "out_batch_no")
|
||||
protected String outBatchNo;
|
||||
|
||||
/**
|
||||
* 微信批次单号
|
||||
*/
|
||||
@SerializedName(value = "batch_id")
|
||||
protected String batchId;
|
||||
|
||||
/**
|
||||
* 批次状态
|
||||
*/
|
||||
@SerializedName(value = "batch_status")
|
||||
protected String batchStatus;
|
||||
|
||||
/**
|
||||
* 批次总笔数
|
||||
*/
|
||||
@SerializedName(value = "total_num")
|
||||
protected Integer totalNum;
|
||||
|
||||
/**
|
||||
* 批次总金额
|
||||
*/
|
||||
@SerializedName(value = "total_amount")
|
||||
protected Integer totalAmount;
|
||||
|
||||
/**
|
||||
* 批次更新时间
|
||||
*/
|
||||
@SerializedName(value = "update_time")
|
||||
private String updateTime;
|
||||
|
||||
/*********************** FINISHED ********************
|
||||
|
||||
/**
|
||||
* 转账成功金额
|
||||
*/
|
||||
@SerializedName(value = "success_amount")
|
||||
protected Integer successAmount;
|
||||
|
||||
/**
|
||||
* 转账成功笔数
|
||||
*/
|
||||
@SerializedName(value = "success_num")
|
||||
protected Integer successNum;
|
||||
|
||||
/**
|
||||
* 转账失败金额
|
||||
*/
|
||||
@SerializedName(value = "fail_amount")
|
||||
protected Integer failAmount;
|
||||
|
||||
/**
|
||||
* 转账失败笔数
|
||||
*/
|
||||
@SerializedName(value = "fail_num")
|
||||
protected Integer failNum;
|
||||
|
||||
/*********************** CLOSED ********************
|
||||
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
@SerializedName(value = "mchid")
|
||||
protected String mchId;
|
||||
|
||||
/**
|
||||
* 批次关闭原因
|
||||
*/
|
||||
@SerializedName(value = "close_reason")
|
||||
protected String closeReason;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,10 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReq
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
@@ -26,7 +23,7 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
|
||||
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient<Config> {
|
||||
|
||||
/**
|
||||
* 渠道编号
|
||||
@@ -77,6 +74,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
return channelId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
@@ -188,7 +190,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
|
||||
validatePayTransferReqDTO(reqDTO);
|
||||
PayTransferRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedTransfer(reqDTO);
|
||||
@@ -202,22 +203,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
private void validatePayTransferReqDTO(PayTransferUnifiedReqDTO reqDTO) {
|
||||
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
|
||||
switch (transferType) {
|
||||
case ALIPAY_BALANCE: {
|
||||
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.Alipay.class);
|
||||
break;
|
||||
}
|
||||
case WX_BALANCE: {
|
||||
ValidationUtils.validate(reqDTO, PayTransferTypeEnum.WxPay.class);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw exception(NOT_IMPLEMENTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
|
||||
@@ -236,14 +221,14 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
|
||||
public final PayTransferRespDTO getTransfer(String outTradeNo) {
|
||||
try {
|
||||
return doGetTransfer(outTradeNo, type);
|
||||
return doGetTransfer(outTradeNo);
|
||||
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
|
||||
getId(), outTradeNo, type, ex);
|
||||
log.error("[getTransfer][客户端({}) outTradeNo({}) 查询转账单异常]",
|
||||
getId(), outTradeNo, ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
}
|
||||
@@ -251,7 +236,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
|
||||
protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo)
|
||||
throws Throwable;
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
@@ -16,13 +16,14 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
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;
|
||||
@@ -30,6 +31,7 @@ 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;
|
||||
@@ -37,10 +39,8 @@ import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
|
||||
|
||||
/**
|
||||
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
|
||||
@@ -81,12 +81,11 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
@Override
|
||||
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws Throwable {
|
||||
// 1. 校验回调数据
|
||||
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
|
||||
StandardCharsets.UTF_8.name(), config.getSignType());
|
||||
verifyNotifyData(params);
|
||||
|
||||
// 2. 解析订单的状态
|
||||
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
|
||||
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||
Integer status = parseStatus(bodyObj.get("trade_status"));
|
||||
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
|
||||
if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
|
||||
@@ -220,11 +219,11 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 校验公钥类型 必须使用公钥证书模式
|
||||
if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
|
||||
throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
|
||||
}
|
||||
// 1.2 构建 AlipayFundTransUniTransferModel
|
||||
// 补充说明:https://opendocs.alipay.com/open/03dcrm?pathHash=4ba3b20b
|
||||
// 沙箱环境:可通过 公钥模式 或 公钥证书模式 加签进行调试
|
||||
// 生产环境:必须使用 公钥证书模式 加签请求强校验请求
|
||||
|
||||
// 1.1 构建 AlipayFundTransUniTransferModel
|
||||
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
|
||||
// ① 通用的参数
|
||||
model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额
|
||||
@@ -237,32 +236,21 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
}
|
||||
// ② 个性化的参数
|
||||
Participant payeeInfo = new Participant();
|
||||
PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
|
||||
switch (transferType) {
|
||||
// TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
|
||||
// @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
|
||||
case ALIPAY_BALANCE: {
|
||||
payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
|
||||
payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
|
||||
payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
|
||||
model.setPayeeInfo(payeeInfo);
|
||||
break;
|
||||
}
|
||||
case BANK_CARD: {
|
||||
payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
|
||||
// TODO 待实现
|
||||
throw exception(NOT_IMPLEMENTED);
|
||||
}
|
||||
default: {
|
||||
throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
|
||||
}
|
||||
}
|
||||
// 1.3 构建 AlipayFundTransUniTransferRequest
|
||||
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);
|
||||
// 执行请求
|
||||
AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
|
||||
// 处理结果
|
||||
|
||||
// 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 会轮询
|
||||
@@ -271,25 +259,24 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
|
||||
reqDTO.getOutTransferNo(), response);
|
||||
} else {
|
||||
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.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
|
||||
response.getOutBizNo(), 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, PayTransferTypeEnum type) throws Throwable {
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws Throwable {
|
||||
// 1.1 构建 AlipayFundTransCommonQueryModel
|
||||
AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
|
||||
model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
|
||||
model.setProductCode("TRANS_ACCOUNT_NO_PWD");
|
||||
model.setBizScene("DIRECT_TRANSFER"); //业务场景
|
||||
model.setOutBizNo(outTradeNo);
|
||||
// 1.2 构建 AlipayFundTransCommonQueryRequest
|
||||
@@ -303,18 +290,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
} else {
|
||||
response = client.execute(request);
|
||||
}
|
||||
// 2.2 处理返回结果
|
||||
if (response.isSuccess()) {
|
||||
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.dealingOf(response.getOrderId(), outTradeNo, response);
|
||||
}
|
||||
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
|
||||
response.getOutBizNo(), response);
|
||||
} else {
|
||||
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")) {
|
||||
@@ -323,12 +299,67 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
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 @chihuo:这里是不是也要实现,支付宝的。
|
||||
// TODO @芋艿:由于支付宝一直没触发回调,这个方法暂时没办法测试
|
||||
@Override
|
||||
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
|
||||
throw new UnsupportedOperationException("未实现");
|
||||
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers)
|
||||
throws Throwable {
|
||||
// 1. 校验回调数据
|
||||
verifyNotifyData(params);
|
||||
|
||||
// 2. 解析转账状态
|
||||
Map<String, String> 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<String, String> 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.getEncoded());
|
||||
verify = AlipaySignature.rsaCheckV1(params, publicKey,
|
||||
StandardCharsets.UTF_8.name(), config.getSignType());
|
||||
} else {
|
||||
throw new IllegalArgumentException("未知的公钥类型:" + config.getMode());
|
||||
}
|
||||
Assert.isTrue(verify, "验签结果不通过");
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
@@ -9,7 +9,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
@@ -78,7 +77,7 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo) {
|
||||
throw new UnsupportedOperationException("待实现");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
@@ -14,17 +15,15 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.WxPayTransferPartnerNotifyV3Result;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
|
||||
import com.github.binarywang.wxpay.bean.notify.*;
|
||||
import com.github.binarywang.wxpay.bean.request.*;
|
||||
import com.github.binarywang.wxpay.bean.result.*;
|
||||
import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.QueryTransferBatchesResult;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBatchesRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBatchesResult;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
|
||||
import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
|
||||
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
@@ -33,8 +32,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -72,6 +69,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
} else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
|
||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
||||
payConfig.setPublicKeyPath(FileUtils.createTempFile(config.getPublicKeyContent()).getPath());
|
||||
// 特殊:强制使用微信公用模式,避免灰度期间的问题!!!
|
||||
payConfig.setStrictlyNeedWechatPaySerial(true);
|
||||
}
|
||||
|
||||
// 创建 client 客户端
|
||||
@@ -88,12 +87,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
case API_VERSION_V3:
|
||||
// TODO @芋艿:【可能是 wxjava 的 bug】参考 https://github.com/binarywang/WxJava/issues/1557
|
||||
client.getConfig().setApiV3HttpClient(null);
|
||||
return doUnifiedOrderV3(reqDTO);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
log.error("[doUnifiedOrder][退款({}) 发起微信支付异常", reqDTO, e);
|
||||
log.error("[doUnifiedOrder][支付({}) 发起微信支付异常", reqDTO, e);
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
|
||||
@@ -225,6 +226,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
}
|
||||
|
||||
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
|
||||
fixV3HttpClientConnectionPoolShutDown();
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
|
||||
.setOutTradeNo(outTradeNo);
|
||||
@@ -297,6 +299,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
fixV3HttpClientConnectionPoolShutDown();
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundV3Request request = new WxPayRefundV3Request()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
@@ -356,34 +359,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V3:
|
||||
return parseTransferNotifyV3(body, headers);
|
||||
case API_VERSION_V2:
|
||||
throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayTransferRespDTO parseTransferNotifyV3(String body, Map<String, String> headers) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
SignatureHeader signatureHeader = getRequestHeader(headers);
|
||||
// TODO @luchi:这个可以复用 wxjava 里的类么?
|
||||
WxPayTransferPartnerNotifyV3Result response = client.baseParseOrderNotifyV3Result(body, signatureHeader, WxPayTransferPartnerNotifyV3Result.class, WxPayTransferPartnerNotifyV3Result.TransferNotifyResult.class);
|
||||
WxPayTransferPartnerNotifyV3Result.TransferNotifyResult result = response.getResult();
|
||||
// 2. 构建结果
|
||||
if (Objects.equals("FINISHED", result.getBatchStatus())) {
|
||||
if (result.getFailNum() <= 0) {
|
||||
return PayTransferRespDTO.successOf(result.getBatchId(), parseDateV3(result.getUpdateTime()),
|
||||
result.getOutBatchNo(), response);
|
||||
}
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(result.getBatchStatus(), result.getCloseReason(), result.getOutBatchNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
try {
|
||||
@@ -440,6 +415,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
}
|
||||
|
||||
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
|
||||
fixV3HttpClientConnectionPoolShutDown();
|
||||
// 1. 构建 WxPayRefundRequest 请求
|
||||
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
|
||||
request.setOutRefundNo(outRefundNo);
|
||||
@@ -463,53 +439,98 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 1. 构建 TransferBatchesRequest 请求
|
||||
List<TransferBatchesRequest.TransferDetail> transferDetailList = Collections.singletonList(
|
||||
TransferBatchesRequest.TransferDetail.newBuilder()
|
||||
.outDetailNo(reqDTO.getOutTransferNo())
|
||||
.transferAmount(reqDTO.getPrice())
|
||||
.transferRemark(reqDTO.getSubject())
|
||||
.openid(reqDTO.getOpenid())
|
||||
.build());
|
||||
// TODO @luchi:能不能我们搞个 TransferBatchesRequestX extends TransferBatchesRequest,这样更简洁一点。
|
||||
TransferBatchesRequest transferBatches = TransferBatchesRequest.newBuilder()
|
||||
fixV3HttpClientConnectionPoolShutDown();
|
||||
// 1. 构建 TransferBillsRequest 请求
|
||||
TransferBillsRequest request = TransferBillsRequest.newBuilder()
|
||||
.appid(this.config.getAppId())
|
||||
.outBatchNo(reqDTO.getOutTransferNo())
|
||||
.batchName(reqDTO.getSubject())
|
||||
.batchRemark(reqDTO.getSubject())
|
||||
.totalAmount(reqDTO.getPrice())
|
||||
.totalNum(transferDetailList.size())
|
||||
.transferDetailList(transferDetailList).build()
|
||||
.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
.outBillNo(reqDTO.getOutTransferNo())
|
||||
.transferAmount(reqDTO.getPrice())
|
||||
.transferRemark(reqDTO.getSubject())
|
||||
.transferSceneId(reqDTO.getChannelExtras().get("sceneId"))
|
||||
.openid(reqDTO.getUserAccount())
|
||||
.userName(reqDTO.getUserName())
|
||||
.transferSceneReportInfos(JsonUtils.parseArray(reqDTO.getChannelExtras().get("sceneReportInfos"),
|
||||
TransferBillsRequest.TransferSceneReportInfo.class))
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
.build();
|
||||
// 特殊:微信转账,必须 0.3 元起,才允许传入姓名
|
||||
if (reqDTO.getPrice() < 30) {
|
||||
request.setUserName(null);
|
||||
}
|
||||
|
||||
// 2.1 执行请求
|
||||
TransferBatchesResult transferBatchesResult = client.getTransferService().transferBatches(transferBatches);
|
||||
// 2.2 创建返回结果
|
||||
return PayTransferRespDTO.dealingOf(transferBatchesResult.getBatchId(), reqDTO.getOutTransferNo(), transferBatchesResult);
|
||||
try {
|
||||
TransferBillsResult response = client.getTransferService().transferBills(request);
|
||||
|
||||
// 2.2 创建返回结果
|
||||
String state = response.getState();
|
||||
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
|
||||
return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response)
|
||||
.setChannelPackageInfo(response.getPackageInfo()); // 一般情况下,只有 WAIT_USER_CONFIRM 会有!
|
||||
}
|
||||
if (Objects.equals("SUCCESS", state)) {
|
||||
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getCreateTime()),
|
||||
response.getOutBillNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(state, response.getFailReason(),
|
||||
response.getOutBillNo(), response);
|
||||
} catch (WxPayException e) {
|
||||
log.error("[doUnifiedTransfer][转账({}) 发起微信支付异常", reqDTO, e);
|
||||
String errorCode = getErrorCode(e);
|
||||
String errorMessage = getErrorMessage(e);
|
||||
return PayTransferRespDTO.closedOf(errorCode, errorMessage,
|
||||
reqDTO.getOutTransferNo(), e.getXmlString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws WxPayException {
|
||||
QueryTransferBatchesRequest request = QueryTransferBatchesRequest.newBuilder()
|
||||
.outBatchNo(outTradeNo).needQueryDetail(true).offset(0).limit(20).detailStatus("ALL")
|
||||
.build();
|
||||
QueryTransferBatchesResult response = client.getTransferService().transferBatchesOutBatchNo(request);
|
||||
QueryTransferBatchesResult.TransferBatch transferBatch = response.getTransferBatch();
|
||||
if (Objects.equals("FINISHED", transferBatch.getBatchStatus())) {
|
||||
// 明细中全部成功则成功,任一失败则失败
|
||||
if (response.getTransferDetailList().stream().allMatch(detail -> Objects.equals("SUCCESS", detail.getDetailStatus()))) {
|
||||
return PayTransferRespDTO.successOf(transferBatch.getBatchId(), parseDateV3(transferBatch.getUpdateTime()),
|
||||
transferBatch.getOutBatchNo(), response);
|
||||
}
|
||||
if (response.getTransferDetailList().stream().anyMatch(detail -> Objects.equals("FAIL", detail.getDetailStatus()))) {
|
||||
return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
|
||||
transferBatch.getOutBatchNo(), response);
|
||||
}
|
||||
protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws WxPayException {
|
||||
fixV3HttpClientConnectionPoolShutDown();
|
||||
// 1. 执行请求
|
||||
TransferBillsGetResult response = client.getTransferService().getBillsByOutBillNo(outTradeNo);
|
||||
|
||||
// 2. 创建返回结果
|
||||
String state = response.getState();
|
||||
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
|
||||
return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response);
|
||||
}
|
||||
if (Objects.equals("CLOSED", transferBatch.getBatchStatus())) {
|
||||
return PayTransferRespDTO.closedOf(transferBatch.getBatchStatus(), transferBatch.getCloseReason(),
|
||||
transferBatch.getOutBatchNo(), response);
|
||||
if (Objects.equals("SUCCESS", state)) {
|
||||
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getUpdateTime()),
|
||||
response.getOutBillNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.dealingOf(transferBatch.getBatchId(), transferBatch.getOutBatchNo(), response);
|
||||
return PayTransferRespDTO.closedOf(state, response.getFailReason(),
|
||||
response.getOutBillNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
|
||||
switch (config.getApiVersion()) {
|
||||
case API_VERSION_V3:
|
||||
return parseTransferNotifyV3(body, headers);
|
||||
case API_VERSION_V2:
|
||||
throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
private PayTransferRespDTO parseTransferNotifyV3(String body, Map<String, String> headers) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
SignatureHeader signatureHeader = getRequestHeader(headers);
|
||||
TransferBillsNotifyResult response = client.getTransferService().parseTransferBillsNotifyResult(body, signatureHeader);
|
||||
TransferBillsNotifyResult.DecryptNotifyResult result = response.getResult();
|
||||
|
||||
// 2. 创建返回结果
|
||||
String state = result.getState();
|
||||
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
|
||||
return PayTransferRespDTO.processingOf(result.getTransferBillNo(), result.getOutBillNo(), response);
|
||||
}
|
||||
if (Objects.equals("SUCCESS", state)) {
|
||||
return PayTransferRespDTO.successOf(result.getTransferBillNo(), parseDateV3(result.getUpdateTime()),
|
||||
result.getOutBillNo(), response);
|
||||
}
|
||||
return PayTransferRespDTO.closedOf(state, result.getFailReason(),
|
||||
result.getOutBillNo(), response);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
@@ -528,6 +549,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
.build();
|
||||
}
|
||||
|
||||
// TODO @芋艿:可能是 wxjava 的 bug:https://github.com/binarywang/WxJava/issues/1557
|
||||
private void fixV3HttpClientConnectionPoolShutDown() {
|
||||
client.getConfig().setApiV3HttpClient(null);
|
||||
}
|
||||
|
||||
static String formatDateV2(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||
@@ -28,10 +27,6 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
|
||||
@Slf4j
|
||||
public class WxPubPayClient extends AbstractWxPayClient {
|
||||
|
||||
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
|
||||
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
|
||||
}
|
||||
|
||||
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
|
||||
super(channelId, channelCode, config);
|
||||
}
|
||||
|
||||
@@ -15,18 +15,9 @@ import java.util.Objects;
|
||||
public enum PayTransferStatusRespEnum {
|
||||
|
||||
WAITING(0, "等待转账"),
|
||||
|
||||
/**
|
||||
* TODO 转账到银行卡. 会有T+0 T+1 到账的请情况。 还未实现
|
||||
* TODO @jason:可以看看其它开源项目,针对这个场景,处理策略是怎么样的?例如说,每天主动轮询?这个状态的单子?
|
||||
*/
|
||||
IN_PROGRESS(10, "转账进行中"),
|
||||
|
||||
SUCCESS(20, "转账成功"),
|
||||
/**
|
||||
* 转账关闭 (失败,或者其它情况)
|
||||
*/
|
||||
CLOSED(30, "转账关闭");
|
||||
PROCESSING(5, "转账进行中"),
|
||||
SUCCESS(10, "转账成功"),
|
||||
CLOSED(20, "转账关闭");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
@@ -39,7 +30,8 @@ public enum PayTransferStatusRespEnum {
|
||||
return Objects.equals(status, CLOSED.getStatus());
|
||||
}
|
||||
|
||||
public static boolean isInProgress(Integer status) {
|
||||
return Objects.equals(status, IN_PROGRESS.getStatus());
|
||||
public static boolean isProcessing(Integer status) {
|
||||
return Objects.equals(status, PROCESSING.getStatus());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.enums.transfer;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 转账类型枚举
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum PayTransferTypeEnum implements ArrayValuable<Integer> {
|
||||
|
||||
ALIPAY_BALANCE(1, "支付宝余额"),
|
||||
WX_BALANCE(2, "微信余额"),
|
||||
BANK_CARD(3, "银行卡"),
|
||||
WALLET_BALANCE(4, "钱包余额");
|
||||
|
||||
public interface WxPay {
|
||||
}
|
||||
|
||||
public interface Alipay {
|
||||
}
|
||||
|
||||
private final Integer type;
|
||||
private final String name;
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(PayTransferTypeEnum::getType).toArray(Integer[]::new);
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
public static PayTransferTypeEnum typeOf(Integer type) {
|
||||
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package com.github.binarywang.wxpay.bean.transfer;
|
||||
|
||||
import com.github.binarywang.wxpay.v3.SpecEncrypt;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 发起商家转账API参数
|
||||
*
|
||||
* @author zhongjun
|
||||
* created on 2022/6/17
|
||||
**/
|
||||
@Data
|
||||
@Builder(builderMethodName = "newBuilder")
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TransferBatchesRequest implements Serializable {
|
||||
private static final long serialVersionUID = -2175582517588397426L;
|
||||
|
||||
/**
|
||||
* 直连商户的appid
|
||||
*/
|
||||
@SerializedName("appid")
|
||||
private String appid;
|
||||
|
||||
/**
|
||||
* 商家批次单号
|
||||
*/
|
||||
@SerializedName("out_batch_no")
|
||||
private String outBatchNo;
|
||||
|
||||
/**
|
||||
* 批次名称
|
||||
*/
|
||||
@SerializedName("batch_name")
|
||||
private String batchName;
|
||||
|
||||
/**
|
||||
* 批次备注
|
||||
*/
|
||||
@SerializedName("batch_remark")
|
||||
private String batchRemark;
|
||||
|
||||
/**
|
||||
* 转账总金额
|
||||
*/
|
||||
@SerializedName("total_amount")
|
||||
private Integer totalAmount;
|
||||
|
||||
/**
|
||||
* 转账总笔数
|
||||
*/
|
||||
@SerializedName("total_num")
|
||||
private Integer totalNum;
|
||||
|
||||
/**
|
||||
* 转账明细列表
|
||||
*/
|
||||
@SpecEncrypt
|
||||
@SerializedName("transfer_detail_list")
|
||||
private List<TransferDetail> transferDetailList;
|
||||
|
||||
/**
|
||||
* 转账场景ID
|
||||
*/
|
||||
@SerializedName("transfer_scene_id")
|
||||
private String transferSceneId;
|
||||
|
||||
/**
|
||||
* 通知地址 说明:异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的url,必须为https,不能携带参数。
|
||||
*/
|
||||
@SerializedName("notify_url")
|
||||
private String notifyUrl;
|
||||
|
||||
@Data
|
||||
@Builder(builderMethodName = "newBuilder")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class TransferDetail {
|
||||
|
||||
/**
|
||||
* 商家明细单号
|
||||
*/
|
||||
@SerializedName("out_detail_no")
|
||||
private String outDetailNo;
|
||||
|
||||
/**
|
||||
* 转账金额
|
||||
*/
|
||||
@SerializedName("transfer_amount")
|
||||
private Integer transferAmount;
|
||||
|
||||
/**
|
||||
* 转账备注
|
||||
*/
|
||||
@SerializedName("transfer_remark")
|
||||
private String transferRemark;
|
||||
|
||||
/**
|
||||
* 用户在直连商户应用下的用户标示
|
||||
*/
|
||||
@SerializedName("openid")
|
||||
private String openid;
|
||||
|
||||
/**
|
||||
* 收款用户姓名
|
||||
*/
|
||||
@SpecEncrypt
|
||||
@SerializedName("user_name")
|
||||
private String userName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user