3订单设计-退款.md 14 KB

退款

用户申请退款

我们来到api服务的OrderRefundController,这里就是用户端退款的接口。

我们直接看一下apply申请退款接口的代码:

@PostMapping("/apply")
@Operation(summary = "申请退款" , description = "申请退款")
public ServerResponseEntity<String> apply(@Valid @RequestBody OrderRefundParam orderRefundParam) {
    String userId = SecurityUtils.getUser().getUserId();
    // 获取订单信息
    Order order = orderService.getOrderByOrderNumberAndUserId(orderRefundParam.getOrderNumber(), userId, true);
    // 订单退款消息校验
    checkOrderReturn(order, orderRefundParam);
    boolean isOrderStatus = Objects.equals(order.getStatus(), OrderStatus.CONSIGNMENT.value())
            || Objects.equals(order.getStatus(), OrderStatus.SUCCESS.value());

    checkAndGetOrderStatus(orderRefundParam, order,isOrderStatus);
    // 生成退款单信息
    OrderRefund newOrderRefund = new OrderRefund();
    // 退款订单校验
    checkReturnOrder(order, orderRefundParam, newOrderRefund);
    // 补充退款订单消息
    setReturnOrderParam(order, orderRefundParam, newOrderRefund);
    // 退款单信息
    orderRefundService.applyRefund(newOrderRefund);

    return ServerResponseEntity.success(newOrderRefund.getRefundSn());
}

可以看到先是进行了订单状态、退款金额和退款方式等判断,然后是生成退款单信息保存到数据库等待下一步处理。

商家处理退款

我们来到商家服务的OrderRefundController,这里就是商家处理退款的接口。

我们直接看一下processRefundOrder处理退款接口的代码:

@SysLog("退款处理 -商家处理退款订单")
@PutMapping("/process")
@Operation(summary = "处理退款订单" , description = "处理退款订单")
@PreAuthorize("@pms.hasPermission('order:refund:update')")
public ServerResponseEntity<Void> processRefundOrder(@RequestBody OrderRefundParam orderRefundParam) {

    // 处理退款操作
    orderRefundParam.setIsPaltform(false);
    OrderRefundDto orderRefundDto = orderRefundService.processRefundOrder(orderRefundParam, SecurityUtils.getShopUser().getShopId());

    // 仅退款,执行退款操作
    if (Objects.equals(orderRefundDto.getApplyType(), 1)) {
        this.submitWxRefund(orderRefundDto);
    }
    ...
    return ServerResponseEntity.success();
}

我们看到processRefundOrder这个退款核心处理方法:

@Override
@Transactional(rollbackFor = Exception.class)
public OrderRefundDto processRefundOrder(OrderRefundParam orderRefundParam, Long shopId) {
    ...
    // 是否同意退款
    if (Objects.equals(orderRefundParam.getRefundSts(), RefundStsType.DISAGREE.value())) {
        // 不同意退款
        disagreeRefund(orderRefundParam, orderRefundDto);
    } else {
        // 正在处理退款的状态(不是平台处理介入退款时)
        if (!orderRefundParam.getIsPaltform()) {
            orderRefundDto.setReturnMoneySts(ReturnMoneyStsType.PROCESSING.value());
        }

        // 判断是退款退货/仅退款(1:仅退款 2:退款退货)
        if (Objects.equals(orderRefundDto.getApplyType(), 1)) {
            // 同意退款
            checkLastRefund(orderRefundDto);
            agreeRefund(orderRefundDto);
            // 减少商家结算金额
            subShopSettlementAmount(orderRefundDto);
        } else {
            // 同意退款退货操作
            // 设置退货物流信息
            ...
        }
    }
    ...
    // 更新数据
    updateById(orderRefundDto);
    return orderRefundDto;
}

可以看到先是判断是否同意退款,如果不同意直接关闭退款,如果同意且仅退款的退款单,则调用agreeRefund方法添加退款结算记录且减少商家待结算金额。 如果是退货退款则设置好退货物流信息,等待下一步卖家发货。 然后我们在回到上一步在商家处理完退款单后,可以看到如果是仅退款的退款单会调用submitWxRefund方法,我们可以在这里找到这行代码:

refundInfoService.doRefund(refundInfo);

通过这个方法进行了退款:

@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public Boolean doRefund(RefundInfoDto refundInfoDto) {
    if (count(new LambdaQueryWrapper<RefundInfo>().eq(RefundInfo::getRefundId, refundInfoDto.getRefundSn())) > 0) {
        return false;
    }
    ...
    // paypal支付,直接退款都不需要回调的
    // paypal、余额支付,直接退款都不需要回调的,只要有一个是需要回调则等回调
    if (Objects.equals(payInfo.getPaySysType(), 0)) {
        logger.info("开始进行普通退款{}",returnRefundInfo);
        refundSuccess = payManager.doRefund(returnRefundInfo);
    } else {
        logger.info("开始进行通联退款{}",returnRefundInfo);
        refundSuccess = allinpayManager.doRefund(returnRefundInfo);
    }
    // 全额积分抵扣或者paypal的支付类型,这里直接退款成功
    boolean canSuccessPayType = Objects.equals(payInfo.getPayType(),PayType.SCOREPAY.value()) || Objects.equals(payInfo.getPayType(),PayType.PAYPAL.value());
    if (canSuccessPayType && !Objects.equals(refundInfoDto.getOnlyRefund(), 1) && refundSuccess) {
        for (RefundInfo refundInfo : refundInfoList) {
            refundSuccess(refundInfo);
            ...
        }
    }
    return refundSuccess;
}

在这里我们可以看到这个订单如果是全额积分抵扣或者paypal的支付类型,这里会直接退款成功,如果是第三方支付如微信/支付宝/通联等就需要等待第三方回调我们的接口进行退款。

退货退款

如果是退货退款在用户申请商家同意,需要用户进行发货,此时我们回到api服务的OrderRefundController, 我们直接看一下submitExpress申请退款接口的核心代码:

orderRefundService.submitExpress(orderRefund, refundDelivery);

在这里将用户填写的退货信息存入数据库改变订单状态,等待商家进行处理。 最后我们再回到商家服务的OrderRefundController, 我们直接看一下submitExpress申请退款接口的代码:

@SysLog("退款处理 -商家退货处理")
@PutMapping("/returnMoney")
@Operation(summary = "退款处理-商家退货处理" , description = "退款处理-商家退货处理")
@PreAuthorize("@pms.hasPermission('order:refund:update')")
public ServerResponseEntity<Void> returnMoney(@RequestBody OrderRefundParam orderRefundParam) {
    if (Objects.isNull(orderRefundParam.getIsReceiver())) {
        // 退货状态不能为空
        throw new YamiShopBindException("yami.order.refund.status.exist");
    }
    // 退货处理
    OrderRefundDto orderRefundDto = orderRefundService.returnMoney(orderRefundParam, SecurityUtils.getShopUser().getShopId());
    // 执行退款操作
    this.submitWxRefund(orderRefundDto);
    return ServerResponseEntity.success();
}

可以看到这行代码:

this.submitWxRefund(orderRefundDto);

以及我们看到在returnMoney方法可以看到:

// 是否同意退款
if (Objects.equals(orderRefundParam.getRefundSts(), RefundStsType.AGREE.value())) {
    // 减少商家结算金额
    subShopSettlementAmount(orderRefundDto);

    // 同意操作(不是平台处理介入退款时)
    if (!orderRefundParam.getIsPaltform()) {
        orderRefundDto.setReturnMoneySts(ReturnMoneyStsType.RECEIVE.value());
    }
    // 同意退款
    agreeRefund(orderRefundDto);
} else {
    disagreeRefund(orderRefundParam, orderRefundDto);
}

跟上面的仅退款接口是一样的,都是先减少了商家结算金额然后调用了submitWxRefund方法进行了退款操作。

退款回调

我们来到submitWxRefund方法里面的doRefund方法,这里的退款方法有一行:

returnRefundInfo.setNotifyUrl("/order/refund/result/" + payInfo.getPaySysType() + "/" + payInfo.getPayType());

并且payManager.doRefund方法中有几处

Domain domain = shopConfig.getDomain();
wxPayRefundRequest.setNotifyUrl(domain.getMultishopDomainName() + refundInfo.getNotifyUrl());

这里面规定的,退款回调的地址,这也就是为什么需要在平台端的后台配置的基础配置中设置商家端接口域名的原因

其中只要是退款回调,均回调至商家服务的OrderRefundController

  • 验签

因为回调域名是公网可以访问的且不需要校验权限,所以退款回调是需要做一些验证的。不然谁都可以退款订单回调的地址,实在是十分危险。

其实PayNoticeController这里对订单的签名进行了校验

refundInfoBo = payManager.validateAndGetRefundInfo(request,paySysType, PayType.instance(payType), data);
  • 更新退款状态

我们看看refundSuccess这里的业务核心方法:

@Override
@Transactional(rollbackFor = Exception.class)
public void refundSuccess(RefundInfo refundInfo) {
    // 已经退款
    if (Objects.equals(refundInfo.getRefundStatus(), RefundStatusEnum.SUCCEED.value())) {
        return;
    }
    refundInfo.setCallbackContent(refundInfo.getCallbackContent());
    refundInfo.setCallbackTime(new Date());
    refundInfo.setRefundStatus(RefundStatusEnum.SUCCEED.value());
    refundInfoMapper.update(refundInfo, new LambdaUpdateWrapper<RefundInfo>().eq(RefundInfo::getRefundId, refundInfo.getRefundId()));
    //重复支付直接退款 不需要验证退款信息
    if(Objects.equals(refundInfo.getRefundType(), RefundKindEnum.REPEAT_PAY.value())) {
        return;
    }
    // 验证退款信息
    eventPublisher.publishEvent(new RefundInfoEvent(refundInfo));
    refundInfo.setStockKeys(refundInfo.getStockKeys());
}

可以看到我们在校验通过后,就进行了修改退款的状态,并把修改后的数据保存到数据库中了。

退款定时任务

在我们某些情况下退款处理成功但是没有收到退款回调,或者退款处理成功但是申请退款失败,难道我们的退款单要一直卡在等待回调的状态吗?此时我们也有定时任务进行处理,首先我们来到OrderRefundTask

@XxlJob("refundRequest")
public void refundRequest() {
    log.info("==============发放退款定时任务开始===================");
    // 找到十分钟之前已经处理退款但是发放退款失败的退款订单
    List<OrderRefundDto> shouldRefundRequestList = orderRefundService.listShouldRefundRequest();
    if (CollUtil.isEmpty(shouldRefundRequestList)) {
        return;
    }
    // 给发放退款失败的订单重新发放一次
    for (OrderRefundDto orderRefundDto : shouldRefundRequestList) {
        RLock lock = redissonClient.getLock(LOCK_REFUND_HANDLE_PREFIX + orderRefundDto.getRefundSn());
        try {
            lock.lock();
            OrderRefund dbOrderRefund = orderRefundService.getById(orderRefundDto.getRefundId());
            if (Objects.equals(dbOrderRefund.getReturnMoneySts(), ReturnMoneyStsType.PROCESSING.value())
                    || Objects.equals(dbOrderRefund.getReturnMoneySts(), ReturnMoneyStsType.RECEIVE.value())) {
                // 商家处理状态,查询退款是否成功
                RefundInfo dbRefundInfo = refundInfoService.getByRefundId(dbOrderRefund.getRefundSn());
                if (Objects.nonNull(dbRefundInfo)) {
                    // 申请退款成功,没有回调
                    log.info("退款单号为{}的订单开始进行退款查单", orderRefundDto.getRefundSn());
                    RefundInfoBo refundInfoBo = payManager.getRefundInfo(PayType.instance(dbRefundInfo.getPayType()), dbRefundInfo.getPayNo(), dbRefundInfo.getPaySysType(), dbRefundInfo.getRefundId());
                    if (refundInfoBo.getIsRefundSuccess()) {
                        // 退款回调
                        refundSuccess(dbRefundInfo, refundInfoBo);
                    }
                } else {
                    // 申请退款失败,重新申请
                    log.info("退款单号为{}的订单开始进行发放退款", orderRefundDto.getRefundSn());
                    // 提交退款请求
                    orderRefundService.submitWxRefund(orderRefundDto);
                }
            }
        } catch (Exception e) {
            log.info("退款单号:{}订单发放退款失败,原因为:{}", orderRefundDto.getRefundSn(), e.getMessage());
        } finally {
            lock.unlock();
        }
    }
    this.refundOrderStock();
    log.info("==============发放退款定时任务结束===================");
}

在这里可以看到我们首先查询十分钟之前符合处理条件的退款订单,在申请退款成功但是没有收到回调时进行处理退款单的状态,或者在退款处理成功但是申请退款失败进行重新发起退款申请,通过这些就可以保证我们的退款整个流程就不会有问题了。

最后还有支付宝电脑支付订单全额退款成功是没有退款回调的,我们也是通过定时任务来查询订单在支付版那边的退款情况来进行处理我们的退款单状态。

@XxlJob("updateAliPayRefundStatus")
public void updateAliPayRefundStatus() {
    log.info("==============支付宝退款订单查单开始===================");
    List<RefundInfo> refundInfoList = refundInfoService.listAliPayRefund();
    if (CollUtil.isEmpty(refundInfoList)) {
        return;
    }
    for (RefundInfo dbRefundInfo : refundInfoList) {
        RLock lock = redissonClient.getLock(LOCK_REFUND_HANDLE_PREFIX + dbRefundInfo.getRefundId());
        try {
            lock.lock();
            RefundInfoBo refundInfoBo = payManager.getRefundInfo(PayType.instance(dbRefundInfo.getPayType()), dbRefundInfo.getPayNo(), dbRefundInfo.getPaySysType(),dbRefundInfo.getRefundId());
            if (!refundInfoBo.getIsRefundSuccess()) {
                continue;
            }
            // 退款回调
            refundSuccess(dbRefundInfo, refundInfoBo);
        } finally {
            lock.unlock();
        }
    }
    log.info("==============支付宝退款订单查单结束===================");
}