Date:2025-07-31

author:Duanyuqing
comment:汇付天下基本配置
main
duanyuqing 6 months ago
parent 1179de7cfd
commit b984f7f649

@ -30,31 +30,30 @@ public class HuifuClientConfig implements PayClientConfig {
*/ */
private String prodMode; private String prodMode;
//产品编号 //产品编号
@Value("${huifu.procutId}") @Value("${hake.huifu.procutId}")
private String procutId; private String procutId;
//系统编号 //系统编号
@Value("${huifu.sysId}") @Value("${hake.huifu.sysId}")
private String sysId; private String sysId;
//私钥 //私钥
@Value("${huifu.rsaPrivateKey}") @Value("${hake.huifu.rsaPrivateKey}")
private String rsaPrivateKey; private String rsaPrivateKey;
//公钥 //公钥
@Value("${huifu.rsaPublicKey}") @Value("${hake.huifu.rsaPublicKey}")
private String rsaPublicKey; private String rsaPublicKey;
//自定义超时时间 //自定义超时时间
@Value("${huifu.customConnectTimeout}") @Value("${hake.huifu.customConnectTimeout}")
private String customSocketTimeout; private String customSocketTimeout;
@Value("${huifu.customConnectTimeout}") @Value("${hake.huifu.customConnectTimeout}")
private String customConnectTimeout; private String customConnectTimeout;
@Value("${huifu.customConnectionRequestTimeout}") @Value("${hake.huifu.customConnectionRequestTimeout}")
private String customConnectionRequestTimeout; private String customConnectionRequestTimeout;

@ -1,56 +1,19 @@
package cn.iocoder.hake.module.pay.framework.pay.core.client.impl.huifu; package cn.iocoder.hake.module.pay.framework.pay.core.client.impl.huifu;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.hake.framework.common.util.io.FileUtils;
import cn.iocoder.hake.framework.common.util.json.JsonUtils;
import cn.iocoder.hake.framework.common.util.object.ObjectUtils;
import cn.iocoder.hake.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.hake.module.pay.framework.pay.config.HuifuClientConfig; import cn.iocoder.hake.module.pay.framework.pay.config.HuifuClientConfig;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.hake.module.pay.framework.pay.core.client.impl.AbstractPayClient;
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.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.impl.WxPayServiceImpl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.*;
import static cn.iocoder.hake.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
import static cn.iocoder.hake.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V3;
/** /**
* *
* *
* @author duanyuqing * @author duanyuqing
*/ */
@Slf4j @Slf4j
public abstract class HuifuPayClient extends AbstractPayClient<HuifuClientConfig> { public abstract class HuifuPayClient {
protected TradePaymentMicropayRequest client;
public HuifuPayClient(Long channelId, String channelCode, HuifuClientConfig config) { public HuifuPayClient(Long channelId, String channelCode, HuifuClientConfig config) {
super(channelId, channelCode, config);
} }
/** /**
@ -60,538 +23,8 @@ public abstract class HuifuPayClient extends AbstractPayClient<HuifuClientConfig
*/ */
protected void doInit(String tradeType) { protected void doInit(String tradeType) {
// 创建 config 配置 // 创建 config 配置
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "publicKeyContent");
payConfig.setTradeType(tradeType);
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
} 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 客户端
client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
// ============ 支付相关 ==========
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
try {
switch (config.getApiVersion()) {
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);
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
reqDTO.getOutTradeNo(), e.getXmlString());
}
}
/**
* V2
*
* @param reqDTO
* @return
*/
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
throws Exception;
/**
* V3
*
* @param reqDTO
* @return
*/
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException;
/**
* V2
*
* @param reqDTO
* @return
*/
protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
return WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
.build();
}
/**
* V3
*
* @param reqDTO
* @return
*/
protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getSubject());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
return request;
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseOrderNotifyV2(body);
case API_VERSION_V3:
return doParseOrderNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusEnum.SUCCESS.getStatus() : PayOrderStatusEnum.CLOSED.getStatus();
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), body);
}
private PayOrderRespDTO doParseOrderNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader);
WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
Integer status = parseStatus(result.getTradeState());
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
result.getOutTradeNo(), body);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doGetOrderV2(outTradeNo);
case API_VERSION_V3:
return doGetOrderV3(outTradeNo);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
outTradeNo, e.getXmlString());
}
throw e;
}
}
private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
.outTradeNo(outTradeNo).build();
// 执行请求
WxPayOrderQueryResult response = client.queryOrder(request);
// 转换结果
Integer status = parseStatus(response.getTradeState());
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
outTradeNo, response);
}
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
.setOutTradeNo(outTradeNo);
// 执行请求
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
// 转换结果
Integer status = parseStatus(response.getTradeState());
String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
outTradeNo, response);
}
private static Integer parseStatus(String tradeState) {
switch (tradeState) {
case "NOTPAY":
case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
return PayOrderStatusEnum.WAITING.getStatus();
case "SUCCESS":
return PayOrderStatusEnum.SUCCESS.getStatus();
case "REFUND":
return PayOrderStatusEnum.REFUND.getStatus();
case "CLOSED":
case "REVOKED": // 已撤销(刷卡支付独有)
case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
return PayOrderStatusEnum.CLOSED.getStatus();
default:
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
}
}
// ============ 退款相关 ==========
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doUnifiedRefundV2(reqDTO);
case API_VERSION_V3:
return doUnifiedRefundV3(reqDTO);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
reqDTO.getOutRefundNo(), e.getXmlString());
}
}
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundRequest request = new WxPayRefundRequest()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRefundFee(reqDTO.getRefundPrice())
.setRefundDesc(reqDTO.getReason())
.setTotalFee(reqDTO.getPayPrice())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundResult response = client.refundV2(request);
// 2.2 创建返回结果
if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
return PayRefundRespDTO.waitingOf(response.getRefundId(),
reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
}
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
.setReason(reqDTO.getReason())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundV3Result response = client.refundV3(request);
// 2.2 创建返回结果
if (Objects.equals("SUCCESS", response.getStatus())) {
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
reqDTO.getOutRefundNo(), response);
}
if (Objects.equals("PROCESSING", response.getStatus())) {
return PayRefundRespDTO.waitingOf(response.getRefundId(),
reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
}
@Override
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseRefundNotifyV2(body);
case API_VERSION_V3:
return parseRefundNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
result.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
private PayRefundRespDTO parseRefundNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader);
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
result.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doGetRefundV2(outTradeNo, outRefundNo);
case API_VERSION_V3:
return doGetRefundV3(outTradeNo, outRefundNo);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
outRefundNo, e.getXmlString());
}
throw e;
}
}
private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
.outTradeNo(outTradeNo)
.outRefundNo(outRefundNo)
.build();
// 2.1 执行请求
WxPayRefundQueryResult response = client.refundQuery(request);
// 2.2 创建返回结果
if (!Objects.equals("SUCCESS", response.getResultCode())) {
return PayRefundRespDTO.waitingOf(null,
outRefundNo, response);
}
WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
record -> record.getOutRefundNo().equals(outRefundNo));
if (refund == null) {
return PayRefundRespDTO.failureOf(outRefundNo, response);
}
switch (refund.getRefundStatus()) {
case "SUCCESS":
return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
outRefundNo, response);
case "PROCESSING":
return PayRefundRespDTO.waitingOf(refund.getRefundId(),
outRefundNo, response);
case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
case "FAIL":
return PayRefundRespDTO.failureOf(outRefundNo, response);
default:
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
}
}
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
request.setOutRefundNo(outRefundNo);
// 2.1 执行请求
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
// 2.2 创建返回结果
switch (response.getStatus()) {
case "SUCCESS":
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
outRefundNo, response);
case "PROCESSING":
return PayRefundRespDTO.waitingOf(response.getRefundId(),
outRefundNo, response);
case "ABNORMAL": // 退款异常
case "CLOSED":
return PayRefundRespDTO.failureOf(outRefundNo, response);
default:
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
}
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 TransferBillsRequest 请求
TransferBillsRequest request = TransferBillsRequest.newBuilder()
.appid(this.config.getAppId())
.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 执行请求
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) 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("SUCCESS", state)) {
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getUpdateTime()),
response.getOutBillNo(), 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);
}
// ========== 各种工具方法 ==========
/**
*
*
* @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221"></a>
*/
private SignatureHeader getRequestHeader(Map<String, String> headers) {
return SignatureHeader.builder()
.signature(headers.get("wechatpay-signature"))
.nonce(headers.get("wechatpay-nonce"))
.serial(headers.get("wechatpay-serial"))
.timeStamp(headers.get("wechatpay-timestamp"))
.build();
}
// TODO @哈客:可能是 wxjava 的 bughttps://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);
} }
static LocalDateTime parseDateV2(String time) {
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
}
static LocalDateTime parseDateV2B(String time) {
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
}
static String formatDateV3(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
}
static LocalDateTime parseDateV3(String time) {
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
}
static String getErrorCode(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCode();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return "CUSTOM_ERROR";
}
return e.getReturnCode();
}
static String getErrorMessage(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCodeDes();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return e.getCustomErrorMsg();
}
return e.getReturnMsg();
}
} }

@ -116,3 +116,11 @@ hake:
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址 transfer-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
demo: false # 关闭演示模式 demo: false # 关闭演示模式
huifu:
procutId: PAYUN
sysId: 6666000162367855
rsaPrivateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCOKQUkTWNZ1lug2yLa+6ESp+XDBN5roQMLQVZPbn7T5Jwo4yK3R8zNjSB8KSLaG23Dkr3xISf5HvFVOmoEtpQrCqzbDrU7hmR00ita/wQIRluh2RAT+RsUHDwCeXPOk3jkugKQtXvhQExxn61LIXAArB07MMkZgl5/HXcEQcLzukpSnQ55WIA1+HFPL20x8hHYEIILtI3zibFJtAoig0VeZPVUDTKbOEILkRriSJ7WQjMlsKw1w/TaFAXrCnQO7GlY8pVxkzW46WDt5xT9m26KNiXcQeS5Je215pgBnFidMAOQLxIlyDyh9/PTe2r0ygoGH52J5HyKHYenLlhv/DyBAgMBAAECggEBAIaxGuwQXsepr9syhU3SCATzC2DBZjO3tHifiTVtTcFZ4xNiUWwyHTvMMTEykJDyWAdrK4ghkAwbYzELTZP1oWE+lhRfVRt29AszblyjLqDgeMVaMj+aUCu3rKvzguQBGhQsoW2WZi8/iq5FSh3bKpGYgYGpcYA342yw8CkaXaoqN8jzH3ohEv9KX3cHIfCAeEg0zNH4+pXDhOjwXc/XIjQ5CoIduYey2JsHt/MyTR/5SrguMcr0VZLqYqJDy5Dd81kL46YlBACq5p1u7effgmVqb64MPGVaVU0FdUFv3CX4cLtEEEG63X1aijxPnKGyy5XEM+k/DFbw25xBBM1UJKECgYEAxCt21DWtS+1g6VVJA+qbpBMSzfA/xblvXyz5owCMmopte3WnzLPlkHdgTxlZSeRHursOPFd3v8hOmovFNw+OBS8Fhdy0b7tLwmb55CEOX7MsLJ5pqsUE+5IjGAVGJNHRlxTtDDXA8fILpeRJouAtAcrheyjmqWz8tsQ1okgWL8UCgYEAuYSZzcwKNHjBwOoMsxxmuh7hM+CXiO5TXnhuFkrVOR23147spKvUaV97z2SHEtbj13k70BTCVMCAyQOrMgL/ro0XHl2dK8zKPSuZvYC23r+AsE69edeJz2uYjDOxXfJJFNL4jqRDfNG/Cbk6VimkVzaeKdfnsf3MYQ5CFZ8eCY0CgYBpDsXy3FRU52oRTEVwPYLhGf3mIJZms+q7VADVlQO3+A6uIdKdxHJbLjN76R1yfzkS/f6fvlA5e3LtPZF+7Wunxwj0KcDQXcQy9qc5z6I9Cl3L/4KjnCQQ/rCguqJYMa5HdUOGWHtel7w5Octd2SUBYr/jD4KIlf+5edcnc+e96QKBgHZtO3GwSuNsIuNvhWPhQYKWq9ReDt4OpZGs9zmr06l+Wxlz14TXW+VYWsTtu3w/SXsHnTMbzWIk8RFhEiv+1hEraBKuV+LZ/FBIQQBD5nkTbqcd3L6m5QZP/TWi2hrKy/RLKPiFy78mdflTEPZn5sz1xMmZVgK9rXZXj8AVrysRAoGBAJJfMtoeNO8bj0TuvC/u+OFUsazjBziTrFRAlrVU6pUkZ+P8huKindNnQSzkH92JpdoPgPPzRsw8yFai3wgRPrExsSosELulEAZMZNnPEn7BxiTQjB7wcCSnRXAmW6g5wUjHhu1wAhGJpV1RSCVMgsfNQy6NOZizQahc6u4Qb77/
rsaPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAik1dhiQNVn69cTq3WhhUxKKDwRQtdKbbAzRTTvkwcYH2J07iG0EmRZTqmZKTvNqGKizQbz9eNrMh7Awn5B+t8aTc+xe3amj7bngf0zk2jAwK36Hrokv5oOCAiyAWSEt9/M6gWUf0R+av5JS34XZmfDVyhcu2+3PdWBaqKlOx0HX9TphuDJDHqWP13+It5X2Iv1kkiaqcY6yjhBF8RlP1vD+Y77W4p+h9QHIAVCJGykjBDBf51pxHMrOuj1yFOpWCX4UlYQISPfdlgu+FARW38mjqV/J2QwITcseg1sb1aRvA2Bve+zdcQTAwNpUwOnqsfov+p7Vh0+VXUt1vyk0abQIDAQAB
customSocketTimeout: 20000
customConnectTimeout: 20000
customConnectionRequestTimeout: 20000

@ -138,4 +138,12 @@ hake:
pay: pay:
order-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 order-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 refund-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
transfer-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址 transfer-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
huifu:
procutId: PAYUN
sysId: 6666000162367855
rsaPrivateKey: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCOKQUkTWNZ1lug2yLa+6ESp+XDBN5roQMLQVZPbn7T5Jwo4yK3R8zNjSB8KSLaG23Dkr3xISf5HvFVOmoEtpQrCqzbDrU7hmR00ita/wQIRluh2RAT+RsUHDwCeXPOk3jkugKQtXvhQExxn61LIXAArB07MMkZgl5/HXcEQcLzukpSnQ55WIA1+HFPL20x8hHYEIILtI3zibFJtAoig0VeZPVUDTKbOEILkRriSJ7WQjMlsKw1w/TaFAXrCnQO7GlY8pVxkzW46WDt5xT9m26KNiXcQeS5Je215pgBnFidMAOQLxIlyDyh9/PTe2r0ygoGH52J5HyKHYenLlhv/DyBAgMBAAECggEBAIaxGuwQXsepr9syhU3SCATzC2DBZjO3tHifiTVtTcFZ4xNiUWwyHTvMMTEykJDyWAdrK4ghkAwbYzELTZP1oWE+lhRfVRt29AszblyjLqDgeMVaMj+aUCu3rKvzguQBGhQsoW2WZi8/iq5FSh3bKpGYgYGpcYA342yw8CkaXaoqN8jzH3ohEv9KX3cHIfCAeEg0zNH4+pXDhOjwXc/XIjQ5CoIduYey2JsHt/MyTR/5SrguMcr0VZLqYqJDy5Dd81kL46YlBACq5p1u7effgmVqb64MPGVaVU0FdUFv3CX4cLtEEEG63X1aijxPnKGyy5XEM+k/DFbw25xBBM1UJKECgYEAxCt21DWtS+1g6VVJA+qbpBMSzfA/xblvXyz5owCMmopte3WnzLPlkHdgTxlZSeRHursOPFd3v8hOmovFNw+OBS8Fhdy0b7tLwmb55CEOX7MsLJ5pqsUE+5IjGAVGJNHRlxTtDDXA8fILpeRJouAtAcrheyjmqWz8tsQ1okgWL8UCgYEAuYSZzcwKNHjBwOoMsxxmuh7hM+CXiO5TXnhuFkrVOR23147spKvUaV97z2SHEtbj13k70BTCVMCAyQOrMgL/ro0XHl2dK8zKPSuZvYC23r+AsE69edeJz2uYjDOxXfJJFNL4jqRDfNG/Cbk6VimkVzaeKdfnsf3MYQ5CFZ8eCY0CgYBpDsXy3FRU52oRTEVwPYLhGf3mIJZms+q7VADVlQO3+A6uIdKdxHJbLjN76R1yfzkS/f6fvlA5e3LtPZF+7Wunxwj0KcDQXcQy9qc5z6I9Cl3L/4KjnCQQ/rCguqJYMa5HdUOGWHtel7w5Octd2SUBYr/jD4KIlf+5edcnc+e96QKBgHZtO3GwSuNsIuNvhWPhQYKWq9ReDt4OpZGs9zmr06l+Wxlz14TXW+VYWsTtu3w/SXsHnTMbzWIk8RFhEiv+1hEraBKuV+LZ/FBIQQBD5nkTbqcd3L6m5QZP/TWi2hrKy/RLKPiFy78mdflTEPZn5sz1xMmZVgK9rXZXj8AVrysRAoGBAJJfMtoeNO8bj0TuvC/u+OFUsazjBziTrFRAlrVU6pUkZ+P8huKindNnQSzkH92JpdoPgPPzRsw8yFai3wgRPrExsSosELulEAZMZNnPEn7BxiTQjB7wcCSnRXAmW6g5wUjHhu1wAhGJpV1RSCVMgsfNQy6NOZizQahc6u4Qb77/
rsaPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAik1dhiQNVn69cTq3WhhUxKKDwRQtdKbbAzRTTvkwcYH2J07iG0EmRZTqmZKTvNqGKizQbz9eNrMh7Awn5B+t8aTc+xe3amj7bngf0zk2jAwK36Hrokv5oOCAiyAWSEt9/M6gWUf0R+av5JS34XZmfDVyhcu2+3PdWBaqKlOx0HX9TphuDJDHqWP13+It5X2Iv1kkiaqcY6yjhBF8RlP1vD+Y77W4p+h9QHIAVCJGykjBDBf51pxHMrOuj1yFOpWCX4UlYQISPfdlgu+FARW38mjqV/J2QwITcseg1sb1aRvA2Bve+zdcQTAwNpUwOnqsfov+p7Vh0+VXUt1vyk0abQIDAQAB
customSocketTimeout: 20000
customConnectTimeout: 20000
customConnectionRequestTimeout: 20000
Loading…
Cancel
Save