|
@@ -1,8 +1,7 @@
|
|
|
package cn.iocoder.yudao.module.pay.service.refund;
|
|
|
|
|
|
+import cn.hutool.core.date.DateUtil;
|
|
|
import cn.hutool.core.util.RandomUtil;
|
|
|
-import cn.hutool.core.util.StrUtil;
|
|
|
-import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
|
|
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;
|
|
@@ -10,22 +9,23 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
|
|
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.PayRefundStatusRespEnum;
|
|
|
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
|
|
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.module.pay.convert.refund.PayRefundConvert;
|
|
|
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.dal.dataobject.order.PayOrderExtensionDO;
|
|
|
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
|
|
-import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
|
|
|
import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
|
|
|
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
|
|
|
+import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
|
|
|
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
|
|
|
-import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
|
|
|
+import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
|
|
|
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
|
|
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
|
|
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
|
|
|
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
|
|
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
|
|
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
|
|
@@ -38,8 +38,11 @@ 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;
|
|
|
+
|
|
|
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
|
|
|
|
|
/**
|
|
|
* 退款订单 Service 实现类
|
|
@@ -59,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|
|
|
|
|
@Resource
|
|
|
private PayRefundMapper refundMapper;
|
|
|
- @Resource
|
|
|
- private PayOrderMapper orderMapper; // TODO @jason:需要改成不直接操作 db;
|
|
|
|
|
|
@Resource
|
|
|
private PayOrderService orderService;
|
|
@@ -80,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|
|
|
|
|
@Override
|
|
|
public Long getRefundCountByAppId(Long appId) {
|
|
|
- return refundMapper.selectCountByApp(appId);
|
|
|
+ return refundMapper.selectCountByAppId(appId);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
@@ -96,82 +97,82 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
|
|
|
- // 获得 PayOrderDO
|
|
|
- PayOrderDO order = orderService.getOrder(reqDTO.getPayOrderId());
|
|
|
- // 校验订单是否存在
|
|
|
- if (Objects.isNull(order) ) {
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
|
|
|
- }
|
|
|
- // 校验 App
|
|
|
- PayAppDO app = appService.validPayApp(order.getAppId());
|
|
|
- // 校验支付渠道是否有效
|
|
|
+ // 1.1 校验 App
|
|
|
+ PayAppDO app = appService.validPayApp(reqDTO.getAppId());
|
|
|
+ // 1.2 校验支付订单
|
|
|
+ PayOrderDO order = validatePayOrderCanRefund(reqDTO);
|
|
|
+ // 1.3 校验支付渠道是否有效
|
|
|
PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
|
|
|
- // 校验支付客户端是否正确初始化
|
|
|
PayClient client = payClientFactory.getPayClient(channel.getId());
|
|
|
if (client == null) {
|
|
|
log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
|
|
|
+ throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
|
|
|
}
|
|
|
-
|
|
|
- // TODO 芋艿:待实现
|
|
|
- String merchantRefundId = "rrr" + RandomUtil.randomNumbers(16);
|
|
|
-
|
|
|
- // 校验退款的条件
|
|
|
- validatePayRefund(reqDTO, order);
|
|
|
- // 退款类型
|
|
|
- PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;
|
|
|
- if (Objects.equals(reqDTO.getPrice(), order.getPrice())) {
|
|
|
- refundType = PayRefundTypeEnum.ALL;
|
|
|
+ // 1.4 校验退款订单是否已经存在
|
|
|
+ PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId(
|
|
|
+ app.getId(), reqDTO.getMerchantRefundId());
|
|
|
+ if (refund != null) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_EXISTS);
|
|
|
}
|
|
|
- PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
|
|
|
- PayRefundDO payRefundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(),
|
|
|
- merchantRefundId); // TODO 芋艿:需要优化
|
|
|
- if (Objects.nonNull(payRefundDO)) {
|
|
|
- // 退款订单已经提交过。
|
|
|
- //TODO 校验相同退款单的金额
|
|
|
- // TODO @jason:咱要不封装一个 ObjectUtils.equalsAny
|
|
|
- if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())) {
|
|
|
- //已成功退款
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_SUCCEED);
|
|
|
- }
|
|
|
- //可以重复提交,保证 退款请求号 一致,由渠道保证幂等
|
|
|
- } else {
|
|
|
- // 成功,插入退款单 状态为生成.没有和渠道交互
|
|
|
- // TODO @jason:搞到 convert 里。一些额外的自动,手动 set 下;
|
|
|
- payRefundDO = PayRefundDO.builder()
|
|
|
- .appId(order.getAppId())
|
|
|
- .channelOrderNo(order.getChannelOrderNo())
|
|
|
- .channelCode(order.getChannelCode())
|
|
|
- .channelId(order.getChannelId())
|
|
|
- .orderId(order.getId())
|
|
|
- .merchantRefundId(merchantRefundId)
|
|
|
- .notifyUrl(app.getRefundNotifyUrl())
|
|
|
- .payPrice(order.getPrice())
|
|
|
- .refundPrice(reqDTO.getPrice())
|
|
|
- .userIp(reqDTO.getUserIp())
|
|
|
- .merchantOrderId(order.getMerchantOrderId())
|
|
|
- .no(orderExtensionDO.getNo())
|
|
|
- .status(PayRefundStatusEnum.WAITING.getStatus())
|
|
|
- .reason(reqDTO.getReason())
|
|
|
- .notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
|
|
|
- .type(refundType.getStatus())
|
|
|
- .build();
|
|
|
- refundMapper.insert(payRefundDO);
|
|
|
- }
|
|
|
- // TODO @jason:搞到 convert 里。一些额外的自动,手动 set 下;
|
|
|
+
|
|
|
+ // 2.1 插入退款单
|
|
|
+ refund = PayRefundConvert.INSTANCE.convert(reqDTO)
|
|
|
+ .setNo(generateRefundNo()).setOrderId(order.getId())
|
|
|
+ .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
|
|
|
+ // 商户相关的字段
|
|
|
+ .setNotifyUrl(app.getRefundNotifyUrl()).setNotifyStatus(PayNotifyStatusEnum.WAITING.getStatus())
|
|
|
+ // 渠道相关字段
|
|
|
+ .setChannelOrderNo(order.getChannelOrderNo())
|
|
|
+ // 退款相关字段
|
|
|
+ .setStatus(PayRefundStatusEnum.WAITING.getStatus())
|
|
|
+ .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice());
|
|
|
+ refundMapper.insert(refund);
|
|
|
+ // 2.2 向渠道发起退款申请
|
|
|
+ PayOrderExtensionDO orderExtension = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
|
|
|
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
|
|
|
unifiedReqDTO.setPrice(reqDTO.getPrice())
|
|
|
- .setOutTradeNo(orderExtensionDO.getNo())
|
|
|
- .setOutRefundNo(merchantRefundId) // TODO 芋艿:需要优化
|
|
|
+ .setOutTradeNo(orderExtension.getNo())
|
|
|
+ .setOutRefundNo(refund.getNo())
|
|
|
.setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl
|
|
|
.setReason(reqDTO.getReason());
|
|
|
- // 向渠道发起退款申请
|
|
|
- client.unifiedRefund(unifiedReqDTO);
|
|
|
- // 检查是否失败,失败抛出业务异常。
|
|
|
- // TODO 渠道的异常记录。
|
|
|
- // TODO @jason:可以先打个 warn log 哈;
|
|
|
+ PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段
|
|
|
+ // 2.3 处理退款返回
|
|
|
+ notifyRefund(channel, refundRespDTO);
|
|
|
+
|
|
|
// 成功在 退款回调中处理
|
|
|
- return payRefundDO.getId();
|
|
|
+ return refund.getId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验支付订单是否可以退款
|
|
|
+ *
|
|
|
+ * @param reqDTO 退款申请信息
|
|
|
+ * @return 支付订单
|
|
|
+ */
|
|
|
+ private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
|
|
|
+ PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
|
|
|
+ if (order == null) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
|
|
|
+ }
|
|
|
+ // 校验状态,必须是支付状态
|
|
|
+ if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 是否已经全额退款
|
|
|
+ if (PayOrderRefundStatusEnum.ALL.getStatus().equals(order.getRefundStatus())) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
|
|
|
+ }
|
|
|
+ // 校验金额 退款金额不能大于原定的金额
|
|
|
+ if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_PRICE_EXCEED);
|
|
|
+ }
|
|
|
+ // 是否有退款中的订单
|
|
|
+ if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
|
|
|
+ PayRefundStatusEnum.WAITING.getStatus()) > 0) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_HAS_REFUNDING);
|
|
|
+ }
|
|
|
+ return order;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -184,88 +185,80 @@ public class PayRefundServiceImpl implements PayRefundService {
|
|
|
return payProperties.getCallbackUrl() + "/" + channel.getId();
|
|
|
}
|
|
|
|
|
|
+ private String generateRefundNo() {
|
|
|
+// wx
|
|
|
+// 2014
|
|
|
+// 10
|
|
|
+// 27
|
|
|
+// 20
|
|
|
+// 09
|
|
|
+// 39
|
|
|
+// 5522657
|
|
|
+// a690389285100
|
|
|
+ // 目前的算法
|
|
|
+ // 时间序列,年月日时分秒 14 位
|
|
|
+ // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
|
|
|
+ return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列
|
|
|
+ RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
|
|
|
+ ;
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
- public void notifyPayRefund(Long channelId, PayRefundRespDTO notify) {
|
|
|
+ public void notifyRefund(Long channelId, PayRefundRespDTO notify) {
|
|
|
+ // 校验支付渠道是否有效
|
|
|
+ channelService.validPayChannel(channelId);
|
|
|
+ // 通知结果
|
|
|
+
|
|
|
// 校验支付渠道是否有效
|
|
|
- // TODO 芋艿:需要重构下这块的逻辑
|
|
|
PayChannelDO channel = channelService.validPayChannel(channelId);
|
|
|
+ // 更新退款订单
|
|
|
+ TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
|
|
|
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
|
|
|
- payRefundSuccess(notify);
|
|
|
+ notifyRefundSuccess(channel, notify);
|
|
|
} else {
|
|
|
- // TODO @jason:那这里可以考虑打个 error logger @芋艿 微信是否存在支付异常通知
|
|
|
+ notifyRefundFailure(channel, notify);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void payRefundSuccess(PayRefundRespDTO refundNotify) {
|
|
|
- // 校验退款单存在
|
|
|
- PayRefundDO refundDO = null; // TODO 芋艿:临时注释
|
|
|
-// PayRefundDO refundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(),
|
|
|
-// refundNotify.getReqNo());
|
|
|
- if (refundDO == null) {
|
|
|
- // TODO 芋艿:临时注释
|
|
|
-// log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
|
|
|
+ private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) {
|
|
|
+ // 1.1 查询 PayRefundDO
|
|
|
+ PayRefundDO refund = refundMapper.selectByAppIdAndNo(
|
|
|
+ channel.getAppId(), notify.getOutRefundNo());
|
|
|
+ if (refund == null) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
|
|
|
+ }
|
|
|
+ if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
|
|
|
}
|
|
|
|
|
|
- // 得到已退金额
|
|
|
- PayOrderDO payOrderDO = orderService.getOrder(refundDO.getOrderId());
|
|
|
- Long refundedAmount = payOrderDO.getRefundPrice();
|
|
|
-
|
|
|
- PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
|
|
|
- if(Objects.equals(payOrderDO.getPrice(), refundedAmount+ refundDO.getRefundPrice())){
|
|
|
- //支付金额 = 已退金额 + 本次退款金额。
|
|
|
- orderStatus = PayOrderStatusEnum.CLOSED;
|
|
|
+ // 1.2 更新 PayRefundDO
|
|
|
+ PayRefundDO updateRefundObj = new PayRefundDO()
|
|
|
+ .setSuccessTime(notify.getSuccessTime())
|
|
|
+ .setChannelRefundNo(notify.getChannelRefundNo())
|
|
|
+ .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
|
|
|
+ .setChannelNotifyData(toJsonString(notify));
|
|
|
+ int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
|
|
|
+ if (updateCounts == 0) { // 校验状态,必须是等待状态
|
|
|
+ throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
|
|
|
}
|
|
|
- // 更新支付订单
|
|
|
- PayOrderDO updateOrderDO = new PayOrderDO();
|
|
|
- updateOrderDO.setId(refundDO.getOrderId())
|
|
|
- .setRefundPrice(refundedAmount + refundDO.getRefundPrice())
|
|
|
- .setStatus(orderStatus.getStatus())
|
|
|
- .setRefundTimes(payOrderDO.getRefundTimes() + 1)
|
|
|
- .setRefundStatus(refundDO.getType());
|
|
|
- orderMapper.updateById(updateOrderDO);
|
|
|
|
|
|
- // 更新退款订单
|
|
|
- PayRefundDO updateRefundDO = new PayRefundDO();
|
|
|
- updateRefundDO.setId(refundDO.getId())
|
|
|
- .setSuccessTime(refundNotify.getSuccessTime())
|
|
|
- // TODO 芋艿:如下两行,临时注释
|
|
|
-// .setChannelRefundNo(refundNotify.getChannelOrderNo())
|
|
|
-// .setNo(refundNotify.getTradeNo())
|
|
|
- .setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
|
|
|
- refundMapper.updateById(updateRefundDO);
|
|
|
+ // 2. 更新订单
|
|
|
+ orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice());
|
|
|
|
|
|
- // 插入退款通知记录
|
|
|
- // TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
|
|
|
+ // 3. 插入退款通知记录
|
|
|
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
|
|
|
- .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
|
|
|
+ .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 校验是否进行退款
|
|
|
- *
|
|
|
- * @param reqDTO 退款申请信息
|
|
|
- * @param order 原始支付订单信息
|
|
|
- */
|
|
|
- private void validatePayRefund(PayRefundCreateReqDTO reqDTO, PayOrderDO order) {
|
|
|
- // 校验状态,必须是支付状态
|
|
|
- if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
|
|
|
- }
|
|
|
- // 是否已经全额退款
|
|
|
- if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
|
|
|
- }
|
|
|
- // 校验金额 退款金额不能大于 原定的金额
|
|
|
- if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_PRICE_PRICE_EXCEED);
|
|
|
- }
|
|
|
- // 校验渠道订单号
|
|
|
- if (StrUtil.isEmpty(order.getChannelOrderNo())) {
|
|
|
- throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_CHN_ORDER_NO_IS_NULL);
|
|
|
- }
|
|
|
- //TODO 退款的期限 退款次数的控制
|
|
|
+ private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
|
|
|
+ // TODO 芋艿:未实现
|
|
|
}
|
|
|
|
|
|
}
|