支付退款申请,支付宝手机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

@ -0,0 +1,26 @@
package cn.iocoder.yudao.coreservice.modules.pay.convert.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
@Mapper
public interface PayRefundCoreConvert {
PayRefundCoreConvert INSTANCE = Mappers.getMapper(PayRefundCoreConvert.class);
PayRefundPostReqBO convert(PayRefundUnifiedRespDTO respDTO);
//TODO 太多需要处理了, 暂时不用
@Mappings(value = {
@Mapping(source = "amount", target = "payAmount"),
@Mapping(source = "id", target = "orderId"),
@Mapping(target = "status",ignore = true)
})
PayRefundDO convert(PayOrderDO orderDO);
}

View File

@ -4,7 +4,7 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
@ -135,7 +135,7 @@ public class PayOrderDO extends BaseDO {
/**
* 退款状态
*
* 枚举 {@link PayOrderRefundStatusEnum}
* 枚举 {@link PayRefundTypeEnum}
*/
private Integer refundStatus;
/**

View File

@ -5,7 +5,9 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChann
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.util.Date;
@ -17,19 +19,34 @@ import java.util.Date;
*
* @author 芋道源码
*/
@TableName("pay_refund")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundDO extends BaseDO {
/**
* 退款单编号,数据库自增
*/
@TableId
private Long id;
/**
* 退款单号,根据规则生成
*
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号。
* 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
* 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
* 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
* 退款单请求号,根据规则生成
*
* 例如说R202109181134287570000
*/
private String no;
private String reqNo;
/**
* 商户编号
*
@ -61,23 +78,39 @@ public class PayRefundDO extends BaseDO {
*/
private Long orderId;
/**
* 交易订单号,根据规则生成
* 调用支付渠道时,使用该字段作为对接的订单号。
* 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;
// ========== 商户相关字段 ==========
/**
* 商户退款订单号
* 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一 TODO 芋艿:需要在测试下
* 商户订单
*/
private String merchantOrderId;
/**
* 商户退款订单号, 由商户系统产生, 由他们保证唯一不能为空通知商户时会传该字段。发送channel 使用 reqNo
* 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一
* TODO 芋艿:我理解 一个商户退款订单,可以对应多条退款记录, 因为有可能失败。但是 退款请求号 reqNo 必须唯一
*
*/
private String merchantRefundNo;
// /**
// * 商户拓展参数
// */
// private String merchantExtra;
/**
* 异步通知地址
*/
private String notifyUrl;
/**
* 通知商户退款结果的回调状态
* TODO 芋艿:0 未发送 1 已发送
* TODO 0 未发送 1 已发送
*/
private Integer notifyStatus;
@ -85,44 +118,76 @@ public class PayRefundDO extends BaseDO {
/**
* 退款状态
*
* TODO 芋艿:状态枚举
*/
private Integer status;
/**
* 用户 IP
* 退款类型(部分退款,全部退款)
*/
private String userIp;
private Integer type;
/**
* 退款金额单位
* 支付金额,单位分
*/
private Long amount;
private Long payAmount;
/**
* 退款金额,单位分
*/
private Long refundAmount;
/**
* 退款原因
*/
private String reason;
/**
* 订单退款成功时间
* 用户 IP
*/
private Date successTime;
private String userIp;
// ========== 渠道相关字段 ==========
/**
* 渠道订单号pay_order 中的channel_order_no 对应
*/
private String channelOrderNo;
/**
* 渠道退款单号,渠道返回
*/
private String channelRefundNo;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
/**
* 支付渠道的额外参数
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
*/
private String channelExtras;
/**
* TODO
* 退款失效时间
*/
private Date expireTime;
/**
* 支付渠道的额外参数
*
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
* 退款成功时间
*/
private String channelExtra;
private Date successTime;
/**
* 退款通知时间
*/
private Date notifyTime;
// ========== 渠道相关字段 ==========
/**
* 渠道订单号
*/
private String channelOrderNo;
/**
* 渠道退款号
*/
private String channelRefundNo;
}

View File

@ -0,0 +1,21 @@
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;
/**
* 退款订单 Mapper
*
*/
@Mapper
public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
}

View File

@ -28,6 +28,12 @@ public interface PayErrorCodeCoreConstants {
ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007003002, "支付订单不处于已支付");
// ========== 支付模块(退款) 1-007-006-000 ==========
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, "未找到对应的退款后置处理类");
/**
* ========== 支付商户信息 1-007-004-000 ==========
*/
@ -41,11 +47,11 @@ public interface PayErrorCodeCoreConstants {
/**
* ========== 支付渠道 1-007-006-000 ==========
* ========== 支付渠道 1-007-001-000 ==========
*/
ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007006000, "支付渠道不存在");
ErrorCode CHANNEL_KEY_READ_ERROR = new ErrorCode(1007006002, "支付渠道秘钥文件读取失败");
ErrorCode CHANNEL_NOT_EXISTS = new ErrorCode(1007001003, "支付渠道不存在");
ErrorCode CHANNEL_KEY_READ_ERROR = new ErrorCode(1007001004, "支付渠道秘钥文件读取失败");
// TODO @aquan下面这个错误码缺了 CHANNEL 前缀。另外,错误码的分段,上面有啦,合并下进去哈
ErrorCode EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007006003, "已存在相同的渠道");
ErrorCode EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001005, "已存在相同的渠道");
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PayRefundStatusEnum {
CREATE(0, "退款订单生成"),
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
PROCESSING_NOTIFY(3,"退款中, 渠道通知结果"),
PROCESSING_QUERY(4,"退款中, 系统查询结果"),
UNKNOWN_RETRY(5,"状态未知,需要重试"),
UNKNOWN_QUERY(6,"状态未知,系统查询结果"),
CLOSE(99, "退款关闭");
private final Integer status;
private final String name;
}

View File

@ -11,7 +11,7 @@ import lombok.Getter;
*/
@Getter
@AllArgsConstructor
public enum PayOrderRefundStatusEnum implements IntArrayValuable {
public enum PayRefundTypeEnum implements IntArrayValuable {
NO(0, "未退款"),
SOME(10, "部分退款"),

View File

@ -5,8 +5,10 @@ import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyLogCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyTaskCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify.PayNotifyLockCoreRedisDAO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
@ -72,6 +74,9 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService {
@Resource
private PayNotifyLockCoreRedisDAO payNotifyLockCoreRedisDAO;
@Resource
private PayRefundMapper payRefundMapper;
@Resource
@Lazy // 循环依赖(自己依赖自己),避免报错
private PayNotifyCoreServiceImpl self;
@ -89,7 +94,9 @@ public class PayNotifyCoreServiceImpl implements PayNotifyCoreService {
setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
} else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
// TODO 芋艿,需要实现下哈
throw new UnsupportedOperationException("需要实现");
PayRefundDO refundDO = payRefundMapper.selectById(task.getDataId());
task.setMerchantId(refundDO.getMerchantId()).setAppId(refundDO.getAppId())
.setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl());
}
// 执行插入

View File

@ -17,12 +17,15 @@ import javax.validation.constraints.NotNull;
@AllArgsConstructor
public class PayRefundOrderReqVO {
@ApiModelProperty(value = "商户单编号", required = true, example = "10")
@NotEmpty(message = "商户订单号不能为空")
@ApiModelProperty(value = "商户退款单编号", required = true, example = "10")
@NotEmpty(message = "商户退款单编号不能为空")
private String merchantOrderId;
@ApiModelProperty(value = "支付退款编号", required = true, example = "20")
@NotNull(message = "支付退款编号不能为空")
private Long payRefundId;
@ApiModelProperty(value = "退款状态(成功,失败)", required = true, example = "10")
private Integer status;
}

View File

@ -2,9 +2,7 @@ package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
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.coreservice.modules.pay.service.order.dto.*;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import javax.validation.Valid;
@ -50,4 +48,6 @@ public interface PayOrderCoreService {
*/
void notifyPayOrder(Long channelId, String channelCode, PayNotifyDataDTO notifyData) throws Exception;
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
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.PayRefundMapper;
/**
* 支付退款订单渠道返回后 , 后置处理抽象类, 处理公用的逻辑
* @author jason
*/
public abstract class PayRefundAbstractChannelPostHandler implements PayRefundChannelPostHandler {
private final PayOrderCoreMapper payOrderCoreMapper;
private final PayRefundMapper payRefundMapper;
public PayRefundAbstractChannelPostHandler(PayOrderCoreMapper payOrderCoreMapper,
PayRefundMapper payRefundMapper){
this.payOrderCoreMapper = payOrderCoreMapper;
this.payRefundMapper = payRefundMapper;
}
/**
* 更新退款单
* @param refundDO 需要更新的退款单信息
*/
protected void updatePayRefund(PayRefundDO refundDO){
payRefundMapper.updateById(refundDO);
}
/**
* 更新原始支付订单
* @param payOrderDO 支付订单信息
*/
protected void updatePayOrder(PayOrderDO payOrderDO){
payOrderCoreMapper.updateById(payOrderDO);
}
}

View File

@ -0,0 +1,24 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
/**
* 支付退款订单 ,渠道返回后 后置处理
* @author jason
*/
public interface PayRefundChannelPostHandler {
/**
* 支持的渠道返回值
* @return 支持的渠道返回值数组
*/
PayChannelRespEnum[] supportHandleResp();
/**
* 根据渠道返回, 处理支付退款单
* @param respBO
*/
void handleRefundChannelResp(PayRefundPostReqBO respBO);
}

View File

@ -0,0 +1,20 @@
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;
/**
* 退款单 Core Service
*
* @author jason
*/
public interface PayRefundCoreService {
/**
* 提交退款申请
* @param reqDTO 退款申请信息
* @return 退款申请返回信息
*/
PayRefundRespBO refund(PayRefundReqBO reqDTO);
}

View File

@ -0,0 +1,96 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.bo;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
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;
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundPostReqBO {
/**
* 渠道的通用返回结果
*/
private PayChannelRespEnum respEnum;
private PayRefundTypeEnum refundTypeEnum;
/**
* 已退款的总金额
*/
private Long refundedAmount;
/**
* 本次退款金额
*/
private Long refundAmount;
/**
* 已退款次数
*/
private Integer refundedTimes;
/**
* 订单编号
*/
private Long orderId;
/**
* 退款单编号
*/
private Long refundId;
/**
* 渠道退款单号
*/
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;
}

View File

@ -0,0 +1,43 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.bo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 退款申请单 Request DTO
*/
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundReqBO {
/**
* 支付订单编号自增
*/
private Long payOrderId;
/**
* 退款金额
*/
private Long amount;
/**
* 退款原因
*/
private String reason;
/**
* 商户退款订单号
*/
private String merchantRefundNo;
/**
* 用户 IP
*/
private String userIp;
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.bo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 退款申请单 Response DTO
*/
@Data
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayRefundRespBO {
/**
* 支付退款单编号, 自增
*/
private Long refundId;
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
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.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
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.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
/**
* 支付退款订单渠道返回失败的后置处理类
* {@link PayChannelRespEnum#CALL_EXCEPTION}
* {@link PayChannelRespEnum#CAN_NOT_RETRY_FAILURE}
*/
@Service
public class PayRefundChannelFailedHandler extends PayRefundAbstractChannelPostHandler {
@Resource
private PayNotifyCoreService payNotifyCoreService;
public PayRefundChannelFailedHandler(PayOrderCoreMapper payOrderCoreMapper, PayRefundMapper payRefundMapper) {
super(payOrderCoreMapper, payRefundMapper);
}
@Override
public PayChannelRespEnum[] supportHandleResp() {
return new PayChannelRespEnum[] {PayChannelRespEnum.CALL_EXCEPTION, PayChannelRespEnum.CAN_NOT_RETRY_FAILURE};
}
@Override
public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
//退款失败
//更新退款单表
PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(respBO.getRefundId())
.setStatus(PayRefundStatusEnum.FAILURE.getStatus())
.setChannelErrorCode(respBO.getChannelErrCode())
.setChannelErrorMsg(Optional.ofNullable(respBO.getChannelErrMsg())
.orElse(respBO.getExceptionMsg()));
updatePayRefund(updateRefundDO);
PayOrderDO updateOrderDO = new PayOrderDO();
//更新订单表
updateOrderDO.setId(respBO.getOrderId())
.setRefundTimes(respBO.getRefundedTimes() + 1);
updatePayOrder(updateOrderDO);
// 立刻插入退款通知记录
// TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(respBO.getRefundId()).build());
}
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
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.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import org.springframework.stereotype.Service;
/**
* 支付退款订单渠道返回通知 {@link PayChannelRespEnum#PROCESSING_NOTIFY},后置处理类
* 支付宝退款单好像没有回调, 微信会触发回调
*/
@Service
public class PayRefundChannelNotifyHandler extends PayRefundAbstractChannelPostHandler {
public PayRefundChannelNotifyHandler(PayOrderCoreMapper payOrderCoreMapper,
PayRefundMapper payRefundMapper) {
super(payOrderCoreMapper, payRefundMapper);
}
@Override
public PayChannelRespEnum[] supportHandleResp() {
return new PayChannelRespEnum[] {PayChannelRespEnum.PROCESSING_NOTIFY};
}
@Override
public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
PayRefundDO updateRefundDO = new PayRefundDO();
//更新退款单表
updateRefundDO.setId(respBO.getRefundId())
.setStatus(PayRefundStatusEnum.PROCESSING_NOTIFY.getStatus());
updatePayRefund(updateRefundDO);
PayOrderDO updateOrderDO = new PayOrderDO();
//更新订单表
updateOrderDO.setId(respBO.getOrderId())
.setRefundTimes(respBO.getRefundedTimes() + 1);
updatePayOrder(updateOrderDO);
}
}

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
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.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* 支付退款订单渠道返回需调用查询接口的后置处理类
* {@link PayChannelRespEnum#PROCESSING_QUERY} //TODO 芋道源码 是不是微信有这样的情况
* {@link PayChannelRespEnum#READ_TIME_OUT_EXCEPTION}
*/
@Service
public class PayRefundChannelQueryHandler extends PayRefundAbstractChannelPostHandler {
public PayRefundChannelQueryHandler(PayOrderCoreMapper payOrderCoreMapper,
PayRefundMapper payRefundMapper) {
super(payOrderCoreMapper, payRefundMapper);
}
@Override
public PayChannelRespEnum[] supportHandleResp() {
return new PayChannelRespEnum[]{PayChannelRespEnum.PROCESSING_QUERY, PayChannelRespEnum.READ_TIME_OUT_EXCEPTION};
}
@Override
public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
final PayChannelRespEnum respEnum = respBO.getRespEnum();
PayRefundStatusEnum refundStatus =
Objects.equals(PayChannelRespEnum.PROCESSING_QUERY, respEnum) ? PayRefundStatusEnum.PROCESSING_QUERY
: PayRefundStatusEnum.UNKNOWN_QUERY;
//更新退款单表
PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(respBO.getRefundId())
.setStatus(refundStatus.getStatus());
updatePayRefund(updateRefundDO);
PayOrderDO updateOrderDO = new PayOrderDO();
//更新订单表
updateOrderDO.setId(respBO.getOrderId())
.setRefundTimes(respBO.getRefundedTimes() + 1);
updatePayOrder(updateOrderDO);
//TODO 发起查询任务
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
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.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
* 支付退款订单渠道返回重试的后置处理类
* {@link PayChannelRespEnum#RETRY_FAILURE}
*/
@Service
public class PayRefundChannelRetryHandler extends PayRefundAbstractChannelPostHandler {
public PayRefundChannelRetryHandler(PayOrderCoreMapper payOrderCoreMapper,
PayRefundMapper payRefundMapper) {
super(payOrderCoreMapper, payRefundMapper);
}
@Override
public PayChannelRespEnum[] supportHandleResp() {
return new PayChannelRespEnum[] {PayChannelRespEnum.RETRY_FAILURE};
}
@Override
public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
PayRefundDO updateRefundDO = new PayRefundDO();
//更新退款单表
updateRefundDO.setId(respBO.getRefundId())
.setStatus(PayRefundStatusEnum.UNKNOWN_RETRY.getStatus())
.setChannelErrorCode(respBO.getChannelErrCode())
.setChannelErrorMsg(respBO.getChannelErrMsg());
updatePayRefund(updateRefundDO);
PayOrderDO updateOrderDO = new PayOrderDO();
//更新订单表
updateOrderDO.setId(respBO.getOrderId())
.setRefundTimes(respBO.getRefundedTimes() + 1);
updatePayOrder(updateOrderDO);
//TODO 发起重试任务
}
}

View File

@ -0,0 +1,64 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
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.PayRefundMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
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.coreservice.modules.pay.service.order.PayRefundAbstractChannelPostHandler;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRespEnum;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
/**
* 支付退款订单渠道返回退款成功的后置处理类
* {@link PayChannelRespEnum#SYNC_SUCCESS}
*/
@Service
public class PayRefundChannelSuccessHandler extends PayRefundAbstractChannelPostHandler {
@Resource
private PayNotifyCoreService payNotifyCoreService;
public PayRefundChannelSuccessHandler(PayOrderCoreMapper payOrderCoreMapper,
PayRefundMapper payRefundMapper) {
super(payOrderCoreMapper, payRefundMapper);
}
@Override
public PayChannelRespEnum[] supportHandleResp() {
return new PayChannelRespEnum[]{PayChannelRespEnum.SYNC_SUCCESS};
}
@Override
public void handleRefundChannelResp(PayRefundPostReqBO respBO) {
//退款成功
PayRefundDO updateRefundDO = new PayRefundDO();
//更新退款单表
updateRefundDO.setId(respBO.getRefundId())
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
.setChannelRefundNo(respBO.getChannelRefundNo())
.setSuccessTime(new Date());
updatePayRefund(updateRefundDO);
PayOrderDO updateOrderDO = new PayOrderDO();
//更新订单表
updateOrderDO.setId(respBO.getOrderId())
.setRefundTimes(respBO.getRefundedTimes() + 1)
.setRefundStatus(respBO.getRefundTypeEnum().getStatus())
.setRefundAmount(respBO.getRefundedAmount()+respBO.getRefundAmount());
updatePayOrder(updateOrderDO);
// 立刻插入退款通知记录
// TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(respBO.getRefundId()).build());
}
}

View File

@ -0,0 +1,203 @@
package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.coreservice.modules.pay.convert.order.PayRefundCoreConvert;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
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.order.PayOrderNotifyStatusEnum;
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;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundChannelPostHandler;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.bo.PayRefundPostReqBO;
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.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.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 org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.*;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@Service
@Slf4j
public class PayRefundCoreServiceImpl implements PayRefundCoreService {
@Resource
private PayOrderCoreMapper payOrderCoreMapper;
@Resource
private PayRefundMapper payRefundMapper;
@Resource
private PayOrderExtensionCoreMapper payOrderExtensionCoreMapper;
@Resource
private PayAppCoreService payAppCoreService;
@Resource
private PayChannelCoreService payChannelCoreService;
@Resource
private PayClientFactory payClientFactory;
/**
* 处理渠道返回结果的后置处理器 集合
*/
@Resource
private List<PayRefundChannelPostHandler> handlerList;
private final EnumMap<PayChannelRespEnum, PayRefundChannelPostHandler> mapHandler = new EnumMap<>(PayChannelRespEnum.class);
@PostConstruct
public void init(){
if (Objects.nonNull(handlerList)) {
handlerList.forEach(t->{
for (PayChannelRespEnum item : t.supportHandleResp()) {
mapHandler.put(item, t);
}
});
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayRefundRespBO refund(PayRefundReqBO reqBO) {
// 获得 PayOrderDO
PayOrderDO order = payOrderCoreMapper.selectById(reqBO.getPayOrderId());
// 校验订单是否存在
if (Objects.isNull(order) ) {
throw exception(PAY_ORDER_NOT_FOUND);
}
// 校验 App
PayAppDO app = payAppCoreService.validPayApp(order.getAppId());
// 校验支付渠道是否有效
PayChannelDO channel = payChannelCoreService.validPayChannel(order.getChannelId());
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) {
log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
}
//校验退款的条件
validatePayRefund(reqBO, order);
//退款类型
PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;
if (Objects.equals(reqBO.getAmount(), order.getAmount())) {
refundType = PayRefundTypeEnum.ALL;
}
//退款单入库 退款单状态:生成, 没有和渠道产生交互
PayOrderExtensionDO orderExtensionDO = payOrderExtensionCoreMapper.selectById(order.getSuccessExtensionId());
PayRefundDO refundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
.appId(order.getAppId())
.channelOrderNo(order.getChannelOrderNo())
.channelCode(order.getChannelCode())
.channelId(order.getChannelId())
.merchantId(order.getMerchantId())
.orderId(order.getId())
.merchantRefundNo(reqBO.getMerchantRefundNo())
.notifyUrl(app.getRefundNotifyUrl())
.payAmount(order.getAmount())
.refundAmount(reqBO.getAmount())
.userIp(reqBO.getUserIp())
.merchantOrderId(order.getMerchantOrderId())
.tradeNo(orderExtensionDO.getNo())
.status(PayRefundStatusEnum.CREATE.getStatus())
.reason(reqBO.getReason())
.notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
.reqNo(PaySeqUtils.genRefundReqNo())
.type(refundType.getStatus())
.build();
payRefundMapper.insert(refundDO);
PayRefundUnifiedReqDTO unifiedReqDTO = PayRefundUnifiedReqDTO.builder()
.userIp(reqBO.getUserIp())
.channelOrderNo(refundDO.getChannelOrderNo())
.payTradeNo(refundDO.getTradeNo())
.refundReqNo(refundDO.getReqNo())
.amount(reqBO.getAmount())
.reason(refundDO.getReason())
.build();
//调用渠道进行退款
PayRefundUnifiedRespDTO refundUnifiedRespDTO = client.unifiedRefund(unifiedReqDTO);
//根据渠道返回获取退款后置处理由postHandler 进行处理
PayRefundChannelPostHandler payRefundChannelPostHandler = mapHandler.get(refundUnifiedRespDTO.getRespEnum());
if(Objects.isNull(payRefundChannelPostHandler)){
throw exception(PAY_REFUND_POST_HANDLER_NOT_FOUND);
}
PayRefundPostReqBO bo = PayRefundCoreConvert.INSTANCE.convert(refundUnifiedRespDTO);
bo.setRefundAmount(reqBO.getAmount())
.setRefundedAmount(order.getRefundAmount())
.setRefundedTimes(order.getRefundTimes())
.setRefundId(refundDO.getId())
.setOrderId(order.getId())
.setRefundTypeEnum(refundType);
//调用退款的后置处理
payRefundChannelPostHandler.handleRefundChannelResp(bo);
return PayRefundRespBO.builder().refundId(refundDO.getId()).build();
}
/**
* 校验是否进行退款
* @param reqBO 退款申请信息
* @param order 原始支付订单信息
*/
private void validatePayRefund(PayRefundReqBO reqBO, PayOrderDO order) {
// 校验状态,必须是支付状态
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
throw exception(PAY_ORDER_STATUS_IS_NOT_SUCCESS);
}
//是否已经全额退款
if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
throw exception(PAY_REFUND_ALL_REFUNDED);
}
// 校验金额 退款金额不能大于 原定的金额
if(reqBO.getAmount() + order.getRefundAmount() > order.getAmount()){
throw exception(PAY_REFUND_AMOUNT_EXCEED);
}
//校验渠道订单号
if (StrUtil.isEmpty(order.getChannelOrderNo())) {
throw exception(PAY_REFUND_CHN_ORDER_NO_IS_NULL);
}
//TODO 退款的期限 退款次数的控制
}
}

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.coreservice.modules.pay.util;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
/**
* 支付相关编号的生产
*/
public class PaySeqUtils {
private static final AtomicLong REFUND_REQ_NO_SEQ = new AtomicLong(0L);
private static final AtomicLong MER_REFUND_NO_SEQ = new AtomicLong(0L);
private static final AtomicLong MER_ORDER_NO_SEQ = new AtomicLong(0L);
/**
* 生成商户退款单号,用于测试,应该由商户系统生成
* @return 商户退款单
*/
public static String genMerchantRefundNo() {
return String.format("%s%s%04d", "MR",
DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
(int) MER_REFUND_NO_SEQ.getAndIncrement() % 10000);
}
/**
* 生成退款请求号
* @return 退款请求号
*/
public static String genRefundReqNo() {
return String.format("%s%s%04d", "RR",
DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
(int) REFUND_REQ_NO_SEQ.getAndIncrement() % 10000);
}
/**
* 生成商户订单编号号 用于测试,应该由商户系统生成
* @return 商户订单编号
*/
public static String genMerchantOrderNo() {
return String.format("%s%s%04d", "MO",
DateUtil.format(new Date(), DatePattern.PURE_DATETIME_MS_PATTERN),
(int) MER_ORDER_NO_SEQ.getAndIncrement() % 10000);
}
}