支付退款申请,支付宝手机wap 相应实现

This commit is contained in:
jason
2021-11-21 11:12:32 +08:00
parent 19f554176c
commit 7d6f205dc0
41 changed files with 1519 additions and 61 deletions

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
/**
* 支付客户端,用于对接各支付渠道的 SDK实现发起支付、退款等功能
@ -36,4 +34,12 @@ public interface PayClient {
*/
PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
/**
* 调用支付渠道,进行退款
* @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果
*/
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
}

View File

@ -6,7 +6,10 @@ import lombok.ToString;
import java.util.Map;
// TODO @jason注释要写下哈。字段不要使用 // 注释,非标准
/**
* 支付订单回调,渠道的统一通知请求数据
*/
@Data
@ToString
@Builder

View File

@ -0,0 +1,78 @@
package cn.iocoder.yudao.framework.pay.core.client.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.URL;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 统一 退款 Request DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedReqDTO {
/**
* 用户 IP
*/
private String userIp;
/**
* 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;
/**
* 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字段} 和 渠道订单号 不能同时为空
*/
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_request_no
* 退款请求单号 同一退款请求单号多次请求只退一笔。
*/
@NotEmpty(message = "退款请求单号")
private String refundReqNo;
/**
* 退款原因
*/
@NotEmpty(message = "退款原因不能为空")
private String reason;
/**
* 退款金额,单位:分
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Long amount;
/**
* 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
*/
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.framework.pay.core.client.dto;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
/**
* 统一 退款 Response DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedRespDTO {
/**
* 渠道的通用返回结果
*/
private PayChannelRespEnum respEnum;
/**
* 渠道退款单号
*/
private String channelRefundNo;
/**
* 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字段} 和 渠道订单号 不能同时为空
*/
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_request_no
* 退款请求单号 同一退款请求单号多次请求只退一笔。
*/
private String refundReqNo;
/**
* 调用异常错误信息
*/
private String exceptionMsg;
/**
* 渠道的错误码
*/
private String channelErrCode;
/**
* 渠道的错误描述
*/
private String channelErrMsg;
//TODO 退款资金渠
}

View File

@ -6,8 +6,13 @@ 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.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import lombok.extern.slf4j.Slf4j;
import java.net.SocketTimeoutException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
@ -22,6 +27,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
* 渠道编号
*/
private final Long channelId;
/**
* 渠道编码
*/
@ -91,7 +97,37 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
return result;
}
protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable;
@Override
public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
PayRefundUnifiedRespDTO resp;
try {
resp = doUnifiedRefund(reqDTO);
}catch (SocketTimeoutException ex){
//网络 read time out 异常
log.error("[unifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), ex);
return PayRefundUnifiedRespDTO.builder()
.exceptionMsg(ex.getMessage())
.respEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION)
.build();
} catch (Throwable ex) {
// 打印异常日志
log.error("[unifiedRefund][request({}) 发起退款失败]", toJsonString(reqDTO), ex);
return PayRefundUnifiedRespDTO.builder()
.exceptionMsg(ex.getMessage())
.respEnum(PayChannelRespEnum.CALL_EXCEPTION)
.build();
}
return resp;
}
protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
}

View File

@ -3,9 +3,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.alipay.api.AlipayApiException;
@ -71,6 +69,8 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
}
@Override
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
//结果转换
@ -82,4 +82,10 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
.data(data.getBody()).build();
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
//TODO 需要实现
throw new UnsupportedOperationException();
}
}

View File

@ -3,28 +3,35 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 支付宝【手机网站】的 PayClient 实现类
* 文档https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
*
* @author 芋道源码
*/
@Slf4j
public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
private DefaultAlipayClient client;
@ -96,4 +103,62 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
.data(data.getBody()).build();
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
model.setTradeNo(reqDTO.getChannelOrderNo());
model.setOutTradeNo(reqDTO.getPayTradeNo());
model.setOutRequestNo(reqDTO.getRefundReqNo());
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
model.setRefundReason(reqDTO.getReason());
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
refundRequest.setBizModel(model);
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
try {
AlipayTradeRefundResponse response = client.execute(refundRequest);
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
if (response.isSuccess()) {
//退款成功
//TODO 沙箱环境 返回 的tradeNo(渠道退款单号) 和 订单的tradNo 是一个值,是不是理解不对?
respDTO.setRespEnum(PayChannelRespEnum.SYNC_SUCCESS)
.setChannelRefundNo(response.getTradeNo())
.setPayTradeNo(response.getOutTradeNo());
}else{
//特殊处理 sub_code ACQ.SYSTEM_ERROR系统错误 需要调用重试任务
//沙箱环境返回的貌似是”aop.ACQ.SYSTEM_ERROR“ 用contain
if (response.getSubCode().contains("ACQ.SYSTEM_ERROR")) {
respDTO.setRespEnum(PayChannelRespEnum.RETRY_FAILURE)
.setChannelErrMsg(response.getSubMsg())
.setChannelErrCode(response.getSubCode());
}else{
//其他当做不可以重试的错误
respDTO.setRespEnum(PayChannelRespEnum.CAN_NOT_RETRY_FAILURE)
.setChannelErrCode(response.getSubCode())
.setChannelErrMsg(response.getSubMsg());
}
}
return respDTO;
} catch (AlipayApiException e) {
//TODO 记录异常日志
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
Throwable cause = e.getCause();
//网络 read time out 异常, 退款状态未知
if (cause instanceof SocketTimeoutException) {
respDTO.setExceptionMsg(e.getMessage())
.setRespEnum(PayChannelRespEnum.READ_TIME_OUT_EXCEPTION);
}else{
respDTO.setExceptionMsg(e.getMessage())
.setChannelErrCode(e.getErrCode())
.setChannelErrMsg(e.getErrMsg())
.setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
}
return respDTO;
}
}
}

View File

@ -8,9 +8,7 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
@ -92,6 +90,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
}
private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
@ -142,4 +141,11 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
.data(data.getBody()).build();
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
//TODO 需要实现
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.framework.pay.core.enums;
/**
* 统一的渠道返回结果
* @author jason
*/
public enum PayChannelRespEnum {
/**
* 接口通讯正常返回, 并明确处理成功 ,不需要通过查询或者回调接口 进行下一步处理
*/
SYNC_SUCCESS,
/**
* 接口通讯正常返回, 但返回错误,并且不能通过重试解决的错误,
* 如提交失败, 业务错误(余额不足), 或者参数错误, 签名错误, 需要干预后才能处理
*/
CAN_NOT_RETRY_FAILURE,
/**
* 接口通讯正常返回,
* 可以通过重试解决的错误. 如系统超时, 系统繁忙。状态未知 不能改变请求参数,如退款单请求号,重发请求
*/
RETRY_FAILURE,
/**
* 接口通讯正常返回,但是处理结果 需要渠道回调进行下一步处理
*/
PROCESSING_NOTIFY,
/**
* 接口通讯正常返回, 但是处理结果,需要调用查询接口 进行查询
*/
PROCESSING_QUERY,
/**
* 本系统调用渠道接口异常, 渠道接口请求未正常发送, 本系统不可预知的异常,较少发生, 可认为失败。 不用重试.
*/
CALL_EXCEPTION,
/**
* 本系统调用渠道接口成功, 但是未接受到请求结果较少发生需合理设置read time out ) 结果未知。 需要调用查询接口进行查询
*/
READ_TIME_OUT_EXCEPTION;
}