支付宝退款申请通知

This commit is contained in:
jason 2021-11-22 16:22:46 +08:00
parent 444ba79822
commit dfde260ebb
13 changed files with 234 additions and 22 deletions

View File

@ -1,12 +1,7 @@
package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order;
import java.util.*;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import org.apache.ibatis.annotations.Mapper;
@ -17,5 +12,7 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
default PayRefundDO selectByReqNo(String reqNo) {
return selectOne("req_no", reqNo);
}
}

View File

@ -47,8 +47,8 @@ public interface PayErrorCodeCoreConstants {
ErrorCode PAY_REFUND_AMOUNT_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款");
ErrorCode PAY_REFUND_CHN_ORDER_NO_IS_NULL = new ErrorCode(1007006002, "该订单的渠道订单为空");
ErrorCode PAY_REFUND_POST_HANDLER_NOT_FOUND = new ErrorCode(1007006002, "未找到对应的退款后置处理类");
ErrorCode PAY_REFUND_POST_HANDLER_NOT_FOUND = new ErrorCode(1007006003, "未找到对应的退款后置处理类");
ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在");
// TODO @aquan下面还两个要合并上去哈另外一般中英文之间要有空格例如说 新建一个 order 数据这样可读性更好
/**

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundReqBO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundRespBO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
/**
* 退款单 Core Service
@ -17,4 +18,17 @@ public interface PayRefundCoreService {
* @return 退款申请返回信息
*/
PayRefundRespBO refund(PayRefundReqBO reqDTO);
/**
* 渠道的退款通知
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param notifyData 通知数据
* @throws Exception 退款通知异常
*/
void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
}

View File

@ -10,7 +10,11 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderExtensionCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundNotifyDTO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
@ -24,8 +28,10 @@ import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundRespBO
import cn.iocoder.yudao.coreservice.modules.pay.util.PaySeqUtils;
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.PayNotifyDataDTO;
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.PayNotifyRefundStatusEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -66,6 +72,9 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
@Resource
private List<PayRefundChannelPostHandler> handlerList;
@Resource
private PayNotifyCoreService payNotifyCoreService;
private final EnumMap<PayChannelRespEnum, PayRefundChannelPostHandler> mapHandler = new EnumMap<>(PayChannelRespEnum.class);
@ -84,7 +93,6 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayRefundRespBO refund(PayRefundReqBO reqBO) {
@ -173,6 +181,64 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
}
@Override
public void notifyPayRefund(Long channelId, String channelCode, PayNotifyDataDTO notifyData) {
log.info("[notifyPayRefund][channelId({}) 回调数据({})]", channelId, notifyData.getBody());
// 校验支付渠道是否有效
PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
//解析渠道退款通知数据 统一处理
PayRefundNotifyDTO refundNotify = client.parseRefundNotify(notifyData);
if(Objects.equals(PayNotifyRefundStatusEnum.SUCCESS,refundNotify.getStatus())){
//退款成功 支付宝只有退款成功才会发通知
PayRefundDO refundDO = payRefundMapper.selectByReqNo(refundNotify.getReqNo());
if (refundDO == null) {
log.error("不存在 seqNo 为{} 的支付退款单",refundNotify.getReqNo());
throw exception(PAY_REFUND_NOT_FOUND);
}
Long refundAmount = refundDO.getRefundAmount();
Integer type = refundDO.getType();
PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
if(PayRefundTypeEnum.ALL.getStatus().equals(type)){
orderStatus = PayOrderStatusEnum.CLOSED;
}
//更新支付订单
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
//需更新已退金额
Long refundedAmount = payOrderDO.getRefundAmount();
PayOrderDO updateOrderDO = new PayOrderDO();
updateOrderDO.setId(refundDO.getOrderId())
.setRefundAmount(refundedAmount + refundAmount)
.setStatus(orderStatus.getStatus())
.setRefundStatus(type);
payOrderCoreMapper.updateById(updateOrderDO);
//跟新退款订单
PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(refundDO.getId())
.setSuccessTime(refundNotify.getRefundSuccessTime())
.setChannelRefundNo(refundNotify.getChannelOrderNo())
.setTradeNo(refundNotify.getTradeNo())
.setNotifyTime(new Date())
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
payRefundMapper.updateById(updateRefundDO);
//插入退款通知记录
// TODO 通知商户成功或者失败. 现在通知似乎没有实现 只是回调
payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
}else{
//TODO 退款失败
}
}
/**
* 校验是否进行退款
* @param reqBO 退款申请信息
@ -197,7 +263,5 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
throw exception(PAY_REFUND_CHN_ORDER_NO_IS_NULL);
}
//TODO 退款的期限 退款次数的控制
}
}

View File

@ -42,4 +42,11 @@ public interface PayClient {
*/
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析支付退款通知数据
* @param notifyData 支付退款通知请求数据
* @return 支付退款通知的Notify DTO
*/
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
}

View File

@ -8,7 +8,7 @@ import java.util.Map;
/**
* 支付订单回调渠道的统一通知请求数据
* 支付订单退款订单回调渠道的统一通知请求数据
*/
@Data
@ToString

View File

@ -0,0 +1,63 @@
package cn.iocoder.yudao.framework.pay.core.client.dto;
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
/**
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
*
* @author jason
*/
@Data
@ToString
@Builder
public class PayRefundNotifyDTO {
/**
* 支付渠道编号
*/
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 Date refundSuccessTime;
}

View File

@ -83,6 +83,12 @@ public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig>
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
//TODO 需要实现
throw new UnsupportedOperationException("需要实现");
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
//TODO 需要实现

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
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.PayNotifyRefundStatusEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
@ -103,7 +104,6 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
.data(data.getBody()).build();
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
@ -119,11 +119,10 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
AlipayTradeRefundResponse response = client.execute(refundRequest);
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
if (response.isSuccess()) {
//退款成功
//退款成功,更新为PROCESSING_NOTIFY 而不是 SYNC_SUCCESS 通过支付宝回调接口处理退款导致触发的异步通知
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
//TODO 沙箱环境 返回 的tradeNo(渠道退款单号 订单的tradNo 是一个值是不是理解不对?
respDTO.setRespEnum(PayChannelRespEnum.SYNC_SUCCESS)
.setChannelRefundNo(response.getTradeNo())
.setPayTradeNo(response.getOutTradeNo());
respDTO.setRespEnum(PayChannelRespEnum.PROCESSING_NOTIFY);
}else{
//特殊处理 sub_code ACQ.SYSTEM_ERROR系统错误 需要调用重试任务
//沙箱环境返回的貌似是aop.ACQ.SYSTEM_ERROR 用contain
@ -153,12 +152,20 @@ public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig>
.setChannelErrMsg(e.getErrMsg())
.setRespEnum(PayChannelRespEnum.CALL_EXCEPTION);
}
return respDTO;
}
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
Map<String, String> params = notifyData.getParams();
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
.tradeNo(params.get("out_trade_no"))
.reqNo(params.get("out_biz_no"))
.status(PayNotifyRefundStatusEnum.SUCCESS)
.refundSuccessTime(DateUtil.parse(params.get("gmt_refund"), "yyyy-MM-dd HH:mm:ss"))
.build();
return notifyDTO;
}
}

View File

@ -141,6 +141,12 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
.data(data.getBody()).build();
}
@Override
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
//TODO 需要实现
throw new UnsupportedOperationException("需要实现");
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {

View File

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

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.userserver.modules.pay.controller.order;
import cn.hutool.core.bean.BeanUtil;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@ -34,6 +35,9 @@ public class PayOrderController {
@Resource
private PayOrderCoreService payOrderCoreService;
@Resource
private PayRefundCoreService payRefundCoreService;
@PostMapping("/submit")
@ApiOperation("提交支付订单")
// @PreAuthenticated // TODO 暂时不加登陆验证前端暂时没做好
@ -85,11 +89,21 @@ public class PayOrderController {
public String notifyAliPayWapPayOrder(@PathVariable("channelId") Long channelId,
@RequestParam Map<String, String> params,
@RequestBody String originData) throws Exception {
//TODO @jason 校验 是否支付宝调用 payclient 中加一个校验方法
payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
//TODO 校验是否支付宝调用 payclient 中加一个校验方法
//支付宝退款交易也会触发支付回调接口
//参考 https://opensupport.alipay.com/support/helpcenter/193/201602484851
//判断是否为支付宝的退款交易
if(isAliPayRefund(params)) {
//退款通知
payRefundCoreService.notifyPayRefund(channelId,PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
}else{
//支付通知
payOrderCoreService.notifyPayOrder(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), PayNotifyDataDTO.builder().params(params).body(originData).build());
}
return "success";
}
/**
* https://opendocs.alipay.com/open/203/105285#%E5%89%8D%E5%8F%B0%E5%9B%9E%E8%B7%B3%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
* @param channelId 渠道id
@ -102,6 +116,19 @@ public class PayOrderController {
return "支付成功";
}
/**
* 是否是支付宝的退款交易
* @param params http content-type application/x-www-form-urlencoded 的参数
* @return
*/
private boolean isAliPayRefund(Map<String, String> params) {
if (params.containsKey("refund_fee")) {
return true;
} else {
return false;
}
}
@RequestMapping("/notify/test")
@ApiOperation("通知的测试接口")
public String notifyTest() {

View File

@ -37,6 +37,7 @@ public class PayRefundController {
reqBO.setUserIp(getClientIP());
//TODO 测试暂时模拟生成商户退款订单
reqBO.setMerchantRefundNo(PaySeqUtils.genMerchantRefundNo());
//reqBO.setMerchantRefundNo("MO202111210814084370000");
return CommonResult.success( PayRefundConvert.INSTANCE.convert(payRefundCoreService.refund(reqBO)));
}
}