mall + pay:

1. 拆分支付回调、退款回调的 URL
2. 修复微信支付回调的时间解析错误
This commit is contained in:
YunaiV
2023-07-17 11:47:45 +08:00
parent 66ed61c641
commit 68a4ef98ca
14 changed files with 166 additions and 76 deletions

View File

@@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty;
public class PayProperties {
/**
* 回调地址
* 支付回调地址
*
* 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL
* 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL
*
* 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
*/
@NotEmpty(message = "回调地址不能为空")
@URL(message = "回调地址的格式必须是 URL")
private String callbackUrl;
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String orderNotifyUrl;
/**
* 退款回调地址
*
* 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
*
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String refundNotifyUrl;
}

View File

@@ -22,6 +22,8 @@ public interface PayClient {
*/
Long getId();
// ============ 支付相关 ==========
/**
* 调用支付渠道,统一下单
*
@@ -30,6 +32,17 @@ public interface PayClient {
*/
PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析 order 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
// ============ 退款相关 ==========
/**
* 调用支付渠道,进行退款
*
@@ -39,16 +52,12 @@ public interface PayClient {
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析回调数据
* 解析 refund 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 回调对象
* 1. {@link PayRefundRespDTO} 退款通知
* 2. {@link PayOrderRespDTO} 支付通知
* @return 支付订单信息
*/
default Object parseNotify(Map<String, String> params, String body) {
throw new UnsupportedOperationException("未实现 parseNotify 方法!");
}
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
}

View File

@@ -97,13 +97,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
@Override
@SneakyThrows
public Object parseNotify(Map<String, String> params, String body) {
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
// ② 全部退款Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
// 1. 校验回调数据
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
@@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
.build();
}
@Override
public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
// ② 全部退款Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
throw new UnsupportedOperationException("支付宝无退款回调");
}
// ========== 各种工具方法 ==========
protected String formatAmount(Integer amount) {

View File

@@ -14,9 +14,11 @@ 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.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
@@ -30,8 +32,7 @@ import java.time.ZoneId;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
import static cn.hutool.core.date.DatePattern.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@@ -61,9 +62,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(tradeType);
// if (WxPayClientConfig.API_VERSION_V2.equals(config.getApiVersion())) {
// payConfig.setSignType(WxPayConstants.SignType.MD5);
// }
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if (Base64.isBase64(config.getKeyContent())) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
@@ -162,8 +160,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
@Override
public Object parseNotify(Map<String, String> params, String body) {
log.info("[parseNotify][微信支付回调 data 数据: {}]", body);
public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
@@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
}
private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(body);
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderRespDTO
.builder()
.outTradeNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId())
.channelUserId(notifyResult.getOpenid())
.successTime(parseDateV2(notifyResult.getTimeEnd()))
// 1. 解析回调
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
return PayOrderRespDTO.builder()
.outTradeNo(response.getOutTradeNo())
.channelOrderNo(response.getTransactionId())
.channelUserId(response.getOpenid())
.status(Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus())
.successTime(parseDateV2(response.getTimeEnd()))
.rawData(response)
.build();
}
private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException {
WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
// TODO 芋艿:翻译下 state
// 转换结果
Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
"支付结果非 SUCCESS");
@@ -209,6 +209,49 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
.build();
}
@Override
public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case API_VERSION_V2:
return parseRefundNotifyV2(body);
case WxPayClientConfig.API_VERSION_V3:
return parseRefundNotifyV3(body);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
// throw buildPayException(e);
throw new RuntimeException(e);
// TODO 芋艿:缺一个异常翻译
}
}
private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
WxPayRefundNotifyResult.ReqInfo reqInfo = response.getReqInfo();
// 2. 构建结果
PayRefundRespDTO notify = new PayRefundRespDTO()
.setChannelRefundNo(reqInfo.getRefundId())
.setOutRefundNo(reqInfo.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", reqInfo.getRefundStatus())) {
notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(parseDateV2B(reqInfo.getSuccessTime()));
} else {
notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
return notify;
}
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
// TODO 芋艿:未实现
return null;
}
// ========== 各种工具方法 ==========
/**
@@ -246,6 +289,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
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);
}