mall + pay:

1. 重构支付回调的逻辑,将回调解析改成 PayOrderRespDTO,为后续轮询做铺垫
2. 调整退款单的表结构
3. 调整退款调用的实现
This commit is contained in:
YunaiV 2023-07-15 10:35:41 +08:00
parent fbb63ee262
commit 518e89dc4b
46 changed files with 436 additions and 499 deletions

View File

@ -1,10 +1,12 @@
package cn.iocoder.yudao.framework.common.util.validation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
import java.util.regex.Pattern;
@ -37,6 +39,12 @@ public class ValidationUtils {
&& PATTERN_XML_NCNAME.matcher(str).matches();
}
public static void validate(Object object, Class<?>... groups) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Assert.notNull(validator);
validate(validator, object, groups);
}
public static void validate(Validator validator, Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (CollUtil.isNotEmpty(constraintViolations)) {

View File

@ -1,12 +1,12 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
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.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
import java.util.Map;
/**
* 支付客户端用于对接各支付渠道的 SDK实现发起支付退款等功能
@ -32,20 +32,22 @@ public interface PayClient {
/**
* 调用支付渠道进行退款
*
* @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果
* @return 退款信息
*/
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析回调数据
*
* @param rawNotify 通知内容
* @param params HTTP 回调接口 content type application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 回调对象
* 1. {@link PayRefundNotifyRespDTO} 退款通知
* 2. {@link PayOrderNotifyRespDTO} 支付通知
* 1. {@link PayRefundRespDTO} 退款通知
* 2. {@link PayOrderRespDTO} 支付通知
*/
default Object parseNotify(PayNotifyReqDTO rawNotify) {
default Object parseNotify(Map<String, String> params, String body) {
throw new UnsupportedOperationException("未实现 parseNotify 方法!");
}

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.Map;
/**
* 支付订单退款订单回调渠道的统一通知请求数据
*/
@Data
@ToString
@Builder
public class PayNotifyReqDTO {
/**
* HTTP 回调接口的 request body
*/
private String body;
/**
* HTTP 回调接口 content type application/x-www-form-urlencoded 的所有参数
*/
private Map<String,String> params;
}

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 支付通知 Response DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderNotifyRespDTO {
/**
* 支付订单号支付模块的
*/
private String orderExtensionNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
}

View File

@ -1,58 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
*
* @author jason
*/
@Data
@ToString
@Builder
public class PayRefundNotifyRespDTO {
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 交易订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说P202110132239124200055
*/
private String tradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号
* 标识一次退款请求需要保证在交易号下唯一如需部分退款则此参数必传
* 针对同一次退款请求如果调用接口失败或异常了重试时需要保证退款请求号不能变更
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成
*
* 例如说RR202109181134287570000
*/
private String reqNo;
/**
* 退款是否成功
*/
private PayNotifyRefundStatusEnum status;
/**
* 退款成功时间
*/
private LocalDateTime refundSuccessTime;
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 渠道支付订单 Response DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderRespDTO {
/**
* 支付状态
*
* 枚举{@link PayOrderStatusRespEnum}
*/
private Integer status;
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO no 字段
*/
private String outTradeNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
/**
* 原始的异步通知结果
*/
private Object rawData;
}

View File

@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
// ========== 商户相关字段 ==========
/**
* 商户订单编号
* 外部订单号
*
* 对应 PayOrderExtensionDO no 字段
*/
@NotEmpty(message = "商户订单编号不能为空")
private String merchantOrderId;
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* 商品标题
*/
@ -63,7 +65,7 @@ public class PayOrderUnifiedReqDTO {
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount;
private Integer price;
/**
* 支付过期时间

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import lombok.Data;
@ -24,11 +23,12 @@ public class PayOrderUnifiedRespDTO {
private String displayContent;
/**
* 同步的通知信息
* 渠道支付订单
*
* 只有在订单直接支付成功时才会进行返回
* 目前只有 bar 条码支付才会出现它是支付发起时直接返回是否支付成功的而其它支付还是异步通知
*/
private PayOrderNotifyRespDTO notify;
private PayOrderRespDTO order;
public PayOrderUnifiedRespDTO(String displayMode, String displayContent) {
this.displayMode = displayMode;

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道退款订单 Response DTO
*
* @author jason
*/
@Data
public class PayRefundRespDTO {
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusRespEnum}
*/
private Integer status;
/**
* 渠道退款单号
*
* 对应 PayRefundDO.channelRefundNo 字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 原始的异步通知结果
*/
private Object rawData;
}

View File

@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull;
public class PayRefundUnifiedReqDTO {
/**
* 用户 IP
* 外部订单号
*
* 对应 PayOrderExtensionDO no 字段
*/
private String userIp;
// TODO @jason这个是否为非必传字段呀只需要传递 payTradeNo 字段即可尽可能精简
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
* 渠道订单号
*/
private String channelOrderNo;
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 支付交易号 {PayOrderExtensionDO no字段} 渠道订单号 不能同时为空
* 外部退款号
*
* 对应 PayRefundDO no 字段
*/
private String payTradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 退款请求单号 同一退款请求单号多次请求只退一笔
* 使用 商户的退款单号{PayRefundDO 字段 merchantRefundNo}
*/
@NotEmpty(message = "退款请求单号")
private String merchantRefundId;
@NotEmpty(message = "退款请求单号不能为空")
private String outRefundNo;
/**
* 退款原因
@ -63,11 +50,12 @@ public class PayRefundUnifiedReqDTO {
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount;
private Integer price;
/**
* 退款结果 notify 回调地址 支付宝退款不需要回调地址 微信需要
* 退款结果 notify 回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;

View File

@ -1,24 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 统一退款 Response DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedRespDTO {
/**
* 渠道退款单编号
*/
private String channelRefundId;
}

View File

@ -5,8 +5,8 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
import lombok.extern.slf4j.Slf4j;
@ -92,10 +92,10 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
throws Throwable;
@Override
public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
// 执行统一退款
PayRefundUnifiedRespDTO resp;
PayRefundRespDTO resp;
try {
resp = doUnifiedRefund(reqDTO);
} catch (ServiceException ex) {
@ -109,7 +109,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
return resp;
}
protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
// ========== 各种工具方法 ==========

View File

@ -2,16 +2,19 @@ package cn.iocoder.yudao.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.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
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.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
import com.alipay.api.*;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
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.AlipayTradeRefundModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeRefundRequest;
@ -22,6 +25,8 @@ import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
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.framework.common.exception.util.ServiceExceptionUtil.exception0;
@ -52,69 +57,72 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
/**
* 支付宝统一的退款接口 alipay.trade.refund
*
* @param reqDTO 退款请求 request DTO
* @return 退款请求 Response
*/
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
model.setTradeNo(reqDTO.getChannelOrderNo());
model.setOutTradeNo(reqDTO.getPayTradeNo());
model.setOutRequestNo(reqDTO.getMerchantRefundId());
model.setRefundAmount(formatAmount(reqDTO.getAmount()));
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setOutRequestNo(reqDTO.getOutRefundNo());
model.setRefundAmount(formatAmount(reqDTO.getPrice()));
// model.setRefundAmount(formatAmount(reqDTO.getPrice() / 2));
model.setRefundReason(reqDTO.getReason());
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
refundRequest.setBizModel(model);
refundRequest.setNotifyUrl(reqDTO.getNotifyUrl());
refundRequest.setReturnUrl(reqDTO.getNotifyUrl());
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizModel(model);
try {
AlipayTradeRefundResponse response = client.execute(refundRequest);
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
// 2.1 执行请求
AlipayTradeRefundResponse response = client.execute(request);
PayRefundRespDTO refund = new PayRefundRespDTO()
.setRawData(response);
// 支付宝只要退款调用返回 success就认为退款成功不需要回调具体可见 parseNotify 方法的说明
// 另外支付宝没有退款单号所以不用设置
if (response.isSuccess()) {
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
//支付宝不返回退款单号设置为空
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
respDTO.setChannelRefundId("");
// return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); TODO
return null;
refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));
Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");
} else {
refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());
}
// 失败需要抛出异常
// return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); TODO
return null;
return refund;
} catch (AlipayApiException e) {
// TODO 记录异常日志
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
// return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); TODO
log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);
return null;
}
}
@Override
@SneakyThrows
public Object parseNotify(PayNotifyReqDTO rawNotify) {
public Object parseNotify(Map<String, String> params, String body) {
// 补充说明支付宝退款时没有回调这点和微信支付是不同的并且退款分成部分退款和全部退款
// 部分退款是会有回调但是它回调的是订单状态的同步回调不是退款订单的回调
// 全部退款Wap 支付有订单状态的同步回调但是 PC/扫码又没有
// 所以这里在解析时即使是退款导致的订单状态同步我们也忽略不做为退款同步而是订单的回调
// 实际上支付宝退款只要发起成功就可以认为退款成功不需要等待回调
// 1. 校验回调数据
String body = rawNotify.getBody();
Map<String, String> params = rawNotify.getParams();
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
StandardCharsets.UTF_8.name(), "RSA2");
StandardCharsets.UTF_8.name(), config.getSignType());
// 2.1 退款的情况
if (bodyObj.containsKey("refund_fee")) {
return PayRefundNotifyRespDTO.builder().channelOrderNo(bodyObj.get("trade_no"))
.tradeNo(bodyObj.get("out_trade_no")).reqNo(bodyObj.get("out_biz_no"))
.status(PayNotifyRefundStatusEnum.SUCCESS)
.refundSuccessTime(parseTime(params.get("gmt_refund")))
.build();
}
// 2.2 支付的情况
return PayOrderNotifyRespDTO.builder()
.orderExtensionNo(bodyObj.get("out_trade_no"))
// 2. 解析订单的状态
String tradeStatus = bodyObj.get("trade_status");
PayOrderStatusRespEnum status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING
: Objects.equals("TRADE_SUCCESS", tradeStatus) ? PayOrderStatusRespEnum.SUCCESS
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED : null;
Assert.notNull(status, (Supplier<Throwable>) () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
});
return PayOrderRespDTO.builder()
.status(Objects.requireNonNull(status).getStatus())
.outTradeNo(bodyObj.get("out_trade_no"))
.channelOrderNo(bodyObj.get("trade_no"))
.channelUserId(bodyObj.get("seller_id"))
.successTime(parseTime(params.get("notify_time")))
.successTime(parseTime(params.get("gmt_payment")))
.rawData(body)
.build();
}

View File

@ -31,10 +31,10 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradeAppPayModel 请求
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
// 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码无线快捷支付产品
// 个性化的参数
// 支付宝扫码支付只有一种展示

View File

@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradePayModel 请求
AlipayTradePayModel model = new AlipayTradePayModel();
// 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setScene("bar_code"); // 当面付条码支付场景
// 个性化的参数
model.setAuthCode(authCode);

View File

@ -33,10 +33,10 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// 个性化的参数

View File

@ -29,10 +29,10 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
// 个性化的参数
// 支付宝扫码支付只有一种展示考虑到前端可能希望二维码扫描后手机打开

View File

@ -30,10 +30,10 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
// 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId());
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
// 个性化的参数
// 支付宝 Wap 支付只有一种展示URL

View File

@ -6,8 +6,7 @@ import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
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.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
@ -22,11 +21,13 @@ 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.PURE_DATETIME_PATTERN;
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;
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;
/**
@ -103,47 +104,47 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
@Override
public Object parseNotify(PayNotifyReqDTO rawNotify) {
log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody());
public Object parseNotify(Map<String, String> params, String body) {
log.info("[parseNotify][微信支付回调 data 数据: {}]", body);
try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case WxPayClientConfig.API_VERSION_V2:
return parseOrderNotifyV2(rawNotify);
return parseOrderNotifyV2(body);
case WxPayClientConfig.API_VERSION_V3:
return parseOrderNotifyV3(rawNotify);
return parseOrderNotifyV3(body);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e);
log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
// throw buildPayException(e);
throw new RuntimeException(e);
// TODO 芋艿缺一个异常翻译
}
}
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException {
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(body);
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
// 转换结果
return PayOrderNotifyRespDTO
return PayOrderRespDTO
.builder()
.orderExtensionNo(notifyResult.getOutTradeNo())
.outTradeNo(notifyResult.getOutTradeNo())
.channelOrderNo(notifyResult.getTransactionId())
.channelUserId(notifyResult.getOpenid())
.successTime(parseDateV2(notifyResult.getTimeEnd()))
.build();
}
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException {
WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null);
private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException {
WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
// 转换结果
Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
"支付结果非 SUCCESS");
return PayOrderNotifyRespDTO.builder()
.orderExtensionNo(result.getOutTradeNo())
return PayOrderRespDTO.builder()
.outTradeNo(result.getOutTradeNo())
.channelOrderNo(result.getTradeState())
.channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null)
.successTime(parseDateV3(result.getSuccessTime()))
@ -175,7 +176,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
if (Objects.equals(e.getReturnCode(), "FAIL")) {
throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getReturnMsg());
}
// 情况三系统异常这里暂时不打交给上层的 AbstractPayClient 统一打
// 情况三系统异常这里暂时不打交给上层的 AbstractPayClient 统一打日志
return e;
}

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
@ -21,7 +21,7 @@ public class WxAppPayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
return null;
}

View File

@ -5,13 +5,14 @@ import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
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.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
import com.github.binarywang.wxpay.constant.WxPayConstants;
@ -57,10 +58,10 @@ public class WxBarPayClient extends AbstractWxPayClient {
}
// 构建 WxPayMicropayRequest 对象
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(expireTime))
.spbillCreateIp(reqDTO.getUserIp())
.authCode(getAuthCode(reqDTO))
@ -70,15 +71,17 @@ public class WxBarPayClient extends AbstractWxPayClient {
try {
WxPayMicropayResult response = client.micropay(request);
// 支付成功例如说用户输入了密码
PayOrderNotifyRespDTO notify = PayOrderNotifyRespDTO.builder()
.orderExtensionNo(response.getOutTradeNo())
PayOrderRespDTO order = PayOrderRespDTO.builder()
.status(PayOrderStatusRespEnum.SUCCESS.getStatus())
.outTradeNo(response.getOutTradeNo())
.channelOrderNo(response.getTransactionId())
.channelUserId(response.getOpenid())
.successTime(parseDateV2(response.getTimeEnd()))
.rawData(response)
.build();
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.BAR_CODE.getMode(),
JsonUtils.toJsonString(response))
.setNotify(notify);
.setOrder(order);
} catch (WxPayException ex) {
// 如果不满足这 3 种任一的则直接抛出 WxPayException 异常不仅需处理
// 1. SYSTEMERROR接口返回错误请立即调用被扫订单结果查询API查询当前订单状态并根据订单的状态决定下一步的操作
@ -102,7 +105,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
return null;
}

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
@ -21,7 +21,7 @@ public class WxH5PayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
return null;
}

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
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.WxPayNativeOrderResult;
@ -37,11 +37,11 @@ public class WxNativePayClient extends AbstractWxPayClient {
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分
.productId(reqDTO.getMerchantOrderId())
.totalFee(reqDTO.getPrice()) // 单位分
.productId(reqDTO.getOutTradeNo())
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
@ -58,9 +58,9 @@ public class WxNativePayClient extends AbstractWxPayClient {
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getBody());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
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());
@ -73,7 +73,7 @@ public class WxNativePayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
return null;
}

View File

@ -5,8 +5,8 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
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.refund.PayRefundUnifiedRespDTO;
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;
@ -47,10 +47,10 @@ public class WxPubPayClient extends AbstractWxPayClient {
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId())
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.openid(getOpenid(reqDTO))
@ -68,9 +68,9 @@ public class WxPubPayClient extends AbstractWxPayClient {
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId());
request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getSubject());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
@ -84,7 +84,7 @@ public class WxPubPayClient extends AbstractWxPayClient {
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
// TODO 需要实现
throw new UnsupportedOperationException();
}

View File

@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的支付状态枚举
*
* @author 遇到源码
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
@ -20,4 +22,14 @@ public enum PayOrderStatusRespEnum {
private final Integer status;
private final String name;
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
}

View File

@ -1,23 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
// TODO 芋艿看看能不能去掉
/**
* 退款通知, 统一的渠道退款状态
*
* @author jason
*/
public enum PayNotifyRefundStatusEnum {
/**
* 支付宝 全额退款 trade_status=TRADE_CLOSED 部分退款 trade_status=TRADE_SUCCESS
* 退款成功
*/
SUCCESS,
/**
* 支付宝退款通知没有这个状态
* 退款异常
*/
ABNORMAL;
}

View File

@ -1,23 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundRespEnum {
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
PROCESSING(3,"退款处理中"),
CLOSED(4, "退款关闭");
private final Integer status;
private final String name;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundStatusRespEnum {
WAITING(0, "未退款"),
SUCCESS(10, "退款成功"),
FAILURE(20, "退款失败");
private final Integer status;
private final String name;
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
}

View File

@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest {
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
reqDTO.setAmount(123);
reqDTO.setPrice(123);
reqDTO.setSubject("IPhone 13");
reqDTO.setBody("biubiubiu");
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setUserIp("127.0.0.1");
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
return reqDTO;

View File

@ -1,8 +1,6 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import com.alipay.api.AlipayApiException;
import com.alipay.api.DefaultAlipayClient;
@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
// 这里设置可以直接随机整个对象
Long shopOrderId = System.currentTimeMillis();
PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
reqDTO.setAmount(1);
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setPrice(1);
reqDTO.setBody("内容:" + shopOrderId);
reqDTO.setSubject("标题:"+shopOrderId);
String notify="http://niubi.natapp1.cc/api/pay/order/notify";

View File

@ -5,14 +5,18 @@ import lombok.Getter;
import java.util.Objects;
/**
* 渠道的退款状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayRefundStatusEnum {
CREATE(0, "退款订单生成"),
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
CLOSE(99, "退款关闭");
WAITING(0, "未退款"),
SUCCESS(10, "退款成功"),
FAILURE(20, "退款失败");
private final Integer status;
private final String name;

View File

@ -3,9 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import io.swagger.v3.oas.annotations.Operation;
@ -61,18 +60,17 @@ public class PayNotifyController {
}
// 2. 解析通知数据
PayNotifyReqDTO rawNotify = PayNotifyReqDTO.builder().params(params).body(body).build();
Object notify = payClient.parseNotify(rawNotify);
Object notify = payClient.parseNotify(params, body);
// 3. 处理通知
// 3.1退款通知
if (notify instanceof PayRefundNotifyRespDTO) {
refundService.notifyPayRefund(channelId, (PayRefundNotifyRespDTO) notify, rawNotify);
if (notify instanceof PayRefundRespDTO) {
refundService.notifyPayRefund(channelId, (PayRefundRespDTO) notify);
return "success";
}
// 3.2支付通知
if (notify instanceof PayOrderNotifyRespDTO) {
orderService.notifyPayOrder(channelId, (PayOrderNotifyRespDTO) notify, rawNotify);
if (notify instanceof PayOrderRespDTO) {
orderService.notifyPayOrder(channelId, (PayOrderRespDTO) notify);
return "success";
}
throw new UnsupportedOperationException("未知通知:" + toJsonString(notify));

View File

@ -76,9 +76,6 @@ public class PayRefundExcelVO {
@ExcelProperty("退款成功时间")
private LocalDateTime successTime;
@ExcelProperty("退款通知时间")
private LocalDateTime notifyTime;
@ExcelProperty("退款失效时间")
private LocalDateTime expireTime;

View File

@ -60,9 +60,10 @@ public interface PayRefundConvert {
PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
payRefundExcelVO.setId(bean.getId());
payRefundExcelVO.setTradeNo(bean.getTradeNo());
payRefundExcelVO.setTradeNo(bean.getNo());
payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
// TODO 芋艿晚点在改
// payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
payRefundExcelVO.setStatus(bean.getStatus());
@ -71,9 +72,7 @@ public interface PayRefundConvert {
payRefundExcelVO.setUserIp(bean.getUserIp());
payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
payRefundExcelVO.setExpireTime(bean.getExpireTime());
payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
payRefundExcelVO.setNotifyTime(bean.getNotifyTime());
payRefundExcelVO.setCreateTime(bean.getCreateTime());
BigDecimal multiple = new BigDecimal(100);

View File

@ -55,7 +55,8 @@ public class PayOrderDO extends BaseDO {
/**
* 商户订单编号
* 例如说内部系统 A 的订单号需要保证每个 PayMerchantDO 唯一
*
* 例如说内部系统 A 的订单号需要保证每个 PayAppDO 唯一
*/
private String merchantOrderId;
/**

View File

@ -32,10 +32,11 @@ public class PayOrderExtensionDO extends BaseDO {
*/
private Long id;
/**
* 支付订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 使用该字段作为 out_trade_no
* 外部订单号根据规则生成
*
* 调用支付渠道时使用该字段作为对接的订单号
* 1. 微信支付对应 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 支付</a> out_trade_no 字段
* 2. 支付宝支付对应 <a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a> out_trade_no 字段
*
* 例如说P202110132239124200055
*/
@ -70,7 +71,7 @@ public class PayOrderExtensionDO extends BaseDO {
/**
* 支付渠道的额外参数
*
* 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html
* 参见 <a href="https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html">参数说明</>
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, String> channelExtras;

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO {
*/
@TableId
private Long id;
/**
* 外部退款号根据规则生成
*
* 调用支付渠道时使用该字段作为对接的退款号
* 1. 微信退款对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> out_refund_no 字段
* 2. 支付宝退款对应 <a href="https://opendocs.alipay.com/open/02e7go"统一收单交易退款接口></a> out_request_no 字段
*/
private String no;
/**
* 应用编号
@ -63,47 +72,27 @@ public class PayRefundDO extends BaseDO {
*/
private Long orderId;
/**
* 交易订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号
* 1. 调用微信支付 https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说P202110132239124200055
*/
private String tradeNo;
// ========== 商户相关字段 ==========
/**
* 商户订单编号
*
* 例如说内部系统 A 的订单号需要保证每个 PayAppDO 唯一
*/
private String merchantOrderId;
/**
* 商户退款订单号, 由商户系统产生 由他们保证唯一不能为空通知商户时会传该字段
* 例如说内部系统 A 的退款订单号需要保证每个 PayMerchantDO 唯一
* 个商户退款订单对应一条退款请求记录可多次提交 渠道保持幂等
* 使用商户退款单作为退款请求号
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号
* 标识一次退款请求需要保证在交易号下唯一如需部分退款则此参数必传
* 针对同一次退款请求如果调用接口失败或异常了重试时需要保证退款请求号不能变更
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成
* 例如说R202109181134287570000
* 商户退款订单号
*
* 例如说内部系统 A 的订单号需要保证每个 PayAppDO 唯一
*/
// TODO @jasonmerchantRefundNo =merchantRefundOId
private String merchantRefundNo;
private String merchantRefundId;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 通知商户退款结果的回调状态
* TODO 0 未发送 1 已发送
*
* 枚举 {@link PayNotifyStatusEnum}
*/
private Integer notifyStatus;
@ -142,22 +131,27 @@ public class PayRefundDO extends BaseDO {
// ========== 渠道相关字段 ==========
/**
* 渠道订单号pay_order 中的channel_order_no 对应
* 渠道订单号
*
* 冗余 {@link PayOrderDO#getChannelOrderNo()}
*/
private String channelOrderNo;
/**
* 微信中的 refund_id
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
* 支付宝没有
* 渠道退款单号渠道返回
* 渠道退款单号
*
* 1. 微信退款对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> refund_id 字段
* 2. 支付宝退款没有字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时错误信息
*/
@ -165,22 +159,15 @@ public class PayRefundDO extends BaseDO {
/**
* 支付渠道的额外参数
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
*
* 参见 <a href="https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html">参数说明</>
*/
private String channelExtras;
/**
* TODO
* 退款失效时间
* 支付渠道异步通知的内容
*
* 在退款成功后会记录回调的数据
*/
private LocalDateTime expireTime;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 退款通知时间
*/
private LocalDateTime notifyTime;
private String channelNotifyData;
}

View File

@ -45,8 +45,10 @@ public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
return selectOne("req_no", reqNo);
}
// TODO 芋艿要重构
default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){
return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
// return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
return null;
}
}

View File

@ -83,17 +83,20 @@ public class PayChannelServiceImpl implements PayChannelService {
*/
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(channelCache)) {
initLocalCache();
return;
}
// 注意忽略自动多租户因为要全局初始化缓存
TenantUtils.executeIgnore(() -> {
// 情况一如果缓存里没有数据则直接刷新缓存
if (CollUtil.isEmpty(channelCache)) {
initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache();
}
});
}
@Override

View File

@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.pay.service.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
@ -103,8 +102,7 @@ public interface PayOrderService {
*
* @param channelId 渠道编号
* @param notify 通知
* @param rawNotify 通知数据
*/
void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify);
void notifyPayOrder(Long channelId, PayOrderRespDTO notify);
}

View File

@ -9,10 +9,10 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.pay.config.PayProperties;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
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.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
@ -148,17 +148,17 @@ public class PayOrderServiceImpl implements PayOrderService {
// 3. 调用三方接口
PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
// 商户相关的字段
.setMerchantOrderId(orderExtension.getNo()) // 注意此处使用的是 PayOrderExtensionDO.no 属性
.setOutTradeNo(orderExtension.getNo()) // 注意此处使用的是 PayOrderExtensionDO.no 属性
.setSubject(order.getSubject()).setBody(order.getBody())
.setNotifyUrl(genChannelPayNotifyUrl(channel))
.setReturnUrl(reqVO.getReturnUrl())
// 订单相关字段
.setAmount(order.getPrice()).setExpireTime(order.getExpireTime());
.setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
PayOrderUnifiedRespDTO unifiedOrderRespDTO = client.unifiedOrder(unifiedOrderReqDTO);
// 4. 如果调用直接支付成功则直接更新支付单状态为成功例如说付款码支付免密支付时就直接验证支付成功
if (unifiedOrderRespDTO.getNotify() != null) {
notifyPayOrderSuccess(channel, unifiedOrderRespDTO.getNotify(), null);
if (unifiedOrderRespDTO.getOrder() != null) {
notifyPayOrder(channel, unifiedOrderRespDTO.getOrder());
// 此处需要读取最新的状态
order = orderMapper.selectById(order.getId());
}
@ -226,16 +226,26 @@ public class PayOrderServiceImpl implements PayOrderService {
@Override
@Transactional(rollbackFor = Exception.class)
public void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) {
public void notifyPayOrder(Long channelId, PayOrderRespDTO notify) {
// 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(channelId);
// 更新支付订单为已支付
TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrderSuccess(channel, notify, rawNotify));
TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrder(channel, notify));
}
private void notifyPayOrderSuccess(PayChannelDO channel, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) {
private void notifyPayOrder(PayChannelDO channel, PayOrderRespDTO notify) {
// 情况一支付成功的回调
if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
notifyPayOrderSuccess(channel, notify);
return;
}
// 情况二非支付成功的回调进行忽略
log.info("[notifyPayOrder][非支付成功的回调({}),直接忽略]", toJsonString(notify));
}
private void notifyPayOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) {
// 1. 更新 PayOrderExtensionDO 支付成功
PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify.getOrderExtensionNo(), rawNotify);
PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify);
// 2. 更新 PayOrderDO 支付成功
Pair<Boolean, PayOrderDO> order = updatePayOrderSuccess(channel, orderExtension, notify);
if (order.getKey()) { // 如果之前已经成功回调则直接返回不用重复记录支付通知记录例如说支付平台重复回调
@ -250,13 +260,12 @@ public class PayOrderServiceImpl implements PayOrderService {
/**
* 更新 PayOrderExtensionDO 支付成功
*
* @param no 支付订单号支付模块
* @param rawNotify 通知数据
* @param notify 通知
* @return PayOrderExtensionDO 对象
*/
private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, PayNotifyReqDTO rawNotify) {
private PayOrderExtensionDO updatePayOrderExtensionSuccess(PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no);
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
if (orderExtension == null) {
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
}
@ -269,10 +278,8 @@ public class PayOrderServiceImpl implements PayOrderService {
}
// 2. 更新 PayOrderExtensionDO
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(),
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
.status(PayOrderStatusEnum.SUCCESS.getStatus())
.channelNotifyData(toJsonString(rawNotify)).build());
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(),
PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build());
if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
@ -290,7 +297,7 @@ public class PayOrderServiceImpl implements PayOrderService {
* valuePayOrderDO 对象
*/
private Pair<Boolean, PayOrderDO> updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
PayOrderNotifyRespDTO notify) {
PayOrderRespDTO notify) {
// 1. 判断 PayOrderDO 是否处于待支付
PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
if (order == null) {

View File

@ -1,11 +1,10 @@
package cn.iocoder.yudao.module.pay.service.refund;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import java.util.List;
@ -62,8 +61,7 @@ public interface PayRefundService {
*
* @param channelId 渠道编号
* @param notify 通知
* @param rawNotify 通知数据
*/
void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify);
void notifyPayRefund(Long channelId, PayRefundRespDTO notify);
}

View File

@ -7,10 +7,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.config.PayProperties;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
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.enums.refund.PayNotifyRefundStatusEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
@ -39,14 +38,13 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/**
* 退款订单 Service 实现类
*
* @author aquan
* @author jason
*/
@Service
@Slf4j
@ -116,7 +114,7 @@ public class PayRefundServiceImpl implements PayRefundService {
}
// TODO 芋艿待实现
String merchantRefundId = RandomUtil.randomNumbers(16);
String merchantRefundId = "rrr" + RandomUtil.randomNumbers(16);
// 校验退款的条件
validatePayRefund(reqDTO, order);
@ -128,12 +126,11 @@ public class PayRefundServiceImpl implements PayRefundService {
PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
PayRefundDO payRefundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(),
merchantRefundId); // TODO 芋艿需要优化
if(Objects.nonNull(payRefundDO)){
if (Objects.nonNull(payRefundDO)) {
// 退款订单已经提交过
//TODO 校验相同退款单的金额
// TODO @jason咱要不封装一个 ObjectUtils.equalsAny
if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
|| Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())) {
//已成功退款
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_SUCCEED);
}
@ -147,14 +144,14 @@ public class PayRefundServiceImpl implements PayRefundService {
.channelCode(order.getChannelCode())
.channelId(order.getChannelId())
.orderId(order.getId())
.merchantRefundNo(merchantRefundId) // TODO 芋艿需要优化
.merchantRefundId(merchantRefundId)
.notifyUrl(app.getRefundNotifyUrl())
.payPrice(order.getPrice())
.refundPrice(reqDTO.getPrice())
.userIp(reqDTO.getUserIp())
.merchantOrderId(order.getMerchantOrderId())
.tradeNo(orderExtensionDO.getNo())
.status(PayRefundStatusEnum.CREATE.getStatus())
.no(orderExtensionDO.getNo())
.status(PayRefundStatusEnum.WAITING.getStatus())
.reason(reqDTO.getReason())
.notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
.type(refundType.getStatus())
@ -163,11 +160,9 @@ public class PayRefundServiceImpl implements PayRefundService {
}
// TODO @jason搞到 convert 一些额外的自动手动 set
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
unifiedReqDTO.setUserIp(reqDTO.getUserIp())
.setAmount(reqDTO.getPrice())
.setChannelOrderNo(order.getChannelOrderNo())
.setPayTradeNo(orderExtensionDO.getNo())
.setMerchantRefundId(merchantRefundId) // TODO 芋艿需要优化
unifiedReqDTO.setPrice(reqDTO.getPrice())
.setOutTradeNo(orderExtensionDO.getNo())
.setOutRefundNo(merchantRefundId) // TODO 芋艿需要优化
.setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿优化下 notifyUrl
.setReason(reqDTO.getReason());
// 向渠道发起退款申请
@ -191,24 +186,25 @@ public class PayRefundServiceImpl implements PayRefundService {
@Override
@Transactional(rollbackFor = Exception.class)
public void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify) {
public void notifyPayRefund(Long channelId, PayRefundRespDTO notify) {
// 校验支付渠道是否有效
// TODO 芋艿需要重构下这块的逻辑
PayChannelDO channel = channelService.validPayChannel(channelId);
if (Objects.equals(PayNotifyRefundStatusEnum.SUCCESS, notify.getStatus())){
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
payRefundSuccess(notify);
} else {
//TODO 支付异常 支付宝似乎没有支付异常的通知
// TODO @jason那这里可以考虑打个 error logger @芋艿 微信是否存在支付异常通知
}
}
private void payRefundSuccess(PayRefundNotifyRespDTO refundNotify) {
private void payRefundSuccess(PayRefundRespDTO refundNotify) {
// 校验退款单存在
PayRefundDO refundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(),
refundNotify.getReqNo());
PayRefundDO refundDO = null; // TODO 芋艿临时注释
// PayRefundDO refundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(),
// refundNotify.getReqNo());
if (refundDO == null) {
log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
// TODO 芋艿临时注释
// log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
}
@ -233,10 +229,10 @@ public class PayRefundServiceImpl implements PayRefundService {
// 更新退款订单
PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(refundDO.getId())
.setSuccessTime(refundNotify.getRefundSuccessTime())
.setChannelRefundNo(refundNotify.getChannelOrderNo())
.setTradeNo(refundNotify.getTradeNo())
.setNotifyTime(LocalDateTime.now())
.setSuccessTime(refundNotify.getSuccessTime())
// TODO 芋艿如下两行临时注释
// .setChannelRefundNo(refundNotify.getChannelOrderNo())
// .setNo(refundNotify.getTradeNo())
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
refundMapper.updateById(updateRefundDO);

View File

@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.pay.service.app;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
@ -18,8 +16,6 @@ import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;

View File

@ -62,7 +62,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setChannelId(1L);
o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
o.setOrderId(1L);
o.setTradeNo("OT0000001");
o.setNo("OT0000001");
o.setMerchantOrderId("MOT0000001");
o.setMerchantRefundNo("MRF0000001");
o.setNotifyUrl("https://www.cancanzi.com");
@ -127,7 +127,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setChannelId(1L);
o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
o.setOrderId(1L);
o.setTradeNo("OT0000001");
o.setNo("OT0000001");
o.setMerchantOrderId("MOT0000001");
o.setMerchantRefundNo("MRF0000001");
o.setNotifyUrl("https://www.cancanzi.com");

View File

@ -28,14 +28,14 @@ export function deleteChannel(id) {
}
// 获得支付渠道
export function getChannel(appId,code) {
export function getChannel(appId, code) {
return request({
url: '/pay/channel/get-channel',
url: '/pay/channel/get',
method: 'get',
params:{
appId:appId,
code:code
appId,
code
},
method: 'get'
})
}