订单初版—5.售后退货链路中的技术问题说明文档

大纲

1.售后退货业务流程

2.售后退货业务链路代码

3.重复发起售后退货请求的接口幂等处理

4.发起售后退货申请的代码流程

5.释放库存、发起退款和释放优惠券逻辑

6.售后退货全链路数据一致性问题分析

7.客服查询售后工单进行审核的业务流程

8.撤销退货申请时使用分布式锁处理并发问题

9.仓储缺品退款场景的流程

10.仓储缺品的退款流程的数据一致性问题

1.售后退货业务流程

2.售后退货业务链路代码

(1)处理售后申请的接口

(2)客服审核售后的接口

下面是v1版本的代码:

(1)处理售后申请的接口

一.使用场景

售后申请接收的是⽤户发起的退货请求,退货分整笔退和单笔退。

⽬前订单的格式是:⼀笔订单对应多个条⽬。订单系统仅⽀持默认每次退货是退⼀个条⽬,不考虑退某条目部分数量的场景。

例如⼀笔订单包含两个条⽬,A条⽬商品数量是2,B条⽬商品数量是1。⽤户退货每次只退⼀个。如果退A,则不考虑A的商品数量,默认就是全退。如果退B,同理。如果已经先退掉了A,再次退B时发现本次退完之后,该笔订单的全部条⽬都已经退掉,则在B中补退运费,在B退成功以后回退给⽤户优惠券。

⽬前此业务场景的缺陷:不⽀持部分退、拆单退,所以判断是否是退货最后⼀单的逻辑⽐较简单。最后⼀笔退货补退运费时,没有⽣成运费的售后单。

以上述例⼦为例:先退掉A,⽣成⼀笔售后单,记录的实际退款⾦额是A的退款⾦额。再退掉B,此时B的实际退款⾦额 = 运费 + B的退款⾦额,⽣成⼀笔售后单。改造点:单独再⽣成⼀张运费的售后单。

二.具体实现

java 复制代码
@RestController
@RequestMapping("/afterSale")
public class AfterSaleController {
    @Autowired
    private OrderAfterSaleService orderAfterSaleService;

    @Autowired
    private RedisLock redisLock;
    ...

    //用户发起退货售后
    @PostMapping("/applyAfterSale")
    public JsonResult<Boolean> applyAfterSale(@RequestBody ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //分布式锁
        String orderId = returnGoodsOrderRequest.getOrderId();
        String key = RedisLockKeyConstants.REFUND_KEY + orderId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.PROCESS_AFTER_SALE_RETURN_GOODS);
        }
        try {
            return orderAfterSaleService.processApplyAfterSale(returnGoodsOrderRequest);
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //处理售后申请入口
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JsonResult<Boolean> processApplyAfterSale(ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //分布式锁
        String orderId = returnGoodsOrderRequest.getOrderId();
        String key = RedisLockKeyConstants.REFUND_KEY + orderId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.PROCESS_AFTER_SALE_RETURN_GOODS);
        }

        try {
            //参数校验
            checkAfterSaleRequestParam(returnGoodsOrderRequest);
            //1.售后单状态验证
            //用order id和sku code查到售后id
            String skuCode = returnGoodsOrderRequest.getSkuCode();
            List<AfterSaleItemDO> orderIdAndSkuCodeList = afterSaleItemDAO.getOrderIdAndSkuCode(orderId, skuCode);
            if (!orderIdAndSkuCodeList.isEmpty()) {
                Long afterSaleId = orderIdAndSkuCodeList.get(0).getAfterSaleId();
                //用售后id查售后支付表
                AfterSaleRefundDO afterSaleRefundDO = afterSaleRefundDAO.findOrderAfterSaleStatus(String.valueOf(afterSaleId));
                //幂等校验:售后支付表里存在当前这笔未退款的记录 不能重复发起售后
                if (orderId.equals(afterSaleRefundDO.getOrderId()) && RefundStatusEnum.UN_REFUND.getCode().equals(afterSaleRefundDO.getRefundStatus())) {
                    throw new OrderBizException(OrderErrorCodeEnum.PROCESS_APPLY_AFTER_SALE_CANNOT_REPEAT);
                }
                //业务校验:已完成支付退款的订单不能再次重复发起手动售后
                AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
                if (afterSaleInfoDO.getAfterSaleStatus() > AfterSaleStatusEnum.REFUNDING.getCode()) {
                    throw new OrderBizException(OrderErrorCodeEnum.PROCESS_APPLY_AFTER_SALE_CANNOT_REPEAT);
                }
            }
            //2.封装数据
            ReturnGoodsAssembleRequest returnGoodsAssembleRequest = buildReturnGoodsData(returnGoodsOrderRequest);
            //3.计算退货金额
            returnGoodsAssembleRequest = calculateReturnGoodsAmount(returnGoodsAssembleRequest);
            //4.售后数据落库
            insertReturnGoodsAfterSale(returnGoodsAssembleRequest, AfterSaleStatusEnum.COMMITED.getCode());
            //5.发起客服审核
            CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest = returnGoodsAssembleRequest.clone(new CustomerReviewReturnGoodsRequest());
            customerApi.customerAudit(customerReviewReturnGoodsRequest);
        } catch (BaseBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

//售后退货入参
@Data
public class ReturnGoodsOrderRequest extends AbstractObject implements Serializable {
    private static final long serialVersionUID = 4893844329777688246L;
    private String orderId;//订单号
    private Integer businessIdentifier;//接入方业务线标识  1, "自营商城"
    private String userId;//用户id
    private Integer returnGoodsCode;//退货原因选项
    private String returnGoodsDesc;//退货原因说明
    private String skuCode;//退货sku编号
}

(2)客服审核售后的接口

一.使用场景

客服审核⽤户的售后请求,分为审核通过和审核拒绝。审核通过后,⾃动发起退款。

二.具体实现

scss 复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //接收客服审核结果入口
    @Override
    public JsonResult<Boolean> receiveCustomerAuditResult(CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        //分布式锁
        String orderId = customerReviewReturnGoodsRequest.getOrderId();
        String key = RedisLockKeyConstants.REFUND_KEY + orderId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.PROCESS_AFTER_SALE_RETURN_GOODS);
        }

        try {
            //组装售后数据
            CustomerAuditAssembleRequest customerAuditAssembleRequest = new CustomerAuditAssembleRequest();
            Long afterSaleId = customerReviewReturnGoodsRequest.getAfterSaleId();
            customerAuditAssembleRequest.setAfterSaleId(afterSaleId);
            customerAuditAssembleRequest.setAfterSaleRefundId(customerReviewReturnGoodsRequest.getAfterSaleRefundId());

            Integer auditResult = customerReviewReturnGoodsRequest.getAuditResult();
            customerAuditAssembleRequest.setReviewTime(new Date());
            customerAuditAssembleRequest.setReviewSource(CustomerAuditSourceEnum.SELF_MALL.getCode());
            customerAuditAssembleRequest.setReviewReasonCode(auditResult);
            customerAuditAssembleRequest.setAuditResultDesc(customerReviewReturnGoodsRequest.getAuditResultDesc());
            AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(customerReviewReturnGoodsRequest.getAfterSaleId());

            //幂等校验:防止客服重复审核订单
            if (afterSaleInfoDO.getAfterSaleStatus() > AfterSaleStatusEnum.COMMITED.getCode()) {
                throw new OrderBizException(OrderErrorCodeEnum.CUSTOMER_AUDIT_CANNOT_REPEAT);
            }

            if (CustomerAuditResult.ACCEPT.getCode().equals(auditResult)) {
                //客服审核通过
                AfterSaleItemDO afterSaleItemDO = afterSaleItemDAO.getOrderIdAndAfterSaleId(orderId, afterSaleId);
                if (afterSaleItemDO == null) {
                    throw new OrderBizException(OrderErrorCodeEnum.AFTER_SALE_ITEM_CANNOT_NULL);
                }

                //更新售后信息
                customerAuditAssembleRequest.setReviewReason(CustomerAuditResult.ACCEPT.getMsg());
                afterSaleInfoDAO.updateCustomerAuditAfterSaleResult(AfterSaleStatusEnum.REVIEW_PASS.getCode(), customerAuditAssembleRequest);
                AfterSaleLogDO afterSaleLogDO = afterSaleOperateLogFactory.get(afterSaleInfoDO, AfterSaleStatusChangeEnum.AFTER_SALE_CUSTOMER_AUDIT_PASS);
                afterSaleLogDAO.save(afterSaleLogDO);

                //执行退款
                ActualRefundMessage actualRefundMessage = new ActualRefundMessage();
                actualRefundMessage.setAfterSaleRefundId(customerAuditAssembleRequest.getAfterSaleRefundId());
                actualRefundMessage.setOrderId(customerAuditAssembleRequest.getOrderId());
                actualRefundMessage.setLastReturnGoods(customerAuditAssembleRequest.isLastReturnGoods());
                actualRefundMessage.setAfterSaleId(customerAuditAssembleRequest.getAfterSaleId());

                //发送实际退款消息到MQ
                defaultProducer.sendMessage(RocketMqConstant.ACTUAL_REFUND_TOPIC, JSONObject.toJSONString(actualRefundMessage), "实际退款");
            } else if (CustomerAuditResult.REJECT.getCode().equals(auditResult)) {
                customerAuditAssembleRequest.setReviewReason(CustomerAuditResult.REJECT.getMsg());
                //审核拒绝更新售后信息
                afterSaleInfoDAO.updateCustomerAuditAfterSaleResult(AfterSaleStatusEnum.REVIEW_REJECTED.getCode(), customerAuditAssembleRequest);
                AfterSaleLogDO afterSaleLogDO = afterSaleOperateLogFactory.get(afterSaleInfoDO, AfterSaleStatusChangeEnum.AFTER_SALE_CUSTOMER_AUDIT_REJECT);
                afterSaleLogDAO.save(afterSaleLogDO);
            }
        } catch (BaseBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

//客服审核退货申请入参
@Data
public class CustomerReviewReturnGoodsRequest extends AbstractObject implements Serializable {
    private static final long serialVersionUID = 5541950604615013941L;
    private Long afterSaleId;//售后id
    private String customerId;//客服id
    private Integer auditResult;//审核结果 1 审核通过  2 审核拒绝
    private Long afterSaleRefundId;//售后支付单id
    private String orderId;//订单id
    private String auditResultDesc;//客服审核结果描述信息
}

//实际退款消费者
@Component
public class ActualRefundListener implements MessageListenerConcurrently {
    @Autowired
    private OrderAfterSaleService orderAfterSaleService;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                String message = new String(messageExt.getBody());
                ActualRefundMessage actualRefundMessage = JSONObject.parseObject(message, ActualRefundMessage.class);
                log.info("ActualRefundConsumer message:{}", message);
                JsonResult<Boolean> jsonResult = orderAfterSaleService.refundMoney(actualRefundMessage);
                if (!jsonResult.getSuccess()) {
                    throw new OrderBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //执行退款
    @Override
    public JsonResult<Boolean> refundMoney(ActualRefundMessage actualRefundMessage) {
        Long afterSaleId = actualRefundMessage.getAfterSaleId();
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        try {
            boolean lock = redisLock.lock(key);
            if (!lock) {
                throw new OrderBizException(OrderErrorCodeEnum.REFUND_MONEY_REPEAT);
            }
            AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(actualRefundMessage.getAfterSaleId());
            Long afterSaleRefundId = actualRefundMessage.getAfterSaleRefundId();
            AfterSaleRefundDO afterSaleRefundDO = afterSaleRefundDAO.getById(afterSaleRefundId);
            //1.封装调用支付退款接口的数据
            PayRefundRequest payRefundRequest = buildPayRefundRequest(actualRefundMessage, afterSaleRefundDO);
            //2.执行退款
            if (!payApi.executeRefund(payRefundRequest)) {
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_REFUND_AMOUNT_FAILED);
            }
            //3.售后记录状态
            //执行退款更新售后信息
            updateAfterSaleStatus(afterSaleInfoDO, AfterSaleStatusEnum.REVIEW_PASS.getCode(), AfterSaleStatusEnum.REFUNDING.getCode());
            //4.退货的最后一笔退优惠券
            if (actualRefundMessage.isLastReturnGoods()) {
                //释放优惠券权益
                String orderId = actualRefundMessage.getOrderId();
                OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(orderId);
                CancelOrderReleaseUserCouponRequest cancelOrderReleaseUserCouponRequest = orderInfoDO.clone(CancelOrderReleaseUserCouponRequest.class);
                defaultProducer.sendMessage(RocketMqConstant.CANCEL_RELEASE_PROPERTY_TOPIC, JSONObject.toJSONString(cancelOrderReleaseUserCouponRequest), "释放优惠券权益");
            }
            return JsonResult.buildSuccess(true);
        } catch (OrderBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

技术核心其实都是:分布式锁 + Seata分布式事务 + RocketMQ异步化。

3.重复发起售后退货请求的接口幂等处理

(1)处理用户发起的售后退货请求时会使用分布式锁

(2)通过售后单记录是否存在或已撤销来实现幂等

每次发起售后申请退货,都是针对一个订单条目进行的。比如一个订单包含5个SKU1、6个SKU2。那么申请退货只能是:5个SKU1或6个SKU2,或者全部退货。

用户发起售后退货申请时,可能会出现多次重复发送申请请求。因此,必须对调用的接口使用分布式锁 + 状态前置校验进行幂等处理。加分布式锁是为了防止并非请求进来后可能会避开状态的前置校验。

下面使用v2版本的代码:

(1)处理用户发起的售后退货请求时会使用分布式锁

less 复制代码
@RestController
@RequestMapping("/afterSale")
public class AfterSaleController {
    @Autowired
    private OrderAfterSaleService orderAfterSaleService;

    @Autowired
    private RedisLock redisLock;
    ...

    //用户发起退货售后
    @PostMapping("/applyAfterSale")
    public JsonResult<Boolean> applyAfterSale(@RequestBody ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //分布式锁
        String orderId = returnGoodsOrderRequest.getOrderId();
        String key = RedisLockKeyConstants.REFUND_KEY + orderId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.PROCESS_AFTER_SALE_RETURN_GOODS);
        }
        try {
            return orderAfterSaleService.processApplyAfterSale(returnGoodsOrderRequest);
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

(2)通过售后单记录是否存在或已撤销来实现幂等

检查售后申请的逻辑如下:

第一种场景: 订单条目A是第一次发起手动售后,此时售后订单条目表没有该订单的记录,orderIdAndSkuCodeList是空,正常执行后面的售后逻辑。

第二种场景: 订单条目A已发起过售后,非"撤销成功"状态的售后单不允许重复发起售后。

scss 复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //当前业务限制说明:
    //目前业务限定,一笔订单包含多笔订单条目,每次手动售后只能退一笔条目,不支持单笔条目多次退不同数量
    //举例:
    //一笔订单包含订单条目A(购买数量10)和订单条目B(购买数量1),每一次可单独发起售后订单条目A or 售后订单条目B
    //如果是售后订单条目A,那么就是把A中购买数量10全部退掉
    //如果是售后订单条目B,那么就是把B中购买数量1全部退款
    //暂不支持第一次退A中的3条,第二次退A中的2条,第三次退A中的5条这种退法
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JsonResult<Boolean> processApplyAfterSale(ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //参数校验
        checkAfterSaleRequestParam(returnGoodsOrderRequest);
        try {
            //1.售后单状态验证
            //用order id和sku code查到售后id
            String orderId = returnGoodsOrderRequest.getOrderId();
            String skuCode = returnGoodsOrderRequest.getSkuCode();
            //场景校验逻辑:
            //第一种场景:订单条目A是第一次发起手动售后,此时售后订单条目表没有该订单的记录,orderIdAndSkuCodeList 是空,正常执行后面的售后逻辑
            //第二种场景:订单条目A已发起过售后,非"撤销成功"状态的售后单不允许重复发起售后
            List<AfterSaleItemDO> orderIdAndSkuCodeList = afterSaleItemDAO.getOrderIdAndSkuCode(orderId, skuCode);
            if (!orderIdAndSkuCodeList.isEmpty()) {
                //查询订单条目所属的售后单状态
                Long afterSaleId = orderIdAndSkuCodeList.get(0).getAfterSaleId();
                AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
                if (!AfterSaleStatusEnum.REVOKE.getCode().equals(afterSaleInfoDO.getAfterSaleStatus())) {
                    //非"撤销成功"状态的售后单不能重复发起售后
                    throw new OrderBizException(OrderErrorCodeEnum.PROCESS_APPLY_AFTER_SALE_CANNOT_REPEAT);
                }
            }
            //2.封装数据
            ReturnGoodsAssembleRequest returnGoodsAssembleRequest = buildReturnGoodsData(returnGoodsOrderRequest);
            //3.计算退款金额
            returnGoodsAssembleRequest = calculateReturnGoodsAmount(returnGoodsAssembleRequest);
            //4.售后数据落库
            insertReturnGoodsAfterSale(returnGoodsAssembleRequest, AfterSaleStatusEnum.COMMITED.getCode());
            //5.发起客服审核
            CustomerReceiveAfterSaleRequest customerReceiveAfterSaleRequest = returnGoodsAssembleRequest.clone(new CustomerReceiveAfterSaleRequest());
            defaultProducer.sendMessage(RocketMqConstant.AFTER_SALE_CUSTOMER_AUDIT_TOPIC,
                JSONObject.toJSONString(customerReceiveAfterSaleRequest), "售后申请发送给客服审核");
        } catch (BaseBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

4.发起售后退货申请的代码流程

(1)查询相关数据集成和封装售后退货请求数据

(2)计算退款金额

(3)插入售后数据

(4)发送客服审核消息

(5)客服审核退货申请并通知订单系统审核结果

整体流程如下:

(1)查询相关数据集成和封装售后退货请求数据

ini 复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //封装售后退货请求数据
    private ReturnGoodsAssembleRequest buildReturnGoodsData(ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        ReturnGoodsAssembleRequest returnGoodsAssembleRequest = returnGoodsOrderRequest.clone(ReturnGoodsAssembleRequest.class);
        String orderId = returnGoodsAssembleRequest.getOrderId();

        //封装 订单信息
        OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(orderId);
        OrderInfoDTO orderInfoDTO = orderInfoDO.clone(OrderInfoDTO.class);
        returnGoodsAssembleRequest.setOrderInfoDTO(orderInfoDTO);

        //封装 订单条目
        List<OrderItemDO> orderItemDOList = orderItemDAO.listByOrderId(orderId);
        List<OrderItemDTO> orderItemDTOList = Lists.newArrayList();
        for (OrderItemDO orderItemDO : orderItemDOList) {
            OrderItemDTO orderItemDTO = orderItemDO.clone(new OrderItemDTO());
            orderItemDTOList.add(orderItemDTO);
        }
        returnGoodsAssembleRequest.setOrderItemDTOList(orderItemDTOList);

        //封装 订单售后条目
        List<AfterSaleItemDO> afterSaleItemDOList = afterSaleItemDAO.listByOrderId(Long.valueOf(orderId));
        List<AfterSaleOrderItemDTO> afterSaleOrderItemRequestList = Lists.newArrayList();
        for (AfterSaleItemDO afterSaleItemDO : afterSaleItemDOList) {
            AfterSaleOrderItemDTO afterSaleOrderItemDTO = afterSaleItemDO.clone(AfterSaleOrderItemDTO.class);
            afterSaleOrderItemRequestList.add(afterSaleOrderItemDTO);
        }
        returnGoodsAssembleRequest.setAfterSaleOrderItemDTOList(afterSaleOrderItemRequestList);

        return returnGoodsAssembleRequest;
    }
    ...
}

(2)计算退款金额

需要注意:如果当前退货的订单条目是该订单的最后一个条目,或者在这次退货申请后该订单不存在订单条目了,那么需要把运费也计算在退款金额上。

ini 复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //一笔订单只有一个条目:整笔退
    //一笔订单有多个条目,每次退一条,退完最后一条补退运费和优惠券
    public ReturnGoodsAssembleRequest calculateReturnGoodsAmount(ReturnGoodsAssembleRequest returnGoodsAssembleRequest) {
        String skuCode = returnGoodsAssembleRequest.getSkuCode();
        String orderId = returnGoodsAssembleRequest.getOrderId();
        //为便于以后扩展,这里封装成list
        List<OrderItemDTO> refundOrderItemDTOList = Lists.newArrayList();
        List<OrderItemDTO> orderItemDTOList = returnGoodsAssembleRequest.getOrderItemDTOList();
        List<AfterSaleOrderItemDTO> afterSaleOrderItemDTOList = returnGoodsAssembleRequest.getAfterSaleOrderItemDTOList();
        //订单条目数
        int orderItemNum = orderItemDTOList.size();
        //售后订单条目数
        int afterSaleOrderItemNum = afterSaleOrderItemDTOList.size();
        //订单条目数,只有一条,退整笔 ,本次售后类型:全部退款
        if (orderItemNum == 1) {
            OrderItemDTO orderItemDTO = orderItemDTOList.get(0);
            returnGoodsAssembleRequest.setAfterSaleType(AfterSaleTypeEnum.RETURN_MONEY.getCode());
            return calculateWholeOrderFefundAmount(
                orderId,
                orderItemDTO.getPayAmount(),
                orderItemDTO.getOriginAmount(),
                returnGoodsAssembleRequest
            );
        }
        //该笔订单还有其他条目,本次售后类型:退货
        returnGoodsAssembleRequest.setAfterSaleType(AfterSaleTypeEnum.RETURN_GOODS.getCode());
        //skuCode和orderId查询本次要退的条目
        OrderItemDO orderItemDO = orderItemDAO.getOrderItemBySkuIdAndOrderId(orderId, skuCode);
        //该笔订单条目数 = 已存的售后订单条目数 + 本次退货的条目(每次退1条)
        if (orderItemNum == afterSaleOrderItemNum + 1) {
            //当前条目是订单的最后一笔
            returnGoodsAssembleRequest = calculateWholeOrderFefundAmount(
                orderId,
                orderItemDO.getPayAmount(),
                orderItemDO.getOriginAmount(),
                returnGoodsAssembleRequest
            );
        } else {
            //只退当前条目
            returnGoodsAssembleRequest.setReturnGoodAmount(orderItemDO.getPayAmount());
            returnGoodsAssembleRequest.setApplyRefundAmount(orderItemDO.getOriginAmount());
            returnGoodsAssembleRequest.setLastReturnGoods(false);
        }
        refundOrderItemDTOList.add(orderItemDO.clone(OrderItemDTO.class));
        returnGoodsAssembleRequest.setRefundOrderItemDTO(refundOrderItemDTOList);

        return returnGoodsAssembleRequest;
    }

    private ReturnGoodsAssembleRequest calculateWholeOrderFefundAmount(String orderId, Integer payAmount, Integer originAmount, ReturnGoodsAssembleRequest returnGoodsAssembleRequest) {
        //模拟取运费
        OrderAmountDO deliveryAmount = orderAmountDAO.getOne(orderId, AmountTypeEnum.SHIPPING_AMOUNT.getCode());
        Integer freightAmount = (deliveryAmount == null || deliveryAmount.getAmount() == null) ? 0 : deliveryAmount.getAmount();

        //最终退款金额 = 实际退款金额 + 运费
        Integer returnGoodAmount = payAmount + freightAmount;
        returnGoodsAssembleRequest.setReturnGoodAmount(returnGoodAmount);
        returnGoodsAssembleRequest.setApplyRefundAmount(originAmount);
        returnGoodsAssembleRequest.setAfterSaleType(AfterSaleTypeEnum.RETURN_MONEY.getCode());
        returnGoodsAssembleRequest.setLastReturnGoods(true);

        return returnGoodsAssembleRequest;
    }
    ...
}

(3)插入售后数据

scss 复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //封装售后退货请求数据
    private void insertReturnGoodsAfterSale(ReturnGoodsAssembleRequest returnGoodsAssembleRequest, Integer afterSaleStatus) {
        OrderInfoDTO orderInfoDTO = returnGoodsAssembleRequest.getOrderInfoDTO();
        OrderInfoDO orderInfoDO = orderInfoDTO.clone(OrderInfoDO.class);
        Integer afterSaleType = returnGoodsAssembleRequest.getAfterSaleType();

        //售后退货过程中的 申请退款金额 和 实际退款金额 是计算出来的,金额有可能不同
        AfterSaleInfoDO afterSaleInfoDO = new AfterSaleInfoDO();
        Integer applyRefundAmount = returnGoodsAssembleRequest.getApplyRefundAmount();
        afterSaleInfoDO.setApplyRefundAmount(applyRefundAmount);
        Integer returnGoodAmount = returnGoodsAssembleRequest.getReturnGoodAmount();
        afterSaleInfoDO.setRealRefundAmount(returnGoodAmount);

        //1.新增售后订单表
        Integer cancelOrderAfterSaleStatus = AfterSaleStatusEnum.COMMITED.getCode();
        String afterSaleId = insertReturnGoodsAfterSaleInfoTable(orderInfoDO, afterSaleType, cancelOrderAfterSaleStatus, afterSaleInfoDO);
        returnGoodsAssembleRequest.setAfterSaleId(afterSaleId);
        //2.新增售后条目表
        insertAfterSaleItemTable(orderInfoDO.getOrderId(), returnGoodsAssembleRequest.getRefundOrderItemDTO(), afterSaleId);
        //3.新增售后变更表
        insertReturnGoodsAfterSaleLogTable(afterSaleId, AfterSaleStatusEnum.UN_CREATED.getCode(), afterSaleStatus);
        //4.新增售后支付表
        AfterSaleRefundDO afterSaleRefundDO = insertAfterSaleRefundTable(orderInfoDTO, afterSaleId, afterSaleInfoDO);
        returnGoodsAssembleRequest.setAfterSaleRefundId(afterSaleRefundDO.getId());
    }

    //售后退货流程 插入订单销售表
    private String insertReturnGoodsAfterSaleInfoTable(OrderInfoDO orderInfoDO, Integer afterSaleType, Integer cancelOrderAfterSaleStatus, AfterSaleInfoDO afterSaleInfoDO) {
        //生成售后订单号
        String afterSaleId = orderNoManager.genOrderId(OrderNoTypeEnum.AFTER_SALE.getCode(), orderInfoDO.getUserId());
        afterSaleInfoDO.setAfterSaleId(Long.valueOf(afterSaleId));
        afterSaleInfoDO.setBusinessIdentifier(BusinessIdentifierEnum.SELF_MALL.getCode());
        afterSaleInfoDO.setOrderId(orderInfoDO.getOrderId());
        afterSaleInfoDO.setOrderSourceChannel(BusinessIdentifierEnum.SELF_MALL.getCode());
        afterSaleInfoDO.setUserId(orderInfoDO.getUserId());
        afterSaleInfoDO.setOrderType(OrderTypeEnum.NORMAL.getCode());
        afterSaleInfoDO.setApplyTime(new Date());
        afterSaleInfoDO.setAfterSaleStatus(cancelOrderAfterSaleStatus);

        //用户售后退货的业务值
        afterSaleInfoDO.setApplySource(AfterSaleApplySourceEnum.USER_RETURN_GOODS.getCode());
        afterSaleInfoDO.setRemark(ReturnGoodsTypeEnum.AFTER_SALE_RETURN_GOODS.getMsg());
        afterSaleInfoDO.setApplyReasonCode(AfterSaleReasonEnum.USER.getCode());
        afterSaleInfoDO.setApplyReason(AfterSaleReasonEnum.USER.getMsg());
        afterSaleInfoDO.setAfterSaleTypeDetail(AfterSaleTypeDetailEnum.PART_REFUND.getCode());

        //退货流程 只退订单的一笔条目
        if (AfterSaleTypeEnum.RETURN_GOODS.getCode().equals(afterSaleType)) {
            afterSaleInfoDO.setAfterSaleType(AfterSaleTypeEnum.RETURN_GOODS.getCode());
        }

        //退货流程 退订单的全部条目 后续按照整笔退款逻辑处理
        if (AfterSaleTypeEnum.RETURN_MONEY.getCode().equals(afterSaleType)) {
            afterSaleInfoDO.setAfterSaleType(AfterSaleTypeEnum.RETURN_MONEY.getCode());
        }
        afterSaleInfoDAO.save(afterSaleInfoDO);
        log.info("新增订单售后记录,订单号:{},售后单号:{},订单售后状态:{}", orderInfoDO.getOrderId(), afterSaleId, afterSaleInfoDO.getAfterSaleStatus());

        return afterSaleId;
    }

    private void insertAfterSaleItemTable(String orderId, List<OrderItemDTO> orderItemDTOList, String afterSaleId) {
        for (OrderItemDTO orderItem : orderItemDTOList) {
            AfterSaleItemDO afterSaleItemDO = new AfterSaleItemDO();
            afterSaleItemDO.setAfterSaleId(Long.valueOf(afterSaleId));
            afterSaleItemDO.setOrderId(orderId);
            afterSaleItemDO.setSkuCode(orderItem.getSkuCode());
            afterSaleItemDO.setProductName(orderItem.getProductName());
            afterSaleItemDO.setProductImg(orderItem.getProductImg());
            afterSaleItemDO.setReturnQuantity(orderItem.getSaleQuantity());
            afterSaleItemDO.setOriginAmount(orderItem.getOriginAmount());
            afterSaleItemDO.setApplyRefundAmount(orderItem.getOriginAmount());
            afterSaleItemDO.setRealRefundAmount(orderItem.getPayAmount());
            afterSaleItemDAO.save(afterSaleItemDO);
        }
    }

    private void insertReturnGoodsAfterSaleLogTable(String afterSaleId, Integer preAfterSaleStatus, Integer currentAfterSaleStatus) {
        AfterSaleLogDO afterSaleLogDO = new AfterSaleLogDO();
        afterSaleLogDO.setAfterSaleId(afterSaleId);
        afterSaleLogDO.setPreStatus(preAfterSaleStatus);
        afterSaleLogDO.setCurrentStatus(currentAfterSaleStatus);
        //售后退货的业务值
        afterSaleLogDO.setRemark(ReturnGoodsTypeEnum.AFTER_SALE_RETURN_GOODS.getMsg());
        afterSaleLogDAO.save(afterSaleLogDO);
        log.info("新增售后单变更信息, 售后单号:{},状态:PreStatus{},CurrentStatus:{}", afterSaleLogDO.getAfterSaleId(), afterSaleLogDO.getPreStatus(), afterSaleLogDO.getCurrentStatus());
    }

    private AfterSaleRefundDO insertAfterSaleRefundTable(OrderInfoDTO orderInfoDTO, String afterSaleId, AfterSaleInfoDO afterSaleInfoDO) {
        String orderId = orderInfoDTO.getOrderId();
        OrderPaymentDetailDO paymentDetail = orderPaymentDetailDAO.getPaymentDetailByOrderId(orderId);
        AfterSaleRefundDO afterSaleRefundDO = new AfterSaleRefundDO();
        afterSaleRefundDO.setAfterSaleId(afterSaleId);
        afterSaleRefundDO.setOrderId(orderId);
        afterSaleRefundDO.setAccountType(AccountTypeEnum.THIRD.getCode());
        afterSaleRefundDO.setRefundStatus(RefundStatusEnum.UN_REFUND.getCode());
        afterSaleRefundDO.setRemark(RefundStatusEnum.UN_REFUND.getMsg());
        afterSaleRefundDO.setRefundAmount(afterSaleInfoDO.getRealRefundAmount());
        afterSaleRefundDO.setAfterSaleBatchNo(orderId + RandomUtil.genRandomNumber(10));

        if (paymentDetail != null) {
            afterSaleRefundDO.setOutTradeNo(paymentDetail.getOutTradeNo());
            afterSaleRefundDO.setPayType(paymentDetail.getPayType());
        }
        afterSaleRefundDAO.save(afterSaleRefundDO);
        log.info("新增售后支付信息,订单号:{},售后单号:{},状态:{}", orderId, afterSaleId, afterSaleRefundDO.getRefundStatus());

        return afterSaleRefundDO;
    }
    ...
}

(4)发送客服审核消息

v1版本是直接调用客服系统的接口来进行通知,v2版本则会通过先发送客服审核消息到MQ,再由客服系统消费该消息。

java 复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JsonResult<Boolean> processApplyAfterSale(ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //参数校验
        checkAfterSaleRequestParam(returnGoodsOrderRequest);
        try {
            //1.售后单状态验证
            //用order id和sku code查到售后id
            String orderId = returnGoodsOrderRequest.getOrderId();
            String skuCode = returnGoodsOrderRequest.getSkuCode();
            //场景校验逻辑:
            //第一种场景:订单条目A是第一次发起手动售后,此时售后订单条目表没有该订单的记录,orderIdAndSkuCodeList 是空,正常执行后面的售后逻辑
            //第二种场景:订单条目A已发起过售后,非"撤销成功"状态的售后单不允许重复发起售后
            List<AfterSaleItemDO> orderIdAndSkuCodeList = afterSaleItemDAO.getOrderIdAndSkuCode(orderId, skuCode);
            if (!orderIdAndSkuCodeList.isEmpty()) {
                //查询订单条目所属的售后单状态
                Long afterSaleId = orderIdAndSkuCodeList.get(0).getAfterSaleId();
                AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
                if (!AfterSaleStatusEnum.REVOKE.getCode().equals(afterSaleInfoDO.getAfterSaleStatus())) {
                    //非"撤销成功"状态的售后单不能重复发起售后
                    throw new OrderBizException(OrderErrorCodeEnum.PROCESS_APPLY_AFTER_SALE_CANNOT_REPEAT);
                }
            }
            //2.封装数据
            ReturnGoodsAssembleRequest returnGoodsAssembleRequest = buildReturnGoodsData(returnGoodsOrderRequest);
            //3.计算退款金额
            returnGoodsAssembleRequest = calculateReturnGoodsAmount(returnGoodsAssembleRequest);
            //4.售后数据落库
            insertReturnGoodsAfterSale(returnGoodsAssembleRequest, AfterSaleStatusEnum.COMMITED.getCode());
            //5.发起客服审核
            CustomerReceiveAfterSaleRequest customerReceiveAfterSaleRequest = returnGoodsAssembleRequest.clone(new CustomerReceiveAfterSaleRequest());
            defaultProducer.sendMessage(RocketMqConstant.AFTER_SALE_CUSTOMER_AUDIT_TOPIC,
                JSONObject.toJSONString(customerReceiveAfterSaleRequest), "售后申请发送给客服审核");
        } catch (BaseBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

@Configuration
public class ConsumerConfig {
    @Autowired
    private RocketMQProperties rocketMQProperties;

    //客服系统接收售后申请消费者
    @Bean("afterSaleCustomerAudit")
    public DefaultMQPushConsumer afterSaleCustomerAudit(AfterSaleCustomerAuditTopicListener afterSaleCustomerAuditTopicListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(AFTER_SALE_CUSTOMER_AUDIT_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(AFTER_SALE_CUSTOMER_AUDIT_TOPIC, "*");
        consumer.registerMessageListener(afterSaleCustomerAuditTopicListener);
        consumer.start();
        return consumer;
    }
}

//接收订单系统售后审核申请
@Component
public class AfterSaleCustomerAuditTopicListener implements MessageListenerConcurrently {
    @Autowired
    private CustomerService customerService;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                String message = new String(messageExt.getBody());
                log.info("AfterSaleCustomerAuditTopicListener message:{}", message);
                CustomerReceiveAfterSaleRequest customerReceiveAfterSaleRequest = JSON.parseObject(message, CustomerReceiveAfterSaleRequest.class);
                //客服接收订单系统的售后申请
                JsonResult<Boolean> jsonResult = customerService.receiveAfterSale(customerReceiveAfterSaleRequest);
                if (!jsonResult.getSuccess()) {
                    throw new CustomerBizException(CustomerErrorCodeEnum.PROCESS_RECEIVE_AFTER_SALE);
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}

@Service
public class CustomerServiceImpl implements CustomerService {
    ...
    @Override
    public JsonResult<Boolean> receiveAfterSale(CustomerReceiveAfterSaleRequest customerReceiveAfterSaleRequest) {
        //1.校验入参
        checkCustomerReceiveAfterSaleRequest(customerReceiveAfterSaleRequest);
        //2.分布式锁
        String afterSaleId = customerReceiveAfterSaleRequest.getAfterSaleId();
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new CustomerBizException(CustomerErrorCodeEnum.PROCESS_RECEIVE_AFTER_SALE_REPEAT);
        }
        try {
            //3.保存售后申请数据
            CustomerReceivesAfterSaleInfoDO customerReceivesAfterSaleInfoDO = customerReceiveAfterSaleRequest.clone(CustomerReceivesAfterSaleInfoDO.class);
            customerReceivesAfterSaleInfoDAO.save(customerReceivesAfterSaleInfoDO);
            log.info("客服保存售后申请信息成功,afterSaleId:{}", customerReceiveAfterSaleRequest.getAfterSaleId());
            return JsonResult.buildSuccess(true);
        } catch (Exception e) {
            throw new CustomerBizException(CustomerErrorCodeEnum.SAVE_AFTER_SALE_INFO_FAILED);
        } finally {
            //4.放锁
            redisLock.unlock(key);
        }
    }
    ...
}

(5)客服审核退货申请并通知订单系统审核结果

typescript 复制代码
@RestController
@RequestMapping("/customer")
public class CustomerController {
    @Autowired
    private CustomerService customerService;

    @Autowired
    private RedisLock redisLock;

    //客服审核售后退货
    @PostMapping("/audit")
    public JsonResult<Boolean> audit(@RequestBody CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        Long afterSaleId = customerReviewReturnGoodsRequest.getAfterSaleId();
        //分布式锁
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new CustomerBizException(CustomerErrorCodeEnum.CUSTOMER_AUDIT_CANNOT_REPEAT);
        }
        try {
            //客服审核
            return customerService.customerAudit(customerReviewReturnGoodsRequest);
        } catch (Exception e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }
}

@Service
public class CustomerServiceImpl implements CustomerService {
    @DubboReference(version = "1.0.0")
    private AfterSaleApi afterSaleApi;
    ...

    @Override
    public JsonResult<Boolean> customerAudit(CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        return afterSaleApi.receiveCustomerAuditResult(customerReviewReturnGoodsRequest);
    }
    ...
}

//订单中心-逆向售后业务接口
@DubboService(version = "1.0.0", interfaceClass = AfterSaleApi.class, retries = 0)
public class AfterSaleApiImpl implements AfterSaleApi {
    ...
    @Override
    public JsonResult<Boolean> receiveCustomerAuditResult(CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        //1.组装接收客服审核结果的数据
        CustomerAuditAssembleRequest customerAuditAssembleResult = buildCustomerAuditAssembleData(customerReviewReturnGoodsRequest);
        //2.客服审核拒绝
        if (CustomerAuditResult.REJECT.getCode().equals(customerAuditAssembleResult.getReviewReasonCode())) {
            //更新 审核拒绝 售后信息
            orderAfterSaleService.receiveCustomerAuditReject(customerAuditAssembleResult);
            return JsonResult.buildSuccess(true);
        }
        //3.客服审核通过
        if (CustomerAuditResult.ACCEPT.getCode().equals(customerAuditAssembleResult.getReviewReasonCode())) {
            String orderId = customerAuditAssembleResult.getOrderId();
            Long afterSaleId = customerAuditAssembleResult.getAfterSaleId();
            AfterSaleItemDO afterSaleItemDO = afterSaleItemDAO.getOrderIdAndAfterSaleId(orderId, afterSaleId);
            if (afterSaleItemDO == null) {
                throw new OrderBizException(OrderErrorCodeEnum.AFTER_SALE_ITEM_CANNOT_NULL);
            }
            //4.组装释放库存参数
            AuditPassReleaseAssetsRequest auditPassReleaseAssetsRequest = buildAuditPassReleaseAssets(afterSaleItemDO, customerAuditAssembleResult, orderId);
            //5.组装事务MQ消息
            TransactionMQProducer producer = defaultProducer.getProducer();
            producer.setTransactionListener(new TransactionListener() {
                @Override
                public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                    try {
                        //更新 审核通过 售后信息
                        orderAfterSaleService.receiveCustomerAuditAccept(customerAuditAssembleResult);
                        return LocalTransactionState.COMMIT_MESSAGE;
                    } catch (Exception e) {
                        log.error("system error", e);
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    }
                }

                @Override
                public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                    Integer customerAuditAfterSaleStatus = orderAfterSaleService.findCustomerAuditAfterSaleStatus(customerAuditAssembleResult.getAfterSaleId());
                    if (AfterSaleStatusEnum.REVIEW_PASS.getCode().equals(customerAuditAfterSaleStatus)) {
                        return LocalTransactionState.COMMIT_MESSAGE;
                    }
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            });
            try {
                Message message = new Message(RocketMqConstant.CUSTOMER_AUDIT_PASS_RELEASE_ASSETS_TOPIC, JSONObject.toJSONString(auditPassReleaseAssetsRequest).getBytes(StandardCharsets.UTF_8));
                //6.发送事务MQ消息 客服审核通过后释放权益资产
                TransactionSendResult result = producer.sendMessageInTransaction(message, auditPassReleaseAssetsRequest);
                if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                    throw new OrderBizException(OrderErrorCodeEnum.SEND_AUDIT_PASS_RELEASE_ASSETS_FAILED);
                }
                return JsonResult.buildSuccess(true);
            } catch (Exception e) {
                throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
            }
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

5.释放库存、发起退款和释放优惠券逻辑

(1)整体流程图

(2)订单系统消费释放资产的消息

(3)库存系统消费释放库存的消息

(4)订单系统消费实际退款的消息

(5)营销系统消费释放优惠券的消息

(1)整体流程图

(2)订单系统消费释放资产的消息

java 复制代码
@Configuration
public class ConsumerConfig {
    @Autowired
    private RocketMQProperties rocketMQProperties;
    ...

    //释放资产消息消费者
    @Bean("auditPassReleaseAssetsConsumer")
    public DefaultMQPushConsumer auditPassReleaseAssetsConsumer(AuditPassReleaseAssetsListener auditPassReleaseAssetsListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CUSTOMER_AUDIT_PASS_RELEASE_ASSETS_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(CUSTOMER_AUDIT_PASS_RELEASE_ASSETS_TOPIC, "*");
        consumer.registerMessageListener(auditPassReleaseAssetsListener);
        consumer.start();
        return consumer;
    }
    ...
}

//消费客服审核通过后发送到MQ的释放资产消息
@Component
public class AuditPassReleaseAssetsListener implements MessageListenerConcurrently {
    @Autowired
    private DefaultProducer defaultProducer;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                //1.消费到释放资产message
                String message = new String(messageExt.getBody());
                log.info("AuditPassReleaseAssetsListener message:{}", message);
                AuditPassReleaseAssetsRequest auditPassReleaseAssetsRequest = JSONObject.parseObject(message, AuditPassReleaseAssetsRequest.class);
                //2.发送释放库存MQ
                ReleaseProductStockDTO releaseProductStockDTO = auditPassReleaseAssetsRequest.getReleaseProductStockDTO();
                ReleaseProductStockRequest releaseProductStockRequest = buildReleaseProductStock(releaseProductStockDTO);
                defaultProducer.sendMessage(RocketMqConstant.CANCEL_RELEASE_INVENTORY_TOPIC,
                    JSONObject.toJSONString(releaseProductStockRequest), "客服审核通过释放库存");
                //3.发送实际退款
                ActualRefundMessage actualRefundMessage = auditPassReleaseAssetsRequest.getActualRefundMessage();
                defaultProducer.sendMessage(RocketMqConstant.ACTUAL_REFUND_TOPIC,
                    JSONObject.toJSONString(actualRefundMessage), "客服审核通过实际退款");
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }

    //组装释放库存数据
    private ReleaseProductStockRequest buildReleaseProductStock(ReleaseProductStockDTO releaseProductStockDTO) {
        List<ReleaseProductStockRequest.OrderItemRequest> orderItemRequestList = new ArrayList<>();
        //补充订单条目
        for (ReleaseProductStockDTO.OrderItemRequest releaseProductOrderItemRequest : releaseProductStockDTO.getOrderItemRequestList()) {
            ReleaseProductStockRequest.OrderItemRequest orderItemRequest = new ReleaseProductStockRequest.OrderItemRequest();
            ReleaseProductStockRequest.OrderItemRequest cloneResult = releaseProductOrderItemRequest.clone(orderItemRequest);
            orderItemRequestList.add(cloneResult);
        }
        ReleaseProductStockRequest releaseProductStockRequest = new ReleaseProductStockRequest();
        releaseProductStockRequest.setOrderId(releaseProductStockDTO.getOrderId());
        releaseProductStockRequest.setOrderItemRequestList(orderItemRequestList);

        return releaseProductStockRequest;
    }
}

(3)库存系统消费释放库存的消息

typescript 复制代码
//监听并消费释放库存消息
@Component
public class ReleaseInventoryListener implements MessageListenerConcurrently {
    @DubboReference(version = "1.0.0")
    private InventoryApi inventoryApi;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
      try {
          for (MessageExt msg : list) {
              String content = new String(msg.getBody(), StandardCharsets.UTF_8);
              log.info("ReleaseInventoryConsumer message:{}", content);
              ReleaseProductStockRequest releaseProductStockRequest = JSONObject.parseObject(content, ReleaseProductStockRequest.class);
              //释放库存
              JsonResult<Boolean> jsonResult = inventoryApi.cancelOrderReleaseProductStock(releaseProductStockRequest);
              if (!jsonResult.getSuccess()) {
                  throw new InventoryBizException(InventoryErrorCodeEnum.CONSUME_MQ_FAILED);
              }
          }
          return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
      } catch (Exception e) {
          log.error("consumer error", e);
          return ConsumeConcurrentlyStatus.RECONSUME_LATER;
      }
    }
}

@DubboService(version = "1.0.0", interfaceClass = InventoryApi.class, retries = 0)
public class InventoryApiImpl implements InventoryApi {
    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private RedisLock redisLock;
    ...

    //回滚库存
    @Override
    public JsonResult<Boolean> cancelOrderReleaseProductStock(ReleaseProductStockRequest releaseProductStockRequest) {
        log.info("开始执行回滚库存,orderId:{}", releaseProductStockRequest.getOrderId());
        List<String> redisKeyList = Lists.newArrayList();
        //分布式锁
        for (ReleaseProductStockRequest.OrderItemRequest orderItemRequest : releaseProductStockRequest.getOrderItemRequestList()) {
            //lockKey: #MODIFY_PRODUCT_STOCK_KEY: skuCode
            String lockKey = RedisLockKeyConstants.MODIFY_PRODUCT_STOCK_KEY + orderItemRequest.getSkuCode();
            redisKeyList.add(lockKey);
        }
        boolean lock = redisLock.multiLock(redisKeyList);
        if (!lock) {
            throw new InventoryBizException(InventoryErrorCodeEnum.RELEASE_PRODUCT_SKU_STOCK_ERROR);
        }
        try {
            //执行释放库存
            Boolean result = inventoryService.releaseProductStock(releaseProductStockRequest);
            return JsonResult.buildSuccess(result);
        } catch (InventoryBizException e) {
            log.error("biz error", e);
            return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
        } catch (Exception e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            //释放分布式锁
            redisLock.unMultiLock(redisKeyList);
        }
    }
    ...
}

(4)订单系统消费实际退款的消息

需要注意:如果本次售后退款的订单条目是当前订单的最后一笔,则发送释放优惠券消息到MQ。

java 复制代码
@Configuration
public class ConsumerConfig {
    @Autowired
    private RocketMQProperties rocketMQProperties;
    ...

    @Bean("actualRefundConsumer")
    public DefaultMQPushConsumer actualRefundConsumer(ActualRefundListener actualRefundListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(ACTUAL_REFUND_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(ACTUAL_REFUND_TOPIC, "*");
        consumer.registerMessageListener(actualRefundListener);
        consumer.start();
        return consumer;
    }
    ...
}

@Component
public class ActualRefundListener implements MessageListenerConcurrently {
    @Autowired
    private OrderAfterSaleService orderAfterSaleService;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                String message = new String(messageExt.getBody());
                ActualRefundMessage actualRefundMessage = JSONObject.parseObject(message, ActualRefundMessage.class);
                log.info("ActualRefundConsumer message:{}", message);
                JsonResult<Boolean> jsonResult = orderAfterSaleService.refundMoney(actualRefundMessage);
                if (!jsonResult.getSuccess()) {
                    throw new OrderBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    @Override
    public JsonResult<Boolean> refundMoney(ActualRefundMessage actualRefundMessage) {
        Long afterSaleId = actualRefundMessage.getAfterSaleId();
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        try {
            boolean lock = redisLock.lock(key);
            if (!lock) {
                throw new OrderBizException(OrderErrorCodeEnum.REFUND_MONEY_REPEAT);
            }
            AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(actualRefundMessage.getAfterSaleId());
            AfterSaleRefundDO afterSaleRefundDO = afterSaleRefundDAO.findOrderAfterSaleStatus(String.valueOf(afterSaleId));
            //1.封装调用支付退款接口的数据
            PayRefundRequest payRefundRequest = buildPayRefundRequest(actualRefundMessage, afterSaleRefundDO);
            //2.执行退款
            if (!payApi.executeRefund(payRefundRequest)) {
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_REFUND_AMOUNT_FAILED);
            }
            //3.本次售后的订单条目是当前订单的最后一笔,发送事务MQ退优惠券,此时isLastReturnGoods标记是true
            if (actualRefundMessage.isLastReturnGoods()) {
                TransactionMQProducer producer = defaultProducer.getProducer();
                //组装事务MQ消息体
                ReleaseUserCouponRequest releaseUserCouponRequest = buildLastOrderReleasesCouponMessage(producer, afterSaleInfoDO, afterSaleId, actualRefundMessage);
                try {
                    //4.发送事务消息 释放优惠券
                    Message message = new Message(RocketMqConstant.CANCEL_RELEASE_PROPERTY_TOPIC, JSONObject.toJSONString(releaseUserCouponRequest).getBytes(StandardCharsets.UTF_8));
                    TransactionSendResult result = producer.sendMessageInTransaction(message, releaseUserCouponRequest);
                    if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                        throw new OrderBizException(OrderErrorCodeEnum.REFUND_MONEY_RELEASE_COUPON_FAILED);
                    }
                    return JsonResult.buildSuccess(true);
                } catch (Exception e) {
                    throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
                }
            } else {
                //当前售后条目非本订单的最后一笔 和 取消订单,在此更新售后状态后流程结束
                //更新售后单状态
                updateAfterSaleStatus(afterSaleInfoDO, AfterSaleStatusEnum.REVIEW_PASS.getCode(), AfterSaleStatusEnum.REFUNDING.getCode());
                return JsonResult.buildSuccess(true);
            }
        } catch (OrderBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

@DubboService(version = "1.0.0", interfaceClass = PayApi.class, retries = 0)
public class PayApiImpl implements PayApi {
    ...
    @Override
    public Boolean executeRefund(PayRefundRequest payRefundRequest) {
        log.info("调用支付接口执行退款,订单号:{},售后单号:{}", payRefundRequest.getOrderId(), payRefundRequest.getAfterSaleId());
        return true;
    }
    ...
}

(5)营销系统消费释放优惠券的消息

typescript 复制代码
@Configuration
public class ConsumerConfig {
    @Autowired
    private RocketMQProperties rocketMQProperties;

    //释放资产权益消息消费者
    @Bean("releaseInventoryConsumer")
    public DefaultMQPushConsumer releaseInventoryConsumer(ReleasePropertyListener releasePropertyListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.RELEASE_PROPERTY_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(RocketMqConstant.CANCEL_RELEASE_PROPERTY_TOPIC, "*");
        consumer.registerMessageListener(releasePropertyListener);
        consumer.start();
        return consumer;
    }
}

@Component
public class ReleasePropertyListener implements MessageListenerConcurrently {
    @DubboReference(version = "1.0.0")
    private MarketApi marketApi;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt msg : list) {
                String content = new String(msg.getBody(), StandardCharsets.UTF_8);
                log.info("ReleasePropertyConsumer message:{}", content);
                ReleaseUserCouponRequest releaseUserCouponRequest = JSONObject.parseObject(content, ReleaseUserCouponRequest.class);
                //释放优惠券
                JsonResult<Boolean> jsonResult = marketApi.releaseUserCoupon(releaseUserCouponRequest);
                if (!jsonResult.getSuccess()) {
                    throw new MarketBizException(MarketErrorCodeEnum.CONSUME_MQ_FAILED);
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}

@DubboService(version = "1.0.0", interfaceClass = MarketApi.class, retries = 0)
public class MarketApiImpl implements MarketApi {
    ...
    //回退用户使用的优惠券
    @Override
    public JsonResult<Boolean> releaseUserCoupon(ReleaseUserCouponRequest releaseUserCouponRequest) {
        log.info("开始执行回滚优惠券,couponId:{}", releaseUserCouponRequest.getCouponId());
        //分布式锁
        String couponId = releaseUserCouponRequest.getCouponId();
        String key = RedisLockKeyConstants.RELEASE_COUPON_KEY + couponId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new MarketBizException(MarketErrorCodeEnum.RELEASE_COUPON_FAILED);
        }
        try {
            //执行释放优惠券
            Boolean result = couponService.releaseUserCoupon(releaseUserCouponRequest);
            return JsonResult.buildSuccess(result);
        } catch (MarketBizException e) {
            log.error("biz error", e);
            return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
        } catch (Exception e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

6.售后退货全链路数据一致性问题分析

(1)更新数据库与发送消息到MQ的数据一致性

(2)消费MQ消息时调用的接口用分布式锁 + 状态前置校验来保证幂等性

(1)更新数据库与发送消息到MQ的数据一致性

售后退货全链路的每一个环节,都会更新数据库 + 发送消息到MQ。此时要用RocketMQ的事务机制,来保证数据库和MQ的数据一致性。比如:订单系统接收到客服的审核结果时,会先更新数据库的售后审核通过信息,然后发送释放资产消息到MQ。否则,如果不用RocketMQ事务机制,且先发送消息到MQ再更新数据库,那么就有可能会出现数据库和MQ的数据不一致。

(2)消费MQ消息时调用的接口用分布式锁 + 状态前置校验来保证幂等性

如果消费MQ的消息失败时,就会返回RECONSUME_LATER给MQ,此时就可能会出现同一条消息被多次重复消费,消费消息时调用的接口可能会出现同一请求多次被调用。因此,必须对调用的接口,使用分布式锁 + 状态前置校验进行幂等处理。加分布式锁是为了防止并非请求进来后可能会避开状态的前置校验。

7.客服查询售后工单进行审核的业务流程

(1)查询售后列表

(2)查询售后详情

(1)查询售后列表

一.业务规则

售后列表和详情只能看到⽤户主动发起的售后退款记录,超时⾃动取消和⽤户⼿动取消的售后单不展示,通过售后单的类型售后类型来区分。

二.具体实现

步骤一: 组装业务查询规则

步骤二: 连表分⻚查询after_sale_info、after_sale_detail、after_sale_pay,具体的字段映射如上⽅的⼊参和表字段映射关系梳理

步骤三: 如果没给排序字段,默认按创建时间降序

步骤四: 查询出来的每条记录根据映射关系梳理,查询对应数据表。然后组装返参,返回给调⽤⽅

步骤五: 通过Builder模式构建复杂查询业务规则

typescript 复制代码
//订单中心-售后查询业务接口
@DubboService(version = "1.0.0", interfaceClass = AfterSaleQueryApi.class)
public class AfterSaleQueryApiImpl implements AfterSaleQueryApi {
    @Autowired
    private AfterSaleQueryService afterSaleQueryService;

    //查询售后列表
    @Override
    public JsonResult<PagingInfo<AfterSaleOrderListDTO>> listAfterSales(AfterSaleQuery query) {
        try {
            //1.参数校验
            afterSaleQueryService.checkQueryParam(query);
            //2.查询
            return JsonResult.buildSuccess(afterSaleQueryService.executeListQuery(query));
        } catch (OrderBizException e) {
            log.error("biz error", e);
            return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
        } catch (Exception e) {
            log.error("error", e);
            return JsonResult.buildError(e.getMessage());
        }
    }
    ...
}

@Service
public class AfterSaleQueryServiceImpl implements AfterSaleQueryService {
    ...
    @Override
    public PagingInfo<AfterSaleOrderListDTO> executeListQuery(AfterSaleQuery query) {
        //当前采用连表查询
        //后续会接入ES
        //1.组装业务查询规则
        AfterSaleListQueryDTO queryDTO = AfterSaleListQueryDTO.Builder.builder()
            .copy(query)
            //默认只展示用户主动发起的售后单
            .userApplySource()
            .setPage(query)
            .build();
        //2.查询
        Page<AfterSaleOrderListDTO> page = afterSaleInfoDAO.listByPage(queryDTO);
        //3.转化
        return PagingInfo.toResponse(page.getRecords(), page.getTotal(), (int) page.getCurrent(), (int) page.getSize());
    }
    ...
}

//查询售后列表的入参
@Data
public class AfterSaleQuery extends AbstractObject implements Serializable {
    public static final Integer MAX_PAGE_SIZE = 100;
    private Integer businessIdentifier;//业务线
    private Set<Integer> orderTypes;//订单类型
    private Set<Integer> afterSaleStatus;//售后单状态
    private Set<Integer> applySources;//售后申请来源
    private Set<Integer> afterSaleTypes;//售后类型
    private Set<Long> afterSaleIds;//售后单号
    private Set<String> orderIds;//订单号
    private Set<String> userIds;//用户ID
    private Set<String> skuCodes;//sku code
    private Pair<Date, Date> createdTimeInterval;//创建时间-查询区间
    private Pair<Date, Date> applyTimeInterval;//售后申请时间-查询区间
    private Pair<Date, Date> reviewTimeInterval;//售后客服审核时间-查询区间
    private Pair<Date, Date> refundPayTimeInterval;//退款支付时间-查询区间
    private Pair<Integer, Integer> refundAmountInterval;退款金额-查询区间
    private Integer pageNo = 1;//页码;默认为1;
    private Integer pageSize = 20;//一页的数据量. 默认为20
}

//售后单列表DTO
@Data
public class AfterSaleOrderListDTO extends AbstractObject implements Serializable {
    private Integer businessIdentifier;//接入方业务标识
    private String afterSaleId;//售后单号
    private String orderId;//订单号
    private Integer orderType;//订单类型
    private String userId;//买家编号
    private Integer orderAmount;//订单实付金额
    private Integer afterSaleStatus;//售后单状态
    private Integer applySource;//申请售后来源
    private Date applyTime;//申请售后时间
    private Integer applyReasonCode;//申请理由编码
    private String applyReason;//申请理由描述
    private Date reviewTime;//客服审核时间
    private Long reviewReasonCode;//客服审核结果编码
    private String reviewReason;//客服审核结果
    private String remark;//备注
    private Integer afterSaleType;//售后类型
    private Integer afterSaleTypeDetail;//售后类型详情
    private String skuCode;//sku code
    private String productName;//商品名
    private String productImg;//商品图片地址
    private Integer returnQuantity;//商品退货数量
    private Integer originAmount;//商品总金额
    private Integer applyRefundAmount;//申请退款金额
    private Integer realRefundAmount;//实际退款金额
}

(2)查询售后详情

kotlin 复制代码
//订单中心-售后查询业务接口
@DubboService(version = "1.0.0", interfaceClass = AfterSaleQueryApi.class)
public class AfterSaleQueryApiImpl implements AfterSaleQueryApi {
    @Autowired
    private AfterSaleQueryService afterSaleQueryService;
    ...

    //查询售后单详情
    @Override
    public JsonResult<AfterSaleOrderDetailDTO> afterSaleDetail(Long afterSaleId) {
        try {
            //1.参数校验
            ParamCheckUtil.checkObjectNonNull(afterSaleId, OrderErrorCodeEnum.AFTER_SALE_ID_IS_NULL);
            //2.查询
            return JsonResult.buildSuccess(afterSaleQueryService.afterSaleDetail(afterSaleId));
        } catch (OrderBizException e) {
            log.error("biz error", e);
            return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
        } catch (Exception e) {
            log.error("error", e);
            return JsonResult.buildError(e.getMessage());
        }
    }
}

@Service
public class AfterSaleQueryServiceImpl implements AfterSaleQueryService {
    ...
    @Override
    public AfterSaleOrderDetailDTO afterSaleDetail(Long afterSaleId) {
        //1.查询售后单
        AfterSaleInfoDO afterSaleInfo = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
        if (null == afterSaleInfo) {
            return null;
        }
        //2.查询售后单条目
        List<AfterSaleItemDO> afterSaleItems = afterSaleItemDAO.listByAfterSaleId(afterSaleId);
        //3.查询售后支付信息
        List<AfterSaleRefundDO> afterSalePays = afterSaleRefundDAO.listByAfterSaleId(afterSaleId);
        //4.查询售后日志
        List<AfterSaleLogDO> afterSaleLogs = afterSaleLogDAO.listByAfterSaleId(afterSaleId);
        //5.构造返参
        return new AfterSaleOrderDetailBuilder()
            .afterSaleInfo(afterSaleInfo)
            .afterSaleItems(afterSaleItems)
            .afterSalePays(afterSalePays)
            .build();
    }
    ...
}

//售后单详情DTO
@Data
public class AfterSaleOrderDetailDTO implements Serializable {
    private AfterSaleInfoDTO afterSaleInfo;//售后信息
    private List<AfterSaleItemDTO> afterSaleItems;//售后单条目
    private List<AfterSalePayDTO> afterSalePays;//售后支付信息
    private List<AfterSaleLogDTO> afterSaleLogs;//售后单日志
}

8.撤销退货申请时使用分布式锁处理并发问题

(1)用户撤销退货申请时的潜在并发问题

(2)使用同一把分布式锁来解决并发问题

(1)用户撤销退货申请时的潜在并发问题

一.潜在的并发问题

客服审核通过用户的申请退货请求,刚好准备退款 + 释放库存 + 释放优惠券。此时用户却点击了撤销售后申请,把售后单状态改为了已撤销。导致用户看到售后已撤销,但却发生了退款和释放优惠券的异常。这就是潜在的并发问题。

二.对应的解决方案

用户撤销售后退货请求、客服审核售后退货请求、售后退款,这三个地方都使用同一把分布式锁REFUND_KEY来解决并发问题。

(2)使用同一把分布式锁来解决并发问题

一.撤销售后退货请求

业务规则:只有提交申请状态才可以撤销。

步骤一:接收撤销售后申请请求

步骤二:查询售后单

步骤三:进⾏业务规则判断

步骤四:加refund_key的分布式锁

步骤五:更新售后单状态为已撤销

步骤六:增加⼀条售后单操作⽇志

步骤七:释放锁

kotlin 复制代码
@RestController
@RequestMapping("/afterSale")
public class AfterSaleController {
    ...
    //用户撤销售后退货申请
    @PostMapping("/revokeAfterSale")
    public JsonResult<Boolean> revokeAfterSale(@RequestBody RevokeAfterSaleRequest request) {
        JsonResult<Boolean> result = afterSaleApi.revokeAfterSale(request);
        return result;
    }
    ...
}

//订单中心-逆向售后业务接口
@DubboService(version = "1.0.0", interfaceClass = AfterSaleApi.class, retries = 0)
public class AfterSaleApiImpl implements AfterSaleApi {
    @Autowired
    private OrderAfterSaleService orderAfterSaleService;
    ...

    @Override
    public JsonResult<Boolean> revokeAfterSale(RevokeAfterSaleRequest request) {
        //1.参数校验
        ParamCheckUtil.checkObjectNonNull(request.getAfterSaleId(), OrderErrorCodeEnum.AFTER_SALE_ID_IS_NULL);
        Long afterSaleId = request.getAfterSaleId();
        String lockKey = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        //2.加锁,锁整个售后单,两个作用
        //2.1 防并发
        //2.2 业务上的考虑:只要涉及售后表的更新,就需要加锁,锁整个售后表,否则算钱的时候,就会由于突然撤销,导致钱多算了
        if (!redisLock.lock(lockKey)) {
            throw new OrderBizException(OrderErrorCodeEnum.AFTER_SALE_CANNOT_REVOKE);
        }
        try {
            //3.撤销申请
            orderAfterSaleService.revokeAfterSale(request);
        } finally {
            //4.释放锁
            redisLock.unlock(lockKey);
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    //撤销售后申请
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void revokeAfterSale(RevokeAfterSaleRequest request) {
        //1.查询售后单
        Long afterSaleId = request.getAfterSaleId();
        AfterSaleInfoDO afterSaleInfo = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
        ParamCheckUtil.checkObjectNonNull(afterSaleInfo, OrderErrorCodeEnum.AFTER_SALE_ID_IS_NULL);
        //2.校验售后单是否可以撤销:只有提交申请状态才可以撤销
        if (!AfterSaleStatusEnum.COMMITED.getCode().equals(afterSaleInfo.getAfterSaleStatus())) {
            throw new OrderBizException(OrderErrorCodeEnum.AFTER_SALE_CANNOT_REVOKE);
        }
        //3.更新售后单状态为:"已撤销"
        afterSaleInfoDAO.updateStatus(afterSaleInfo.getAfterSaleId(), AfterSaleStatusEnum.COMMITED.getCode(), AfterSaleStatusEnum.REVOKE.getCode());
        //4.增加一条售后单操作日志
        afterSaleLogDAO.save(afterSaleOperateLogFactory.get(afterSaleInfo, AfterSaleStatusChangeEnum.AFTER_SALE_REVOKE));
    }
    ...
}

二.客服审核售后退货请求

kotlin 复制代码
@RestController
@RequestMapping("/customer")
public class CustomerController {
    ...
    //客服审核售后退货请求
    @PostMapping("/audit")
    public JsonResult<Boolean> audit(@RequestBody CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        Long afterSaleId = customerReviewReturnGoodsRequest.getAfterSaleId();
        //分布式锁
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new CustomerBizException(CustomerErrorCodeEnum.CUSTOMER_AUDIT_CANNOT_REPEAT);
        }
        try {
            //客服审核
            return customerService.customerAudit(customerReviewReturnGoodsRequest);
        } catch (Exception e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

@Service
public class CustomerServiceImpl implements CustomerService {
    ...
    @Override
    public JsonResult<Boolean> customerAudit(CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        return afterSaleApi.receiveCustomerAuditResult(customerReviewReturnGoodsRequest);
    }
    ...
}

三.售后退款

java 复制代码
@Configuration
public class ConsumerConfig {
    @Autowired
    private RocketMQProperties rocketMQProperties;
    ...

    @Bean("actualRefundConsumer")
    public DefaultMQPushConsumer actualRefundConsumer(ActualRefundListener actualRefundListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(ACTUAL_REFUND_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(ACTUAL_REFUND_TOPIC, "*");
        consumer.registerMessageListener(actualRefundListener);
        consumer.start();
        return consumer;
    }
    ...
}

@Component
public class ActualRefundListener implements MessageListenerConcurrently {
    @Autowired
    private OrderAfterSaleService orderAfterSaleService;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                String message = new String(messageExt.getBody());
                ActualRefundMessage actualRefundMessage = JSONObject.parseObject(message, ActualRefundMessage.class);
                log.info("ActualRefundConsumer message:{}", message);
                JsonResult<Boolean> jsonResult = orderAfterSaleService.refundMoney(actualRefundMessage);
                if (!jsonResult.getSuccess()) {
                    throw new OrderBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    @Override
    public JsonResult<Boolean> refundMoney(ActualRefundMessage actualRefundMessage) {
        Long afterSaleId = actualRefundMessage.getAfterSaleId();
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        try {
            boolean lock = redisLock.lock(key);
            if (!lock) {
                throw new OrderBizException(OrderErrorCodeEnum.REFUND_MONEY_REPEAT);
            }

            AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(actualRefundMessage.getAfterSaleId());
            AfterSaleRefundDO afterSaleRefundDO = afterSaleRefundDAO.findOrderAfterSaleStatus(String.valueOf(afterSaleId));
            //1.封装调用支付退款接口的数据
            PayRefundRequest payRefundRequest = buildPayRefundRequest(actualRefundMessage, afterSaleRefundDO);
            //2.执行退款
            if (!payApi.executeRefund(payRefundRequest)) {
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_REFUND_AMOUNT_FAILED);
            }
            //3.本次售后的订单条目是当前订单的最后一笔,发送事务MQ退优惠券,此时isLastReturnGoods标记是true
            if (actualRefundMessage.isLastReturnGoods()) {
                TransactionMQProducer producer = defaultProducer.getProducer();
                //组装事务MQ消息体
                ReleaseUserCouponRequest releaseUserCouponRequest = buildLastOrderReleasesCouponMessage(producer, afterSaleInfoDO, afterSaleId, actualRefundMessage);
                try {
                    //4.发送事务消息 释放优惠券
                    Message message = new Message(RocketMqConstant.CANCEL_RELEASE_PROPERTY_TOPIC, JSONObject.toJSONString(releaseUserCouponRequest).getBytes(StandardCharsets.UTF_8));
                    TransactionSendResult result = producer.sendMessageInTransaction(message, releaseUserCouponRequest);
                    if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                        throw new OrderBizException(OrderErrorCodeEnum.REFUND_MONEY_RELEASE_COUPON_FAILED);
                    }
                    return JsonResult.buildSuccess(true);
                } catch (Exception e) {
                    throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
                }
            } else {
                //当前售后条目非本订单的最后一笔 和 取消订单,在此更新售后状态后流程结束
                //更新售后单状态
                updateAfterSaleStatus(afterSaleInfoDO, AfterSaleStatusEnum.REVIEW_PASS.getCode(), AfterSaleStatusEnum.REFUNDING.getCode());
                return JsonResult.buildSuccess(true);
            }
        } catch (OrderBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

注意:用户发起售后退货请求与用户撤销售后退货请求并没有使用同一把锁。

scss 复制代码
@RestController
@RequestMapping("/afterSale")
public class AfterSaleController {
    ...
    //用户发起售后退货请求
    @PostMapping("/applyAfterSale")
    public JsonResult<Boolean> applyAfterSale(@RequestBody ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //分布式锁
        String orderId = returnGoodsOrderRequest.getOrderId();
        String key = RedisLockKeyConstants.REFUND_KEY + orderId;
        boolean lock = redisLock.lock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.PROCESS_AFTER_SALE_RETURN_GOODS);
        }
        try {
            return orderAfterSaleService.processApplyAfterSale(returnGoodsOrderRequest);
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    ...
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JsonResult<Boolean> processApplyAfterSale(ReturnGoodsOrderRequest returnGoodsOrderRequest) {
        //参数校验
        checkAfterSaleRequestParam(returnGoodsOrderRequest);
        try {
            //1.售后单状态验证
            //用order id和sku code查到售后id
            String orderId = returnGoodsOrderRequest.getOrderId();
            String skuCode = returnGoodsOrderRequest.getSkuCode();
            //场景校验逻辑:
            //第一种场景:订单条目A是第一次发起手动售后,此时售后订单条目表没有该订单的记录,orderIdAndSkuCodeList 是空,正常执行后面的售后逻辑
            //第二种场景:订单条目A已发起过售后,非"撤销成功"状态的售后单不允许重复发起售后
            List<AfterSaleItemDO> orderIdAndSkuCodeList = afterSaleItemDAO.getOrderIdAndSkuCode(orderId, skuCode);
            if (!orderIdAndSkuCodeList.isEmpty()) {
                //查询订单条目所属的售后单状态
                Long afterSaleId = orderIdAndSkuCodeList.get(0).getAfterSaleId();
                AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
                if (!AfterSaleStatusEnum.REVOKE.getCode().equals(afterSaleInfoDO.getAfterSaleStatus())) {
                    //非"撤销成功"状态的售后单不能重复发起售后
                    throw new OrderBizException(OrderErrorCodeEnum.PROCESS_APPLY_AFTER_SALE_CANNOT_REPEAT);
                }
            }
            //2.封装数据
            ReturnGoodsAssembleRequest returnGoodsAssembleRequest = buildReturnGoodsData(returnGoodsOrderRequest);
            //3.计算退款金额
            returnGoodsAssembleRequest = calculateReturnGoodsAmount(returnGoodsAssembleRequest);
            //4.售后数据落库
            insertReturnGoodsAfterSale(returnGoodsAssembleRequest, AfterSaleStatusEnum.COMMITED.getCode());
            //5.发起客服审核
            CustomerReceiveAfterSaleRequest customerReceiveAfterSaleRequest = returnGoodsAssembleRequest.clone(new CustomerReceiveAfterSaleRequest());
            defaultProducer.sendMessage(RocketMqConstant.AFTER_SALE_CUSTOMER_AUDIT_TOPIC,
                JSONObject.toJSONString(customerReceiveAfterSaleRequest), "售后申请发送给客服审核");
        } catch (BaseBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

9.仓储缺品退款场景的流程

(1)仓储缺品的场景和业务规则流程

(2)仓储缺品的具体实现

(1)仓储缺品的场景和业务规则流程

场景: 当订单履约后,会被推送到履约系统。捡货⼈员根据订单要求拣货时,发现有商品缺货,就会直接对订单进⾏缺品退款,然后缺品部分的钱直接回退到⽤户账户去。

业务规则流程: 缺品的商品sku的数量不能超过下订单商品sku数量,只有⽀付之后,配送之前才可以发起缺品。

(2)仓储缺品的具体实现

订单系统收到缺品请求后:⾸先对缺品请求进⾏业务规则校验,校验失败直接抛异常,校验成功进⼊缺品退款处理流程。然后,具体的处理流程是构造缺品类型售后单、售后单条⽬、售后退款单,并插⼊数据库,将缺品信息(缺品数量、sku、⾦额)存储在订单的扩展字段⾥(ext_json)。

scss 复制代码
计算订单缺品退款总⾦额 = 每个缺品sku的缺品退款⾦额总和
计算单个缺品sku的退款⾦额 = (缺品数量 / 下单数量) * 原付款⾦额

接着,发送退款消息ActualRefundMessage到RocketMQ。当订单系统的退款消息消费者收到退款消息后就会发起退款流程,即退款消息消费者ActualRefundConsumer会将钱退还给⽤户。

技术要点:

要点一:本地事务和发送MQ消息的最终⼀致性

要点二:接⼝幂等,同样的缺品请求发送多次不会⽣成多条缺品记录,这通过分布式锁 + 前置校验实现

scss 复制代码
@RestController
@RequestMapping("/afterSale")
public class AfterSaleController {
    ...
    //缺品请求
    @PostMapping("/lackItem")
    public JsonResult<LackDTO> lackItem(@RequestBody LackRequest request) {
        JsonResult<LackDTO> result = afterSaleApi.lackItem(request);
        return result;
    }
    ...
}

//订单中心-逆向售后业务接口
@DubboService(version = "1.0.0", interfaceClass = AfterSaleApi.class, retries = 0)
public class AfterSaleApiImpl implements AfterSaleApi {
    @Autowired
    private OrderLackService orderLackItemService;
    ...

    //缺品接口
    @Override
    public JsonResult<LackDTO> lackItem(LackRequest request) {
        try {
            //1.参数基本校验
            ParamCheckUtil.checkStringNonEmpty(request.getOrderId(), OrderErrorCodeEnum.ORDER_ID_IS_NULL);
            ParamCheckUtil.checkCollectionNonEmpty(request.getLackItems(), OrderErrorCodeEnum.LACK_ITEM_IS_NULL);
            //2.加锁防并发
            String lockKey = RedisLockKeyConstants.LACK_REQUEST_KEY + request.getOrderId();
            if (!redisLock.lock(lockKey)) {
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_NOT_ALLOW_TO_LACK);
            }
            //3.参数校验
            CheckLackDTO checkResult = orderLackItemService.checkRequest(request);
            try {
                //4.缺品处理
                return JsonResult.buildSuccess(orderLackItemService.executeLackRequest(request, checkResult));
            } finally {
                redisLock.unlock(lockKey);
            }
        } catch (OrderBizException e) {
            log.error("biz error", e);
            return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
        } catch (Exception e) {
            log.error("error", e);
            return JsonResult.buildError(e.getMessage());
        }
    }
    ...
}

//订单缺品请求
@Data
public class LackRequest extends AbstractObject implements Serializable {
    private String orderId;//订单号
    private String userId;//用户id
    private Set<LackItemRequest> lackItems;//具体的缺品项
}

//具体的缺品项
@Data
public class LackItemRequest extends AbstractObject implements Serializable {
    private String skuCode;//sku编码
    private Integer lackNum;//缺品数量
}

@Service
public class OrderLackServiceImpl implements OrderLackService {
    ...
    //校验入参
    @Override
    public CheckLackDTO checkRequest(LackRequest request) throws OrderBizException {
        //1.查询订单
        OrderInfoDO order = orderInfoDAO.getByOrderId(request.getOrderId());
        ParamCheckUtil.checkObjectNonNull(order, OrderErrorCodeEnum.ORDER_NOT_FOUND);
        //2.校验订单是否可以发起缺品
        //可以发起缺品的前置条件:
        //(1)订单的状态为:"已出库"
        //(2)订单未发起过缺品
        if (!OrderStatusEnum.canLack().contains(order.getOrderStatus()) || isOrderLacked(order)) {
            throw new OrderBizException(OrderErrorCodeEnum.ORDER_NOT_ALLOW_TO_LACK);
        }
        //3.查询订单item
        List<OrderItemDO> orderItems = orderItemDAO.listByOrderId(request.getOrderId());
        //4.校验具体的缺品项
        List<LackItemDTO> lackItems = new ArrayList<>();
        for (LackItemRequest itemRequest : request.getLackItems()) {
            lackItems.add(checkLackItem(order, orderItems, itemRequest));
        }
        //5.构造返参
        return new CheckLackDTO(order, lackItems);
    }

    //缺品处理
    @Transactional(rollbackFor = Exception.class)
    @Override
    public LackDTO executeLackRequest(LackRequest request, CheckLackDTO checkLackItemDTO) {
        OrderInfoDO order = checkLackItemDTO.getOrder();
        List<LackItemDTO> lackItems = checkLackItemDTO.getLackItems();
        //1.生成缺品售后单
        AfterSaleInfoDO lackAfterSaleOrder = buildLackAfterSaleInfo(order);
        //2.生成缺品售后单item
        List<AfterSaleItemDO> afterSaleItems = new ArrayList<>();
        lackItems.forEach(item -> {
            afterSaleItems.add(buildLackAfterSaleItem(order, lackAfterSaleOrder, item));
        });
        //3.计算订单缺品退款总金额
        Integer lackApplyRefundAmount = afterSaleAmountService.calculateOrderLackApplyRefundAmount(afterSaleItems);
        Integer lackRealRefundAmount = afterSaleAmountService.calculateOrderLackRealRefundAmount(afterSaleItems);
        lackAfterSaleOrder.setApplyRefundAmount(lackApplyRefundAmount);
        lackAfterSaleOrder.setRealRefundAmount(lackRealRefundAmount);
        //4.构造售后退款单
        AfterSaleRefundDO afterSaleRefund = buildLackAfterSaleRefundDO(order, lackAfterSaleOrder);
        //5.构造订单缺品扩展信息
        OrderExtJsonDTO lackExtJson = buildOrderLackExtJson(request, order, lackAfterSaleOrder);
        //6.存储售后单、item和退款单
        afterSaleInfoDAO.save(lackAfterSaleOrder);
        afterSaleItemDAO.saveBatch(afterSaleItems);
        afterSaleRefundDAO.save(afterSaleRefund);
        //更新订单扩展信息
        orderInfoDAO.updateOrderExtJson(order.getOrderId(), lackExtJson);
        //7.发送缺品退款的消息
        sendLackRefund(order, lackAfterSaleOrder, afterSaleRefund.getId());
        // TODO 使用RocketMQ 事务消息保证本地事务 + 发送缺品退款消息的最终一致性
        return new LackDTO(order.getOrderId(), lackAfterSaleOrder.getAfterSaleId());
    }
    ...
}

10.仓储缺品的退款流程的数据一致性问题

仓储缺品的退款流程也会更新数据库 + 发送消息到MQ,所以也可能会出现:一.更新数据库成功,但发送消息到MQ失败,二.发送消息到MQ成功,但更新数据库失败。

因此,需要做如下处理:

一.数据库和MQ的数据可能出现不一致

更新数据库 + 发送消息到MQ时使用事务机制。

二.消费MQ消息时可能出现失败而导致不一致

对消费失败的消息重试消费RECONSUME_LATER。

三.重试消费可能因破坏幂等性而导致数据不一致

通过分布式锁 + 状态检查保证幂等,加分布式锁是为了防止并非请求进来后避开了状态的前置校验。

相关推荐
codervibe2 分钟前
项目中如何用策略模式实现多角色登录解耦?(附实战代码)
java·后端
TCChzp4 分钟前
synchronized全链路解析:从字节码到JVM内核的锁实现与升级策略
java·jvm
大葱白菜5 分钟前
🧩 Java 枚举详解:从基础到实战,掌握类型安全与优雅设计
java·程序员
笑衬人心。7 分钟前
在 Mac 上安装 Java 和 IntelliJ IDEA(完整笔记)
java·macos·intellij-idea
SimonKing14 分钟前
颠覆传统IO:零拷贝技术如何重塑Java高性能编程?
java·后端·程序员
sniper_fandc25 分钟前
SpringBoot系列—MyBatis(xml使用)
java·spring boot·mybatis
胚芽鞘6811 小时前
查询依赖冲突工具maven Helper
java·数据库·maven
Charlie__ZS1 小时前
若依框架去掉Redis
java·redis·mybatis
咖啡啡不加糖2 小时前
RabbitMQ 消息队列:从入门到Spring Boot实战
java·spring boot·rabbitmq
玩代码2 小时前
Java线程池原理概述
java·开发语言·线程池