订单初版—8.取消订单实现的重构文档

大纲

1.取消订单的流程回顾

2.拦截履约、取消订单与释放资产的强一致问题

3.取消订单与拦截履约的AT分布式事务

4.释放资产的RocketMQ事务消息原理

5.释放资产的设计:异步化 + 扩展性 + 消息不丢失

6.消费释放资产消息后进行多路推送MQ消息的设计

7.释放具体资产时的幂等性保障

8.双异步退款链路强一致方案

9.发起售后退货链路数据一致方案

10.审核售后退货链路数据一致方案

11.拣货出库和缺品退款的业务场景

12.缺品退款一致性事务代码流程

13.进行代码重构的要点与示例

14.自动关单功能业务流程梳理

15.XXL-Job分布式调度系统架构原理分析

16.基于XXL-Job分布式调度的定时关单功能实现

17.订单系统与XXL-Job配合运行原理

18.根据XXL-Job源码来验证关单分布式调度原理

1.取消订单的流程回顾

订单逆向链路有三个核心环节:取消订单 + 售后处理 + 缺品退款。

2.拦截履约、取消订单与释放资产的强一致问题

更新订单状态为已取消 + 拦截履约,要绑定成刚性事务,确保强一致。否则可能会出现数据不一致的情况,比如订单已取消,但还是配送了。因此,可以使用Seata的AT模式。

更新订单状态 + 发送释放资产的消息,也需要确保强一致。否则也可能出现数据不一致的情况,比如订单已取消,但优惠券没释放。因此,可以使用RocketMQ的事务消息,确保一起成功,一起失败。

3.取消订单与拦截履约的AT分布式事务

(1)原理流程图

(2)具体实现

(1)原理流程图

(2)具体实现

一.通过事务消息来更新订单状态+发送释放资产消息

二.添加@GlobalTransactional注解开启AT模式

三.履约系统对拦截履约的处理

一.通过事务消息来更新订单状态+发送释放资产消息

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

    //用户手动取消订单
    @PostMapping("/cancelOrder")
    public JsonResult<Boolean> cancelOrder(@RequestBody CancelOrderRequest cancelOrderRequest) {
        return orderAfterSaleService.cancelOrder(cancelOrderRequest);
    }
    ...
}

@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    @Autowired
    private AfterSaleManager afterSaleManager;
    ...

    //取消订单/超时未支付取消
    @Override
    public JsonResult<Boolean> cancelOrder(CancelOrderRequest cancelOrderRequest) {
        //入参检查
        checkCancelOrderRequestParam(cancelOrderRequest);

        //分布式锁
        String orderId = cancelOrderRequest.getOrderId();
        String key = RedisLockKeyConstants.CANCEL_KEY + orderId;
        boolean lock = redisLock.tryLock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.CANCEL_ORDER_REPEAT);
        }

        try {
            //执行取消订单
            return executeCancelOrder(cancelOrderRequest, orderId);
        } catch (Exception e) {
            log.error("biz error", e);
            throw new OrderBizException(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }

    @Override
    public JsonResult<Boolean> executeCancelOrder(CancelOrderRequest cancelOrderRequest, String orderId) {
        //1.组装数据
        OrderInfoDO orderInfoDO = findOrderInfo(orderId);
        CancelOrderAssembleRequest cancelOrderAssembleRequest = buildAssembleRequest(orderId, cancelOrderRequest, orderInfoDO);
        if (cancelOrderAssembleRequest.getOrderInfoDTO().getOrderStatus() >= OrderStatusEnum.OUT_STOCK.getCode()) {
            throw new OrderBizException(OrderErrorCodeEnum.CURRENT_ORDER_STATUS_CANNOT_CANCEL);
        }

        TransactionMQProducer producer = defaultProducer.getProducer();
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                try {
                    //2.执行履约取消、更新订单状态、新增订单日志操作
                    afterSaleManager.cancelOrderFulfillmentAndUpdateOrderStatus(cancelOrderAssembleRequest);
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (Exception e) {
                    log.error("system error", e);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                //查询订单状态是否已更新为"已取消"
                OrderInfoDO orderInfoByDatabase = orderInfoDAO.getByOrderId(orderId);
                if (OrderStatusEnum.CANCELED.getCode().equals(orderInfoByDatabase.getOrderStatus())) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });

        try {
            Message message = new Message(RocketMqConstant.RELEASE_ASSETS_TOPIC, JSONObject.toJSONString(cancelOrderAssembleRequest).getBytes(StandardCharsets.UTF_8));
            //3.发送事务消息 释放权益资产
            TransactionSendResult result = producer.sendMessageInTransaction(message, cancelOrderAssembleRequest);
            if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                throw new OrderBizException(OrderErrorCodeEnum.CANCEL_ORDER_PROCESS_FAILED);
            }
            return JsonResult.buildSuccess(true);
        } catch (Exception e) {
            throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
        }
    }
    ...
}

二.添加@GlobalTransactional注解开启AT模式

复制代码
@Service
public class AfterSaleManagerImpl implements AfterSaleManager {
    @DubboReference(version = "1.0.0")
    private FulfillApi fulfillApi;
    ...

    @Override
    @GlobalTransactional(rollbackFor = Exception.class)
    public void cancelOrderFulfillmentAndUpdateOrderStatus(CancelOrderAssembleRequest cancelOrderAssembleRequest) {
        //履约取消
        cancelFulfill(cancelOrderAssembleRequest);

        //更新订单状态和记录订单操作日志
        updateOrderStatusAndSaveOperationLog(cancelOrderAssembleRequest);
    }

    //调用履约拦截订单
    private void cancelFulfill(CancelOrderAssembleRequest cancelOrderAssembleRequest) {
        OrderInfoDTO orderInfoDTO = cancelOrderAssembleRequest.getOrderInfoDTO();
        if (OrderStatusEnum.CREATED.getCode().equals(orderInfoDTO.getOrderStatus())) {
            return;
        }
        CancelFulfillRequest cancelFulfillRequest = orderConverter.convertCancelFulfillRequest(orderInfoDTO);
        fulfillRemote.cancelFulfill(cancelFulfillRequest);
    }

    //更新订单状态和记录订单操作日志
    private void updateOrderStatusAndSaveOperationLog(CancelOrderAssembleRequest cancelOrderAssembleRequest) {
        //更新订单表
        OrderInfoDO orderInfoDO = orderConverter.orderInfoDTO2DO(cancelOrderAssembleRequest.getOrderInfoDTO());
        orderInfoDO.setCancelType(cancelOrderAssembleRequest.getCancelType().toString());
        orderInfoDO.setOrderStatus(OrderStatusEnum.CANCELED.getCode());
        orderInfoDO.setCancelTime(new Date());
        orderInfoDAO.updateOrderInfo(orderInfoDO);
        log.info("更新订单信息OrderInfo状态: orderId:{},status:{}", orderInfoDO.getOrderId(), orderInfoDO.getOrderStatus());

        //新增订单操作操作日志表
        Integer cancelType = Integer.valueOf(orderInfoDO.getCancelType());
        String orderId = orderInfoDO.getOrderId();
        OrderOperateLogDO orderOperateLogDO = new OrderOperateLogDO();
        orderOperateLogDO.setOrderId(orderId);
        orderOperateLogDO.setPreStatus(cancelOrderAssembleRequest.getOrderInfoDTO().getOrderStatus());
        orderOperateLogDO.setCurrentStatus(OrderStatusEnum.CANCELED.getCode());
        orderOperateLogDO.setOperateType(OrderOperateTypeEnum.AUTO_CANCEL_ORDER.getCode());
        if (OrderCancelTypeEnum.USER_CANCELED.getCode().equals(cancelType)) {
            orderOperateLogDO.setOperateType(OrderOperateTypeEnum.MANUAL_CANCEL_ORDER.getCode());
            orderOperateLogDO.setRemark(OrderOperateTypeEnum.MANUAL_CANCEL_ORDER.getMsg()
                + orderOperateLogDO.getPreStatus() + "-" + orderOperateLogDO.getCurrentStatus());
        }
        if (OrderCancelTypeEnum.TIMEOUT_CANCELED.getCode().equals(cancelType)) {
            orderOperateLogDO.setOperateType(OrderOperateTypeEnum.AUTO_CANCEL_ORDER.getCode());
            orderOperateLogDO.setRemark(OrderOperateTypeEnum.AUTO_CANCEL_ORDER.getMsg()
                + orderOperateLogDO.getPreStatus() + "-" + orderOperateLogDO.getCurrentStatus());
        }
        orderOperateLogDAO.save(orderOperateLogDO);
        log.info("新增订单操作日志OrderOperateLog状态,orderId:{}, PreStatus:{},CurrentStatus:{}", orderInfoDO.getOrderId(),
            orderOperateLogDO.getPreStatus(), orderOperateLogDO.getCurrentStatus());
    }
    ...
}

@Component
public class FulfillRemote {
    @DubboReference(version = "1.0.0")
    private FulfillApi fulfillApi;

    //取消订单履约
    public void cancelFulfill(CancelFulfillRequest cancelFulfillRequest) {
        JsonResult<Boolean> jsonResult = fulfillApi.cancelFulfill(cancelFulfillRequest);
        if (!jsonResult.getSuccess()) {
            throw new OrderBizException(OrderErrorCodeEnum.CANCEL_ORDER_FULFILL_ERROR);
        }
    }
}

三.履约系统对拦截履约的处理

复制代码
@DubboService(version = "1.0.0", interfaceClass = FulfillApi.class, retries = 0)
public class FulfillApiImpl implements FulfillApi {
    @Autowired
    private FulfillService fulfillService;

    @Autowired
    private TmsRemote tmsRemote;

    @Autowired
    private WmsRemote wmsRemote;
    ...

    @Override
    public JsonResult<Boolean> cancelFulfill(CancelFulfillRequest cancelFulfillRequest) {
        log.info("取消履约:request={}", JSONObject.toJSONString(cancelFulfillRequest));
        //1.取消履约单
        fulfillService.cancelFulfillOrder(cancelFulfillRequest.getOrderId());
        //2.取消捡货
        wmsRemote.cancelPickGoods(cancelFulfillRequest.getOrderId());
        //3.取消发货
        tmsRemote.cancelSendOut(cancelFulfillRequest.getOrderId());
        return JsonResult.buildSuccess(true);
    }
    ...
}

@Service
public class FulfillServiceImpl implements FulfillService {
    @Autowired
    private OrderFulfillDAO orderFulfillDAO;

    @Autowired
    private OrderFulfillItemDAO orderFulfillItemDAO;
    ...

    @Override
    public void cancelFulfillOrder(String orderId) {
        //1.查询履约单
        OrderFulfillDO orderFulfill = orderFulfillDAO.getOne(orderId);
        //2.移除履约单
        if (null != orderFulfill) {
            orderFulfillDAO.removeById(orderFulfill.getId());
            //3.查询履约单条目
            List<OrderFulfillItemDO> fulfillItems = orderFulfillItemDAO.listByFulfillId(orderFulfill.getFulfillId());
            //4.移除履约单条目
            for (OrderFulfillItemDO item : fulfillItems) {
                orderFulfillItemDAO.removeById(item.getId());
            }
        }
    }
    ...
}

@Component
public class WmsRemote {
    @DubboReference(version = "1.0.0", retries = 0)
    private WmsApi wmsApi;
    ...

    //取消捡货
    public void cancelPickGoods(String orderId) {
        JsonResult<Boolean> jsonResult = wmsApi.cancelPickGoods(orderId);
        if (!jsonResult.getSuccess()) {
            throw new FulfillBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
        }
    }
}

@Component
public class TmsRemote {
    @DubboReference(version = "1.0.0", retries = 0)
    private TmsApi tmsApi;
    ...

    //取消发货
    public void cancelSendOut(String orderId) {
        JsonResult<Boolean> jsonResult = tmsApi.cancelSendOut(orderId);
        if (!jsonResult.getSuccess()) {
            throw new FulfillBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
        }
    }
}

@DubboService(version = "1.0.0", interfaceClass = WmsApi.class, retries = 0)
public class WmsApiImpl implements WmsApi {
    ...
    @Transactional(rollbackFor = Exception.class)
    @Override
    public JsonResult<Boolean> cancelPickGoods(String orderId) {
        log.info("取消捡货,orderId={}", orderId);
        //1.查询出库单
        List<DeliveryOrderDO> deliveryOrders = deliveryOrderDAO.listByOrderId(orderId);
        if (CollectionUtils.isNotEmpty(deliveryOrders)) {
            //2.移除出库单
            List<Long> ids = deliveryOrders.stream().map(DeliveryOrderDO::getId).collect(Collectors.toList());
            deliveryOrderDAO.removeByIds(ids);
            //3.移除条目
            List<String> deliveryOrderIds = deliveryOrders.stream().map(DeliveryOrderDO::getDeliveryOrderId).collect(Collectors.toList());
            List<DeliveryOrderItemDO> items = deliveryOrderItemDAO.listByDeliveryOrderIds(deliveryOrderIds);
            if (CollectionUtils.isNotEmpty(items)) {
                ids = items.stream().map(DeliveryOrderItemDO::getId).collect(Collectors.toList());
                deliveryOrderItemDAO.removeByIds(ids);
            }
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

@DubboService(version = "1.0.0", interfaceClass = TmsApi.class, retries = 0)
public class TmsApiImpl implements TmsApi {
    ...
    @Transactional(rollbackFor = Exception.class)
    @Override
    public JsonResult<Boolean> cancelSendOut(String orderId) {
        log.info("取消发货,orderId={}", orderId);
        //1.查询物流单
        List<LogisticOrderDO> logisticOrders = logisticOrderDAO.listByOrderId(orderId);
        //2.移除物流单
        if (CollectionUtils.isNotEmpty(logisticOrders)) {
            List<Long> ids = logisticOrders.stream().map(LogisticOrderDO::getId).collect(Collectors.toList());
            logisticOrderDAO.removeByIds(ids);
        }
        return JsonResult.buildSuccess(true);
    }
    ...
}

如果履约系统取消履约时,先发送取消履约的消息到MQ。虽然可以提升拦截履约接口的性能 + 提高拦截履约的扩展性。但是可能会出现如下的情形:订单状态已取消 + 优惠券已释放 + 已退款,但是订单却还在履约配送中。这种情况下,可能需要订单履约配送各环节需要对订单状态进行判断。

复制代码
@DubboService(version = "1.0.0", interfaceClass = FulfillApi.class, retries = 0)
public class FulfillApiImpl implements FulfillApi {
    ...
    @Override
    public JsonResult<Boolean> cancelFulfill(CancelFulfillRequest cancelFulfillRequest) {
        log.info("取消履约:request={}", JSONObject.toJSONString(cancelFulfillRequest));

        //发送取消履约消息
        defaultProducer.sendMessage(RocketMqConstant.CANCEL_FULFILL_TOPIC, JSONObject.toJSONString(cancelFulfillRequest), "取消履约");

        return JsonResult.buildSuccess(true);
    }
    ...
}

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

    //取消履约消息消费者
    @Bean("cancelFulfillConsumer")
    public DefaultMQPushConsumer cancelFulfillConsumer(CancelFulfillTopicListener cancelFulfillTopicListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CANCEL_FULFILL_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(CANCEL_FULFILL_TOPIC, "*");
        consumer.registerMessageListener(cancelFulfillTopicListener);
        consumer.start();
        return consumer;
    }
    ...
}

//消费取消订单履约消息
@Component
public class CancelFulfillTopicListener implements MessageListenerConcurrently {
    @Autowired
    private FulfillService fulfillService;

    @DubboReference(version = "1.0.0", retries = 0)
    private WmsApi wmsApi;

    @DubboReference(version = "1.0.0", retries = 0)
    private TmsApi tmsApi;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        try {
            for (MessageExt messageExt : msgs) {
                String message = new String(messageExt.getBody());
                CancelFulfillRequest request = JSON.parseObject(message, CancelFulfillRequest.class);

                //1.取消履约单
                fulfillService.cancelFulfillOrder(request.getOrderId());

                //2.取消捡货
                wmsApi.cancelPickGoods(request.getOrderId());

                //3.取消发货
                tmsApi.cancelSendOut(request.getOrderId());
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("消费取消订单履约消息", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
}

4.释放资产的RocketMQ事务消息原理

复制代码
@Service
public class OrderAfterSaleServiceImpl implements OrderAfterSaleService {
    @Autowired
    private AfterSaleManager afterSaleManager;
    ...

    //取消订单/超时未支付取消
    @Override
    public JsonResult<Boolean> cancelOrder(CancelOrderRequest cancelOrderRequest) {
        //入参检查
        checkCancelOrderRequestParam(cancelOrderRequest);

        //分布式锁
        String orderId = cancelOrderRequest.getOrderId();
        String key = RedisLockKeyConstants.CANCEL_KEY + orderId;
        boolean lock = redisLock.tryLock(key);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.CANCEL_ORDER_REPEAT);
        }

        try {
            //执行取消订单
            return executeCancelOrder(cancelOrderRequest, orderId);
        } catch (Exception e) {
            log.error("biz error", e);
            throw new OrderBizException(e.getMessage());
        } finally {
            redisLock.unlock(key);
        }
    }

    @Override
    public JsonResult<Boolean> executeCancelOrder(CancelOrderRequest cancelOrderRequest, String orderId) {
        //1.组装数据
        OrderInfoDO orderInfoDO = findOrderInfo(orderId);
        CancelOrderAssembleRequest cancelOrderAssembleRequest = buildAssembleRequest(orderId, cancelOrderRequest, orderInfoDO);
        if (cancelOrderAssembleRequest.getOrderInfoDTO().getOrderStatus() >= OrderStatusEnum.OUT_STOCK.getCode()) {
            throw new OrderBizException(OrderErrorCodeEnum.CURRENT_ORDER_STATUS_CANNOT_CANCEL);
        }

        TransactionMQProducer producer = defaultProducer.getProducer();
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                try {
                    //2.执行履约取消、更新订单状态、新增订单日志操作
                    afterSaleManager.cancelOrderFulfillmentAndUpdateOrderStatus(cancelOrderAssembleRequest);
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (Exception e) {
                    log.error("system error", e);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                //查询订单状态是否已更新为"已取消"
                OrderInfoDO orderInfoByDatabase = orderInfoDAO.getByOrderId(orderId);
                if (OrderStatusEnum.CANCELED.getCode().equals(orderInfoByDatabase.getOrderStatus())) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });

        try {
            Message message = new Message(RocketMqConstant.RELEASE_ASSETS_TOPIC, JSONObject.toJSONString(cancelOrderAssembleRequest).getBytes(StandardCharsets.UTF_8));
            //3.发送事务消息 释放权益资产
            TransactionSendResult result = producer.sendMessageInTransaction(message, cancelOrderAssembleRequest);
            if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                throw new OrderBizException(OrderErrorCodeEnum.CANCEL_ORDER_PROCESS_FAILED);
            }
            return JsonResult.buildSuccess(true);
        } catch (Exception e) {
            throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
        }
    }
    ...
}

5.释放资产的设计:异步化 + 扩展性 + 消息不丢失

(1)异步化 + 扩展性 + 消息不丢失

(2)取消订单释放资产的代码实现 + 具体业务流程

(1)异步化 + 扩展性 + 消息不丢失

一.异步化

从接口性能上来说,需要异步。如果取消订单时,除了同步更新订单状态 + 同步拦截履约外。还要同步释放库存 + 释放优惠券 + 退款,则可能会非常耗时,性能很差。

从业务语义上来说,可以异步。取消订单时,如果履约能拦截成功则进行取消,如果拦截失败则不取消。所以,只要成功拦截履约 + 更新订单为已取消,就可以返回响应给用户了。

二.扩展性

如果以后增加更多的资产类型,比如用户虚拟币、用户积分、信贷资产等,那么可以直接在消费释放资产的Listener中添加这些资产类型即可。引入释放资产的Topic,可以提高扩展性,然后进行具体释放的消息会通过多路推送到MQ。

三.消息不丢失

更新订单状态为已取消 + 发送释放资产消息到MQ时,使用了RocketMQ的事务消息,来保证更新订单状态 + 发送消息一起成功。

消费释放资产消息时,会继续多路发送具体的资产释放消息到MQ,如果出现发送失败则会返回RECONSUME_LATER给MQ进行重试,从而确保消息能发送到MQ。

(2)取消订单释放资产的代码实现 + 具体业务流程

一.取消订单释放资产的代码实现

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

    //释放资产消息消费者
    @Bean("releaseAssetsConsumer")
    public DefaultMQPushConsumer releaseAssetsConsumer(ReleaseAssetsListener releaseAssetsListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RELEASE_ASSETS_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(RELEASE_ASSETS_TOPIC, "*");
        consumer.registerMessageListener(releaseAssetsListener);
        consumer.start();
        return consumer;
    }
    ...
}

//监听并消费释放资产消息
@Component
public class ReleaseAssetsListener implements MessageListenerConcurrently {
    @Autowired
    private DefaultProducer defaultProducer;

    @Autowired
    private OrderItemDAO orderItemDAO;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        try {
            for (MessageExt messageExt : list) {
                //1.消费到释放资产message
                String message = new String(messageExt.getBody());
                log.info("ReleaseAssetsListener message:{}", message);
                CancelOrderAssembleRequest cancelOrderAssembleRequest = JSONObject.parseObject(message, CancelOrderAssembleRequest.class);
                OrderInfoDTO orderInfoDTO = cancelOrderAssembleRequest.getOrderInfoDTO();

                //2.发送取消订单退款请求MQ
                if (orderInfoDTO.getOrderStatus() > OrderStatusEnum.CREATED.getCode()) {
                    defaultProducer.sendMessage(RocketMqConstant.CANCEL_REFUND_REQUEST_TOPIC,
                        JSONObject.toJSONString(cancelOrderAssembleRequest), "取消订单退款");
                }

                //3.发送释放库存MQ
                ReleaseProductStockRequest releaseProductStockRequest = buildReleaseProductStock(orderInfoDTO, orderItemDAO);
                defaultProducer.sendMessage(RocketMqConstant.CANCEL_RELEASE_INVENTORY_TOPIC,
                    JSONObject.toJSONString(releaseProductStockRequest), "取消订单释放库存");

                //4.发送释放优惠券MQ
                if (!Strings.isNullOrEmpty(orderInfoDTO.getCouponId())) {
                    ReleaseUserCouponRequest releaseUserCouponRequest = buildReleaseUserCoupon(orderInfoDTO);
                    defaultProducer.sendMessage(RocketMqConstant.CANCEL_RELEASE_PROPERTY_TOPIC,
                        JSONObject.toJSONString(releaseUserCouponRequest), "取消订单释放优惠券");
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
    ...
}

二.取消订单释放资产的具体业务流程

6.消费释放资产消息后进行多路推送MQ消息的设计

(1)增加释放资产消息的消费者进行多路推送的疑问

(2)增加释放资产消息的消费者进行多路推送的原因

(1)增加释放资产消息的消费者进行多路推送的疑问

为什么要通过"释放资产消息"的消费者来进行多路推送MQ消息?为什么不让各业务系统直接消费"释放资产消息"?比如库存系统直接消费"释放资产消息"来释放库存,营销系统直接消费"释放资产消息"来释放优惠券,支付系统直接消费"释放资产消息"来进行退款处理。

如果省略掉"释放资产消息的消费者",其实也可以实现异步化 + 扩展性。当新增一种释放的资产,只需对应业务系统订阅"释放资产消息"即可。但是最佳实践还是:增加一个"释放资产消息的消费者"来进行多路推送MQ消息。

(2)增加释放资产消息的消费者进行多路推送的原因

**原因一:**如果库存系统释放库存时,只能订阅"释放资产消息",可能会导致:别的业务场景也需要释放库存时,产生Topic混乱。

如果有另外的业务场景也需要释放库存,比如提前购业务需要释放库存。提前购业务要实现释放库存,可以发送两种消息到MQ来实现。

第一种消息是:向MQ也发送"释放资产消息"。这样其实就不合理了,因为该业务只需要释放库存。然后它发送的消息却会被营销系统进行消费,并判断是否需要释放优惠券,以及被支付系统进行消费,并判断是否需要退款。

第二种消息是:向MQ发送另外一个Topic消息,比如"提前购释放资产消息"。那么此时,就需要库存系统增加一个Topic进行消费。这样就导致库存系统为了释放库存,需要监听多个来自订单系统的Topic,这也是不合理的。

**原因二:**各个业务系统应该都有一些对外开放的Topic来与自己进行交互。如果没有这些公开的Topic,则只能通过RPC进行调用来交互了。

"释放资产消息"是为取消订单的场景量身定制的,"释放资产消息的消费者"也是为取消订单的场景量身定制的,"释放资产消息的消费者"会进行多路MQ消息推送来实现可扩展性。

"释放资产消息"及其消费者是专门服务于取消订单场景的,与场景强耦合。"释放库存消息"及其消费者也专门服务于库存系统的,与库存系统强耦合。"释放优惠券消息"及其消费者也专门服务于营销系统,与营销系统强耦合。

各个业务系统都应该提供一些Topic公开让其他系统进行业务处理。比如库存系统可公开一个Topic,让其他系统可进行扣减库存、释放库存。营销系统也可公开一个Topic,让其他系统可进行优惠券释放等。

每个系统要把自己能够让其他系统来进行交互的一些Topic给开放出来,这样对于取消订单的场景,就可以找各个系统对外开放的Topic。然后把相应的消息推送到这些开放的Topic,来实现业务逻辑。

7.释放具体资产时的幂等性保障

(1)加分布式锁的两个场景总结

(2)消费释放库存消息时的幂等性处理

(3)消费释放优惠券消息时的幂等性处理

(1)加分布式锁的两个场景总结

**场景一:**不同接口间操作同一个数据,防止并发,比如取消订单和支付回调。

**场景二:**消费MQ消息时,防止可能会出现重复消息导致不同机器的并发消费。

所以消费MQ消息时,一般都会使用分布式锁 + 状态前置检查来实现幂等。

(2)消费释放库存消息时的幂等性处理

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

    //释放库存消息消费者
    @Bean("releaseInventoryConsumer")
    public DefaultMQPushConsumer releaseInventoryConsumer(ReleaseInventoryListener releaseInventoryListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.RELEASE_INVENTORY_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(RocketMqConstant.CANCEL_RELEASE_INVENTORY_TOPIC, "*");
        consumer.registerMessageListener(releaseInventoryListener);
        consumer.start();
        return consumer;
    }
    ...
}

//监听并消费释放库存消息
@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;
    ...

    //回滚库存
    @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);
        }
    }
    ...
}

@Service
public class InventoryServiceImpl implements InventoryService {
    ...
    //释放商品库存
    @Override
    public Boolean releaseProductStock(ReleaseProductStockRequest releaseProductStockRequest) {
        //检查入参
        checkReleaseProductStockRequest(releaseProductStockRequest);
        String orderId = releaseProductStockRequest.getOrderId();
        List<ReleaseProductStockRequest.OrderItemRequest> orderItemRequestList = releaseProductStockRequest.getOrderItemRequestList();
        for (ReleaseProductStockRequest.OrderItemRequest orderItemRequest : orderItemRequestList) {
            String skuCode = orderItemRequest.getSkuCode();

            //1.添加redis释放库存锁,作用
            //(1)防同一笔订单重复释放
            //(2)重量级锁,保证mysql+redis扣库存的原子性,同一时间只能有一个订单来释放,需要锁查询+扣库存
            String lockKey = RedisLockKeyConstants.RELEASE_PRODUCT_STOCK_KEY + skuCode;
            //获取不到锁,等待3s
            Boolean locked = redisLock.tryLock(lockKey, CoreConstant.DEFAULT_WAIT_SECONDS);
            if (!locked) {
                log.error("无法获取释放库存锁,orderId={},skuCode={}", orderId, skuCode);
                throw new InventoryBizException(InventoryErrorCodeEnum.RELEASE_PRODUCT_SKU_STOCK_LOCK_CANNOT_ACQUIRE);
            }

            try {
                //2.查询mysql库存数据
                ProductStockDO productStockDO = productStockDAO.getBySkuCode(skuCode);
                if (productStockDO == null) {
                    throw new InventoryBizException(InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_NOT_FOUND_ERROR);
                }

                //3.查询redis库存数据
                String productStockKey = CacheSupport.buildProductStockKey(skuCode);
                Map<String, String> productStockValue = redisCache.hGetAll(productStockKey);
                if (productStockValue.isEmpty()) {
                    //如果查询不到redis库存数据,将mysql库存数据放入redis,以mysql的数据为准
                    addProductStockProcessor.addStockToRedis(productStockDO);
                }
                Integer saleQuantity = orderItemRequest.getSaleQuantity();

                //4.校验是否释放过库存
                ProductStockLogDO productStockLog = productStockLogDAO.getLog(orderId, skuCode);
                if (null != productStockLog && productStockLog.getStatus().equals(StockLogStatusEnum.RELEASED.getCode())) {
                    log.info("已释放过库存,orderId={},skuCode={}", orderId, skuCode);
                    return true;
                }

                //5.释放库存
                releaseProductStockProcessor.doRelease(orderId, skuCode, saleQuantity, productStockLog);
            } finally {
                redisLock.unlock(lockKey);
            }
        }
        return true;
    }
    ...
}

(3)消费释放优惠券消息时的幂等性处理

复制代码
@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 {
    @Autowired
    private CouponService couponService;
    ...

    //回退用户使用的优惠券
    @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);
        }
    }
    ...
}

@Service
public class CouponServiceImpl implements CouponService {
    ...
    //释放用户优惠券
    @Override
    public Boolean releaseUserCoupon(ReleaseUserCouponRequest releaseUserCouponRequest) {
        String userId = releaseUserCouponRequest.getUserId();
        String couponId = releaseUserCouponRequest.getCouponId();
        CouponDO couponAchieve = couponDAO.getUserCoupon(userId, couponId);
        if (CouponUsedStatusEnum.UN_USED.getCode().equals(couponAchieve.getUsed())) {
            log.info("当前用户未使用优惠券,不用回退,userId:{},couponId:{}", userId, couponId);
            return true;
        }
        couponAchieve.setUsed(CouponUsedStatusEnum.UN_USED.getCode());
        couponAchieve.setUsedTime(null);
        couponDAO.updateById(couponAchieve);
        return true;
    }
    ...
}

8.双异步退款链路强一致方案

(1)什么是双异步

(2)使用事务消息保证数据库和MQ的数据一致性

(3)消费准备退款消息时使用事务消息来进行处理

(4)消费实际退款消息失败时会通过重试确保成功

(1)什么是双异步

**异步一:**消费释放资产消费者,首先会更新订单 + 发送准备退款消息到MQ。

**异步二:**然后订单系统会消费准备退款消息,再发送实际退款消息到MQ。

(2)使用事务消息保证数据库和MQ的数据一致性

订单系统消费准备退款消息时,会插入订单售后数据到数据库 + 发送MQ,此时需要使用事务消息保证数据库和MQ的数据一致性。

(3)消费准备退款消息时使用事务消息来进行处理

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

    //消费退款准备请求消息的消费者
    @Bean("cancelRefundConsumer")
    public DefaultMQPushConsumer cancelRefundConsumer(CancelRefundListener cancelRefundListener) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.REQUEST_CONSUMER_GROUP);
        consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
        consumer.subscribe(RocketMqConstant.CANCEL_REFUND_REQUEST_TOPIC, "*");
        consumer.registerMessageListener(cancelRefundListener);
        consumer.start();
        return consumer;
    }
    ...
}

@Component
public class CancelRefundListener 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());
                CancelOrderAssembleRequest cancelOrderAssembleRequest = JSONObject.parseObject(message, CancelOrderAssembleRequest.class);
                log.info("CancelRefundConsumer message:{}", message);
                //执行 取消订单/超时未支付取消 前的操作
                JsonResult<Boolean> jsonResult = orderAfterSaleService.processCancelOrder(cancelOrderAssembleRequest);
                if (!jsonResult.getSuccess()) {
                    throw new OrderBizException(OrderErrorCodeEnum.CONSUME_MQ_FAILED);
                }
            }
            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> processCancelOrder(CancelOrderAssembleRequest cancelOrderAssembleRequest) {
        String orderId = cancelOrderAssembleRequest.getOrderId();
        //分布式锁
        String key = RedisLockKeyConstants.REFUND_KEY + orderId;
        try {
            boolean lock = redisLock.lock(key);
            if (!lock) {
                throw new OrderBizException(OrderErrorCodeEnum.PROCESS_REFUND_REPEAT);
            }
            //执行退款前的准备工作
            //生成售后订单号
            OrderInfoDTO orderInfoDTO = cancelOrderAssembleRequest.getOrderInfoDTO();
            OrderInfoDO orderInfoDO = orderInfoDTO.clone(OrderInfoDO.class);
            String afterSaleId = orderNoManager.genOrderId(OrderNoTypeEnum.AFTER_SALE.getCode(), orderInfoDO.getUserId());

            //1.计算 取消订单 退款金额
            CancelOrderRefundAmountDTO cancelOrderRefundAmountDTO = calculatingCancelOrderRefundAmount(cancelOrderAssembleRequest);
            cancelOrderAssembleRequest.setCancelOrderRefundAmountDTO(cancelOrderRefundAmountDTO);
            TransactionMQProducer producer = defaultProducer.getProducer();
            producer.setTransactionListener(new TransactionListener() {
                @Override
                public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                    try {
                        //2.取消订单操作 记录售后信息
                        afterSaleManager.insertCancelOrderAfterSale(cancelOrderAssembleRequest, AfterSaleStatusEnum.REVIEW_PASS.getCode(), orderInfoDO, afterSaleId);
                        return LocalTransactionState.COMMIT_MESSAGE;
                    } catch (Exception e) {
                        log.error("system error", e);
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    }
                }
                
                @Override
                public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                    // 查询售后数据是否插入成功
                    AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(Long.valueOf(afterSaleId));
                    List<AfterSaleItemDO> afterSaleItemDOList = afterSaleItemDAO.listByAfterSaleId(Long.valueOf(afterSaleId));
                    List<AfterSaleLogDO> afterSaleLogDOList = afterSaleLogDAO.listByAfterSaleId(Long.valueOf(afterSaleId));
                    List<AfterSaleRefundDO> afterSaleRefundDOList = afterSaleRefundDAO.listByAfterSaleId(Long.valueOf(afterSaleId));
                    if (afterSaleInfoDO != null && afterSaleItemDOList.isEmpty() && afterSaleLogDOList.isEmpty() && afterSaleRefundDOList.isEmpty()) {
                        return LocalTransactionState.COMMIT_MESSAGE;
                    }
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            });

            try {
                //3.组装事务MQ消息
                ActualRefundMessage actualRefundMessage = new ActualRefundMessage();
                actualRefundMessage.setOrderId(cancelOrderAssembleRequest.getOrderId());
                actualRefundMessage.setLastReturnGoods(cancelOrderAssembleRequest.isLastReturnGoods());
                actualRefundMessage.setAfterSaleId(Long.valueOf(afterSaleId));
                Message message = new Message(RocketMqConstant.ACTUAL_REFUND_TOPIC, JSONObject.toJSONString(actualRefundMessage).getBytes(StandardCharsets.UTF_8));
                //4.发送事务MQ消息--实际退款消息
                TransactionSendResult result = producer.sendMessageInTransaction(message, actualRefundMessage);
                if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                    throw new OrderBizException(OrderErrorCodeEnum.PROCESS_REFUND_FAILED);
                }
                return JsonResult.buildSuccess(true);
            } catch (Exception e) {
                throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
            }
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

(4)消费实际退款消息失败时会通过重试确保成功

复制代码
@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.执行退款
            payRemote.executeRefund(payRefundRequest);
            //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);
        }
    }

    private ReleaseUserCouponRequest buildLastOrderReleasesCouponMessage(TransactionMQProducer producer, AfterSaleInfoDO afterSaleInfoDO, Long afterSaleId, ActualRefundMessage actualRefundMessage) {
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                try {
                    //更新售后单状态
                    updateAfterSaleStatus(afterSaleInfoDO, AfterSaleStatusEnum.REVIEW_PASS.getCode(), AfterSaleStatusEnum.REFUNDING.getCode());
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (Exception e) {
                    log.error("system error", e);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                //查询售后单状态是"退款中"
                AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
                if (AfterSaleStatusEnum.REFUNDING.getCode().equals(afterSaleInfoDO.getAfterSaleStatus())) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });

        //组装释放优惠券权益消息数据
        String orderId = actualRefundMessage.getOrderId();
        OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(orderId);
        ReleaseUserCouponRequest releaseUserCouponRequest = new ReleaseUserCouponRequest();
        releaseUserCouponRequest.setCouponId(orderInfoDO.getCouponId());
        releaseUserCouponRequest.setUserId(orderInfoDO.getUserId());
        return releaseUserCouponRequest;
    }
    ...

    //支付退款回调处理
    @Override
    @Transactional(rollbackFor = Exception.class)
    public JsonResult<Boolean> receivePaymentRefundCallback(RefundCallbackRequest payRefundCallbackRequest) {
        String afterSaleId = payRefundCallbackRequest.getAfterSaleId();
        String key = RedisLockKeyConstants.REFUND_KEY + afterSaleId;
        try {
            boolean lock = redisLock.lock(key);
            if (!lock) {
                throw new OrderBizException(OrderErrorCodeEnum.PROCESS_PAY_REFUND_CALLBACK_REPEAT);
            }

            //1.入参校验
            checkRefundCallbackParam(payRefundCallbackRequest);
            //2.获取三方支付退款的返回结果
            Integer afterSaleStatus;
            Integer refundStatus;
            String refundStatusMsg;
            if (RefundStatusEnum.REFUND_SUCCESS.getCode().equals(payRefundCallbackRequest.getRefundStatus())) {
                afterSaleStatus = AfterSaleStatusEnum.REFUNDED.getCode();
                refundStatus = RefundStatusEnum.REFUND_SUCCESS.getCode();
                refundStatusMsg = RefundStatusEnum.REFUND_SUCCESS.getMsg();
            } else {
                afterSaleStatus = AfterSaleStatusEnum.FAILED.getCode();
                refundStatus = RefundStatusEnum.REFUND_FAIL.getCode();
                refundStatusMsg = RefundStatusEnum.REFUND_FAIL.getMsg();
            }
            //3.更新售后记录,支付退款回调更新售后信息
            updatePaymentRefundCallbackAfterSale(payRefundCallbackRequest, afterSaleStatus, refundStatus, refundStatusMsg);
            //4.发短信
            sendRefundMobileMessage(afterSaleId);
            //5.发APP通知
            sendRefundAppMessage(afterSaleId);
            return JsonResult.buildSuccess();
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new OrderBizException(OrderErrorCodeEnum.PROCESS_PAY_REFUND_CALLBACK_FAILED);
        } finally {
            redisLock.unlock(key);
        }
    }
    ...
}

9.发起售后退货链路数据一致方案

(1)用户发起售后退货的处理链路

(2)插入售后数据 + 发送售后退货消息到MQ时使用事务消息来保证一致性

(1)用户发起售后退货的处理链路

(2)插入售后数据 + 发送售后退货消息到MQ时使用事务消息来保证一致性

复制代码
@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.tryLock(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 {
    ...
    //当前业务限制说明:
    //目前业务限定,一笔订单包含多笔订单条目,每次手动售后只能退一笔条目,不支持单笔条目多次退不同数量
    //举例:
    //一笔订单包含订单条目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);

            TransactionMQProducer producer = defaultProducer.getProducer();
            ReturnGoodsAssembleRequest finalReturnGoodsAssembleRequest = returnGoodsAssembleRequest;

            //4.生成售后订单号
            OrderInfoDTO orderInfoDTO = returnGoodsAssembleRequest.getOrderInfoDTO();
            OrderInfoDO orderInfoDO = orderConverter.orderInfoDTO2DO(orderInfoDTO);
            String afterSaleId = orderNoManager.genOrderId(OrderNoTypeEnum.AFTER_SALE.getCode(), orderInfoDO.getUserId());
            producer.setTransactionListener(new TransactionListener() {
                @Override
                public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                    try {
                        //5.售后数据落库
                        insertReturnGoodsAfterSale(finalReturnGoodsAssembleRequest, AfterSaleStatusEnum.COMMITED.getCode(), afterSaleId, orderInfoDO, orderInfoDTO);
                        return LocalTransactionState.COMMIT_MESSAGE;
                    } catch (Exception e) {
                        log.error("system error", e);
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    }
                }

                @Override
                public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                    //查询售后数据是否插入成功
                    AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(Long.valueOf(afterSaleId));
                    List<AfterSaleItemDO> afterSaleItemDOList = afterSaleItemDAO.listByAfterSaleId(Long.valueOf(afterSaleId));
                    List<AfterSaleLogDO> afterSaleLogDOList = afterSaleLogDAO.listByAfterSaleId(Long.valueOf(afterSaleId));
                    List<AfterSaleRefundDO> afterSaleRefundDOList = afterSaleRefundDAO.listByAfterSaleId(Long.valueOf(afterSaleId));
                    if (afterSaleInfoDO != null && !afterSaleItemDOList.isEmpty() && !afterSaleLogDOList.isEmpty() && !afterSaleRefundDOList.isEmpty()) {
                        return LocalTransactionState.COMMIT_MESSAGE;
                    }
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            });

            try {
                //6.组装发送消息数据
                CustomerReceiveAfterSaleRequest customerReceiveAfterSaleRequest = orderConverter.convertReturnGoodsAssembleRequest(returnGoodsAssembleRequest);
                customerReceiveAfterSaleRequest.setAfterSaleId(afterSaleId);
                Message message = new Message(RocketMqConstant.AFTER_SALE_CUSTOMER_AUDIT_TOPIC, JSONObject.toJSONString(customerReceiveAfterSaleRequest).getBytes(StandardCharsets.UTF_8));
                //7.发起客服审核
                TransactionSendResult result = producer.sendMessageInTransaction(message, customerReceiveAfterSaleRequest);
                if (!result.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE)) {
                    throw new OrderBizException(OrderErrorCodeEnum.SEND_AFTER_SALE_CUSTOMER_AUDIT_MQ_FAILED);
                }
                return JsonResult.buildSuccess(true);
            } catch (Exception e) {
                throw new OrderBizException(OrderErrorCodeEnum.SEND_TRANSACTION_MQ_FAILED);
            }
        } catch (BaseBizException e) {
            log.error("system error", e);
            return JsonResult.buildError(e.getMessage());
        }
    }
    ...
}

@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.tryLock(key);
        if (!lock) {
            throw new CustomerBizException(CustomerErrorCodeEnum.PROCESS_RECEIVE_AFTER_SALE_REPEAT);
        }

        try {
            JsonResult<Long> afterSaleRefundIdJsonResult = afterSaleRemote.customerFindAfterSaleRefundInfo(customerReceiveAfterSaleRequest);
            //3.保存售后申请数据
            customerReceiveAfterSaleRequest.setAfterSaleRefundId(afterSaleRefundIdJsonResult.getData());
            CustomerReceivesAfterSaleInfoDO customerReceivesAfterSaleInfoDO = customerConverter.convertCustomerReceivesAfterSalInfoDO(customerReceiveAfterSaleRequest);
            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);
        }
    }
    ...
}

10.审核售后退货链路数据一致方案

一.更新售后数据 + 发送退款消息到MQ也使用了MQ的事务消息来保证数据一致性

二.增加消费释放资产消息的消费者,多路推送释放具体资源的MQ消息来提高扩展性

三.利用分布式锁 + 状态前置检查确保消息消费幂等

四.进行退款没有采用双异步,因为此时退款不需要更新订单状态。即没有发送准备退款消息到MQ,而是只发送实际退款消息到MQ

复制代码
@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.tryLock(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 {
    @Autowired
    private AfterSaleRemote afterSaleRemote;
    ...

    //客服审核
    @Override
    public JsonResult<Boolean> customerAudit(CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        return afterSaleRemote.receiveCustomerAuditResult(customerReviewReturnGoodsRequest);
    }
    ...
}

@Component
public class AfterSaleRemote {
    @DubboReference(version = "1.0.0")
    private AfterSaleApi afterSaleApi;

    //接收客服的审核结果
    public JsonResult<Boolean> receiveCustomerAuditResult(CustomerReviewReturnGoodsRequest customerReviewReturnGoodsRequest) {
        JsonResult<Boolean> jsonResult = afterSaleApi.receiveCustomerAuditResult(customerReviewReturnGoodsRequest);
        if (!jsonResult.getSuccess()) {
            throw new CustomerBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
        }
        return jsonResult;
    }
    ...
}

@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);
    }
    ...
}

@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;
    }
    ...
}

@Component
public class AuditPassReleaseAssetsListener implements MessageListenerConcurrently {
    @Autowired
    private DefaultProducer defaultProducer;

    @Autowired
    private OrderConverter orderConverter;

    @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), "客服审核通过释放库存", null, null);
                //3.发送实际退款
                ActualRefundMessage actualRefundMessage = auditPassReleaseAssetsRequest.getActualRefundMessage();
                defaultProducer.sendMessage(RocketMqConstant.ACTUAL_REFUND_TOPIC,
                    JSONObject.toJSONString(actualRefundMessage), "客服审核通过实际退款", null, null);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            log.error("consumer error", e);
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
    ...
}

11.拣货出库和缺品退款的业务场景

12.缺品退款一致性事务代码流程

复制代码
@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) {
        log.info("request={}", JSONObject.toJSONString(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.tryLock(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());
        }
    }
    ...
}

@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) throws Exception {
        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.构造订单缺品信息
        OrderLackInfo orderLackInfo = OrderLackInfo.builder()
            .lackAfterSaleOrder(lackAfterSaleOrder)
            .afterSaleItems(afterSaleItems)
            .afterSaleRefund(afterSaleRefund)
            .lackExtJson(lackExtJson)
            .orderId(order.getOrderId())
            .build();

        //7.通过缺品事务消息保存缺品数据
        TransactionMQProducer producer = defaultProducer.getProducer();
        producer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                try {
                    //保存缺品数据
                    orderLackProcessor.saveLackInfo(orderLackInfo);
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (BaseBizException e) {
                    throw e;
                } catch (Exception e) {
                    log.error("system error", e);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                //检查缺品售后单是否已经创建
                Long afterSaleId = lackAfterSaleOrder.getAfterSaleId();
                AfterSaleInfoDO afterSaleInfoDO = afterSaleInfoDAO.getOneByAfterSaleId(afterSaleId);
                if (afterSaleInfoDO != null) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });

        //8.发送缺品退款的消息
        ActualRefundMessage actualRefundMessage = new ActualRefundMessage();
        actualRefundMessage.setAfterSaleRefundId(afterSaleRefund.getId());
        actualRefundMessage.setOrderId(order.getOrderId());
        actualRefundMessage.setAfterSaleId(lackAfterSaleOrder.getAfterSaleId());
        String topic = ACTUAL_REFUND_TOPIC;
        byte[] body = JSON.toJSONString(actualRefundMessage).getBytes(StandardCharsets.UTF_8);
        Message mq = new Message(topic, null, order.getOrderId(), body);
        producer.sendMessageInTransaction(mq, order);

        return new LackDTO(order.getOrderId(), lackAfterSaleOrder.getAfterSaleId());
    }
}

13.进行代码重构的要点与示例

(1)高并发场景下for循环写数据库问题

for循环读写优化成批量读写。

(2)高并发for循环RPC调用与服务雪崩问题

for循环RPC调用优化成合并调用。

(3)代码重构之冗长方法抽取分离

按业务逻辑划分,提取业务处理方法。

(4)代码重构之分布式锁与幂等检查强关联

加分布式锁后,紧接着应该就是数据的状态检查。

(5)支付回调的整洁代码重构

一.重构前的代码方法过长 + 逻辑不清晰

二.重构后的代码业务清晰 + 代码简洁

重构五步曲:提取参数、入参检查、加分布式锁、幂等检查、提取业务处理方法。

一.重构前的代码方法过长 + 逻辑不清晰

复制代码
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderManager orderManager;
    ...

    //支付回调
    //支付回调有2把分布式锁的原因说明:同一笔订单在同一时间只能支付or取消
    //不可以同时对一笔订单,既发起支付,又发起取消
    @Override
    public void payCallback(PayCallbackRequest payCallbackRequest) {
        log.info(LoggerFormat.build().remark("payCallback->request").data("request", payCallbackRequest).finish());
        //入参检查
        checkPayCallbackRequestParam(payCallbackRequest);
        String orderId = payCallbackRequest.getOrderId();
        Integer payAmount = payCallbackRequest.getPayAmount();
        Integer payType = payCallbackRequest.getPayType();
        List<String> redisKeyList = Lists.newArrayList();

        //加支付分布式锁避免支付系统并发回调
        String orderPayKey = RedisLockKeyConstants.ORDER_PAY_KEY + orderId;
        //加取消订单分布式锁避免支付和取消订单同时操作同一笔订单
        String cancelOrderKey = RedisLockKeyConstants.CANCEL_KEY + orderId;
        redisKeyList.add(orderPayKey);
        redisKeyList.add(cancelOrderKey);
        boolean lock = redisLock.multiLock(redisKeyList);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.ORDER_PAY_CALLBACK_ERROR);
        }

        try {
            //从数据库中查询出当前订单信息
            OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(orderId);
            OrderPaymentDetailDO orderPaymentDetailDO = orderPaymentDetailDAO.getPaymentDetailByOrderId(orderId);
            //校验参数
            if (orderInfoDO == null || orderPaymentDetailDO == null) {
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_INFO_IS_NULL);
            }
            if (!payAmount.equals(orderInfoDO.getPayAmount())) {
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_CALLBACK_PAY_AMOUNT_ERROR);
            }

            //异常场景判断
            Integer orderStatus = orderInfoDO.getOrderStatus();
            if (OrderStatusEnum.CREATED.getCode().equals(orderStatus)) {
                //如果订单状态是 "已创建",直接更新订单状态为已支付,并发送事务消息
                TransactionMQProducer transactionMQProducer = defaultProducer.getProducer();
                transactionMQProducer.setTransactionListener(new TransactionListener() {
                    @Override
                    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                        try {
                            orderManager.updateOrderStatusPaid(payCallbackRequest, orderInfoDO, orderPaymentDetailDO);
                            return LocalTransactionState.COMMIT_MESSAGE;
                        } catch (BaseBizException e) {
                            throw e;
                        } catch (Exception e) {
                            log.error("system error", e);
                            return LocalTransactionState.ROLLBACK_MESSAGE;
                        }
                    }

                    @Override
                    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                        //检查订单是否是已支付
                        OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(orderId);
                        if (orderInfoDO != null && OrderStatusEnum.PAID.getCode().equals(orderInfoDO.getOrderStatus())) {
                            return LocalTransactionState.COMMIT_MESSAGE;
                        }
                        return LocalTransactionState.ROLLBACK_MESSAGE;
                    }
                });
                //发送 "订单已完成支付" 消息
                sendPaidOrderSuccessMessage(transactionMQProducer, orderInfoDO);
            } else {
                //如果订单状态不是 "已创建"
                if (OrderStatusEnum.CANCELED.getCode().equals(orderStatus)) {
                    //如果订单那状态是取消状态
                    Integer payStatus = orderPaymentDetailDO.getPayStatus();
                    if (PayStatusEnum.UNPAID.getCode().equals(payStatus)) {
                        //调用退款
                        executeOrderRefund(orderInfoDO, orderPaymentDetailDO);
                        throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_ERROR);
                    } else if (PayStatusEnum.PAID.getCode().equals(payStatus)) {
                        if (payType.equals(orderPaymentDetailDO.getPayType())) {
                            throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_SAME_ERROR);
                        } else {
                            throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_NO_SAME_ERROR);
                        }
                    }
                } else {
                    //如果订单状态不是取消状态
                    if (PayStatusEnum.PAID.getCode().equals(orderPaymentDetailDO.getPayStatus())) {
                        if (payType.equals(orderPaymentDetailDO.getPayType())) {
                            return;
                        }
                        //调用退款
                        executeOrderRefund(orderInfoDO, orderPaymentDetailDO);
                        throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_REPEAT_ERROR);
                    }
                }
            }
            log.info(LoggerFormat.build().remark("payCallback->response").finish());
        } catch (Exception e) {
            log.error("payCallback error", e);
            throw new OrderBizException(e.getMessage());
        } finally {
            //释放分布式锁
            redisLock.unMultiLock(redisKeyList);
        }
    }
    ...
}

@Service
public class OrderManagerImpl implements OrderManager {
    ...
    //支付回调更新订单状态
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateOrderStatusPaid(PayCallbackRequest payCallbackRequest, OrderInfoDO orderInfoDO, OrderPaymentDetailDO orderPaymentDetailDO) {
        //主单信息
        String orderId = payCallbackRequest.getOrderId();
        Integer preOrderStatus = orderInfoDO.getOrderStatus();
        orderInfoDO.setOrderStatus(OrderStatusEnum.PAID.getCode());
        orderInfoDAO.updateById(orderInfoDO);

        //主单支付信息
        orderPaymentDetailDO.setPayStatus(PayStatusEnum.PAID.getCode());
        orderPaymentDetailDAO.updateById(orderPaymentDetailDO);

        //新增订单状态变更日志
        OrderOperateLogDO orderOperateLogDO = new OrderOperateLogDO();
        orderOperateLogDO.setOrderId(orderId);
        orderOperateLogDO.setOperateType(OrderOperateTypeEnum.PAID_ORDER.getCode());
        orderOperateLogDO.setPreStatus(preOrderStatus);
        orderOperateLogDO.setCurrentStatus(orderInfoDO.getOrderStatus());
        orderOperateLogDO.setRemark("订单支付回调操作" + orderOperateLogDO.getPreStatus() + "-" + orderOperateLogDO.getCurrentStatus());
        orderOperateLogDAO.save(orderOperateLogDO);

        //判断是否存在子订单
        List<OrderInfoDO> subOrderInfoDOList = orderInfoDAO.listByParentOrderId(orderId);
        if (subOrderInfoDOList != null && !subOrderInfoDOList.isEmpty()) {
            //先将主订单状态设置为无效订单
            Integer newPreOrderStatus = orderInfoDO.getOrderStatus();
            orderInfoDO.setOrderStatus(OrderStatusEnum.INVALID.getCode());
            orderInfoDAO.updateById(orderInfoDO);

            //新增订单状态变更日志
            OrderOperateLogDO newOrderOperateLogDO = new OrderOperateLogDO();
            newOrderOperateLogDO.setOrderId(orderId);
            newOrderOperateLogDO.setOperateType(OrderOperateTypeEnum.PAID_ORDER.getCode());
            newOrderOperateLogDO.setPreStatus(newPreOrderStatus);
            newOrderOperateLogDO.setCurrentStatus(OrderStatusEnum.INVALID.getCode());
            orderOperateLogDO.setRemark("订单支付回调操作,主订单状态变更" + newOrderOperateLogDO.getPreStatus() + "-" + newOrderOperateLogDO.getCurrentStatus());
            orderOperateLogDAO.save(newOrderOperateLogDO);

            //再更新子订单的状态
            List<OrderInfoDO> tempSubOrderInfoDOList = new ArrayList<>();
            List<String> tempSubOrderIdList = new ArrayList<>();
            List<OrderOperateLogDO> tempSubOrderOperateLogDOList = new ArrayList<>();
            for (OrderInfoDO subOrderInfo : subOrderInfoDOList) {
                Integer subPreOrderStatus = subOrderInfo.getOrderStatus();
                subOrderInfo.setOrderStatus(OrderStatusEnum.PAID.getCode());
                tempSubOrderInfoDOList.add(subOrderInfo);
                //子订单的支付明细
                String subOrderId = subOrderInfo.getOrderId();
                tempSubOrderIdList.add(subOrderId);
                //订单状态变更日志
                OrderOperateLogDO subOrderOperateLogDO = new OrderOperateLogDO();
                subOrderOperateLogDO.setOrderId(subOrderId);
                subOrderOperateLogDO.setOperateType(OrderOperateTypeEnum.PAID_ORDER.getCode());
                subOrderOperateLogDO.setPreStatus(subPreOrderStatus);
                subOrderOperateLogDO.setCurrentStatus(OrderStatusEnum.PAID.getCode());
                orderOperateLogDO.setRemark("订单支付回调操作,子订单状态变更" + subOrderOperateLogDO.getPreStatus() + "-" + subOrderOperateLogDO.getCurrentStatus());
                tempSubOrderOperateLogDOList.add(subOrderOperateLogDO);
            }

            //更新子订单
            if (!tempSubOrderInfoDOList.isEmpty()) {
                orderInfoDAO.updateBatchById(tempSubOrderInfoDOList);
            }

            //更新子订单的支付明细
            if (!tempSubOrderIdList.isEmpty()) {
                OrderPaymentDetailDO subOrderPaymentDetailDO = new OrderPaymentDetailDO();
                subOrderPaymentDetailDO.setPayStatus(PayStatusEnum.PAID.getCode());
                orderPaymentDetailDAO.updateBatchByOrderIds(subOrderPaymentDetailDO, tempSubOrderIdList);
            }

            //新增订单状态变更日志
            if (!tempSubOrderOperateLogDOList.isEmpty()) {
                orderOperateLogDAO.saveBatch(tempSubOrderOperateLogDOList);
            }
        }
    }
    ...
}

二.重构后的代码业务清晰 + 代码简洁

OrderServiceImpl的重构:

复制代码
@Service
public class OrderServiceImpl implements OrderService {
    ...
    //支付回调
    //支付回调有2把分布式锁的原因说明:同一笔订单在同一时间只能支付or取消
    //不可以同时对一笔订单,既发起支付,又发起取消
    @Override
    public void payCallback(PayCallbackRequest payCallbackRequest) {
        log.info(LoggerFormat.build().remark("payCallback->request").data("request", payCallbackRequest).finish());
        //提取请求参数中的数据
        String orderId = payCallbackRequest.getOrderId();
        Integer payType = payCallbackRequest.getPayType();

        //从数据库中查询出当前订单信息
        OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(orderId);
        OrderPaymentDetailDO orderPaymentDetailDO = orderPaymentDetailDAO.getPaymentDetailByOrderId(orderId);

        //入参检查
        checkPayCallbackRequestParam(payCallbackRequest, orderInfoDO, orderPaymentDetailDO);

        //为支付回调操作进行多重分布式锁加锁
        List<String> redisKeyList = Lists.newArrayList();
        payCallbackMultiLock(redisKeyList, orderId);
        try {
            Integer orderStatus = orderInfoDO.getOrderStatus();
            Integer payStatus = orderPaymentDetailDO.getPayStatus();
            //幂等性检查
            if (!OrderStatusEnum.CREATED.getCode().equals(orderStatus)) {
                //异常场景处理
                payCallbackFailure(orderStatus, payStatus, payType, orderPaymentDetailDO, orderInfoDO);
                return;
            }
            //执行正式的订单支付回调处理
            doPayCallback(orderInfoDO);
            log.info(LoggerFormat.build().remark("payCallback->response").finish());
        } catch (Exception e) {
            log.error("payCallback error", e);
            throw new OrderBizException(e.getMessage());
        } finally {
            //释放分布式锁
            redisLock.unMultiLock(redisKeyList);
        }
    }

    //检查订单支付回调接口入参
    private void checkPayCallbackRequestParam(PayCallbackRequest payCallbackRequest, OrderInfoDO orderInfoDO, OrderPaymentDetailDO orderPaymentDetailDO) {
        ParamCheckUtil.checkObjectNonNull(payCallbackRequest);

        //订单号
        String orderId = payCallbackRequest.getOrderId();
        ParamCheckUtil.checkStringNonEmpty(orderId);

        //支付金额
        Integer payAmount = payCallbackRequest.getPayAmount();
        ParamCheckUtil.checkObjectNonNull(payAmount);

        //支付系统交易流水号
        String outTradeNo = payCallbackRequest.getOutTradeNo();
        ParamCheckUtil.checkStringNonEmpty(outTradeNo);

        //支付类型
        Integer payType = payCallbackRequest.getPayType();
        ParamCheckUtil.checkObjectNonNull(payType);
        if (PayTypeEnum.getByCode(payType) == null) {
            throw new OrderBizException(OrderErrorCodeEnum.PAY_TYPE_PARAM_ERROR);
        }

        //商户ID
        String merchantId = payCallbackRequest.getMerchantId();
        ParamCheckUtil.checkStringNonEmpty(merchantId);

        //校验参数
        if (orderInfoDO == null || orderPaymentDetailDO == null) {
            throw new OrderBizException(OrderErrorCodeEnum.ORDER_INFO_IS_NULL);
        }
        if (!payAmount.equals(orderInfoDO.getPayAmount())) {
            throw new OrderBizException(OrderErrorCodeEnum.ORDER_CALLBACK_PAY_AMOUNT_ERROR);
        }
    }

    //支付回调加分布式锁
    private void payCallbackMultiLock(List<String> redisKeyList, String orderId) {
        //加支付分布式锁避免支付系统并发回调
        String orderPayKey = RedisLockKeyConstants.ORDER_PAY_KEY + orderId;
        //加取消订单分布式锁避免支付和取消订单同时操作同一笔订单
        String cancelOrderKey = RedisLockKeyConstants.CANCEL_KEY + orderId;
        redisKeyList.add(orderPayKey);
        redisKeyList.add(cancelOrderKey);
        boolean lock = redisLock.multiLock(redisKeyList);
        if (!lock) {
            throw new OrderBizException(OrderErrorCodeEnum.ORDER_PAY_CALLBACK_ERROR);
        }
    }

    //支付回调异常的时候处理逻辑
    public void payCallbackFailure(Integer orderStatus, Integer payStatus, Integer payType, OrderPaymentDetailDO orderPaymentDetailDO, OrderInfoDO orderInfoDO) {
        //如果订单那状态是取消状态
        //可能是支付回调前就取消了订单,也有可能支付回调成功后取消了订单
        if (OrderStatusEnum.CANCELED.getCode().equals(orderStatus)) {
            //此时如果订单的支付状态是未支付的话
            //说明用户在取消订单的时候,支付系统还没有完成回调,而支付系统又已经扣了用户的钱,所以要调用一下退款
            if (PayStatusEnum.UNPAID.getCode().equals(payStatus)) {
                //调用退款
                executeOrderRefund(orderInfoDO, orderPaymentDetailDO);
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_ERROR);
            }

            //此时如果订单的支付状态是已支付的话
            //说明用户在取消订单的时候,订单已经不是"已创建"状态了
            if (PayStatusEnum.PAID.getCode().equals(payStatus)) {
                if (payType.equals(orderPaymentDetailDO.getPayType())) {
                    //非"已创建"状态订单的取消操作本身就会进行退款的
                    //所以如果是同种支付方式,说明用户并没有进行多次支付,是不需要调用退款接口
                    throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_SAME_ERROR);
                } else {
                    //而非同种支付方式的话,说明用户还是更换了不同支付方式进行了多次扣款,所以需要调用一下退款接口
                    //调用退款
                    executeOrderRefund(orderInfoDO, orderPaymentDetailDO);
                    throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_NO_SAME_ERROR);
                }
            }
        } else {
            //如果订单状态不是取消状态(那么就是已履约、已出库、配送中等状态)
            if (PayStatusEnum.PAID.getCode().equals(payStatus)) {
                //如果是同种支付方式回调,说明用户是并没有发起重复付款的,只是支付系统多触发了一次回调
                //这里做好冥等判断,直接return即可,不需要调用退款接口
                if (payType.equals(orderPaymentDetailDO.getPayType())) {
                    return;
                }

                //如果是非同种支付方式,说明用户更换了不同的支付方式发起了重复付款,所以要调用一下退款接口
                //调用退款
                executeOrderRefund(orderInfoDO, orderPaymentDetailDO);
                throw new OrderBizException(OrderErrorCodeEnum.ORDER_CANCEL_PAY_CALLBACK_REPEAT_ERROR);
            }
        }
    }

    //支付回调成功的时候处理逻辑
    private void doPayCallback(OrderInfoDO orderInfoDO) throws MQClientException {
        //如果订单状态是 "已创建",直接更新订单状态为已支付,并发送事务消息
        TransactionMQProducer transactionMQProducer = paidOrderSuccessProducer.getProducer();
        setPayCallbackTransactionListener(transactionMQProducer);
        sendPayCallbackSuccessMessage(transactionMQProducer, orderInfoDO);
    }

    //发送支付成功消息时,设置事务消息TransactionListener组件
    private void setPayCallbackTransactionListener(TransactionMQProducer transactionMQProducer) {
        transactionMQProducer.setTransactionListener(new TransactionListener() {
            @Override
            public LocalTransactionState executeLocalTransaction(Message message, Object o) {
                try {
                    OrderInfoDO orderInfo = (OrderInfoDO) o;
                    orderManager.updateOrderStatusWhenPayCallback(orderInfo);
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (BaseBizException e) {
                    throw e;
                } catch (Exception e) {
                    log.error("system error", e);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
                PaidOrderSuccessMessage paidOrderSuccessMessage = JSON.parseObject(new String(messageExt.getBody(), StandardCharsets.UTF_8), PaidOrderSuccessMessage.class);
                //检查订单是否是已支付
                OrderInfoDO orderInfoDO = orderInfoDAO.getByOrderId(paidOrderSuccessMessage.getOrderId());
                if (orderInfoDO != null && OrderStatusEnum.PAID.getCode().equals(orderInfoDO.getOrderStatus())) {
                    return LocalTransactionState.COMMIT_MESSAGE;
                }
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });
    }

    //送订单已完成支付消息,触发订单进行履约
    private void sendPayCallbackSuccessMessage(TransactionMQProducer transactionMQProducer, OrderInfoDO orderInfoDO) throws MQClientException {
        String orderId = orderInfoDO.getOrderId();
        PaidOrderSuccessMessage message = new PaidOrderSuccessMessage();
        message.setOrderId(orderId);
        log.info(LoggerFormat.build().remark("发送订单已支付消息").data("message", message).finish());
        String topic = RocketMqConstant.PAID_ORDER_SUCCESS_TOPIC;
        byte[] body = JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8);
        Message mq = new MQMessage(topic, null, orderId, body);
        TransactionSendResult result = transactionMQProducer.sendMessageInTransaction(mq, orderInfoDO);
        if (!result.getSendStatus().equals(SendStatus.SEND_OK)) {
            throw new OrderBizException(OrderErrorCodeEnum.ORDER_PAY_CALLBACK_SEND_MQ_ERROR);
        }
    }
    ...
}

OrderManagerImpl的重构:

复制代码
@Service
public class OrderManagerImpl implements OrderManager {
    ...
    //支付回调更新订单状态
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateOrderStatusWhenPayCallback(OrderInfoDO orderInfoDO) {
        //更新主单的状态
        updateMasterOrderStatus(orderInfoDO);

        //判断是否存在子订单
        String orderId = orderInfoDO.getOrderId();
        List<OrderInfoDO> subOrderInfoDOList = orderInfoDAO.listByParentOrderId(orderId);
        if (subOrderInfoDOList == null || subOrderInfoDOList.isEmpty()) {
            return;
        }

        //更新子单的状态
        updateSubOrderStatus(orderInfoDO, subOrderInfoDOList);
    }

    //更新主订单状态
    private void updateMasterOrderStatus(OrderInfoDO orderInfoDO) {
        String orderId = orderInfoDO.getOrderId();

        //更新主单订单状态
        Integer preOrderStatus = orderInfoDO.getOrderStatus();
        Integer currentStatus = OrderStatusEnum.PAID.getCode();
        List<String> orderIdList = Collections.singletonList(orderId);
        updateOrderStatus(orderIdList, currentStatus);

        //更新主单支付状态
        updateOrderPayStatus(orderIdList, PayStatusEnum.PAID.getCode());

        //新增主单订单状态变更日志
        Integer operateType = OrderOperateTypeEnum.PAID_ORDER.getCode();
        String remark = "订单支付回调操作" + preOrderStatus + "-" + currentStatus;
        saveOrderOperateLog(orderId, operateType, preOrderStatus, currentStatus, remark);
    }

    //更新子订单状态
    private void updateSubOrderStatus(OrderInfoDO orderInfoDO, List<OrderInfoDO> subOrderInfoDOList) {
        //如果出现了拆单的话,完成支付之后,主单会被废弃掉,子单,每个子单后续自己去走履约流程就可以了
        //业务上的设计,针对主单信息去做一个后续的操作
        String orderId = orderInfoDO.getOrderId();
        Integer newPreOrderStatus = orderInfoDO.getOrderStatus();
        Integer currentOrderStatus = OrderStatusEnum.INVALID.getCode();

        //先将主订单状态设置为无效订单,主单直接设置为invalid,废掉主单
        List<String> orderIdList = Collections.singletonList(orderId);
        updateOrderStatus(orderIdList, currentOrderStatus);

        //新增订单状态变更日志
        Integer operateType = OrderOperateTypeEnum.PAID_ORDER.getCode();

        //把主单的状态从PAYED更新为INVALID
        String remark = "订单支付回调操作,主订单状态变更" + newPreOrderStatus + "-" + currentOrderStatus;
        saveOrderOperateLog(orderId, operateType, newPreOrderStatus, currentOrderStatus, remark);

        //再更新子订单的状态,子单会更新为PAID
        Integer subCurrentOrderStatus = OrderStatusEnum.PAID.getCode();
        List<String> subOrderIdList = subOrderInfoDOList.stream().map(OrderInfoDO::getOrderId).collect(Collectors.toList());

        //更新子订单状态
        updateOrderStatus(subOrderIdList, subCurrentOrderStatus);

        //更新子订单的支付明细
        updateOrderPayStatus(subOrderIdList, PayStatusEnum.PAID.getCode());

        //保存子订单操作日志
        saveSubOrderOperateLog(subCurrentOrderStatus, subOrderInfoDOList);
    }

    //更新订单状态
    private void updateOrderStatus(List<String> orderIdList, Integer orderStatus) {
        OrderInfoDO orderInfoDO = new OrderInfoDO();
        orderInfoDO.setOrderStatus(orderStatus);
        if (orderIdList.size() == 1) {
            orderInfoDAO.updateByOrderId(orderInfoDO, orderIdList.get(0));
        } else {
            orderInfoDAO.updateBatchByOrderIds(orderInfoDO, orderIdList);
        }
    }

    //更新订单支付状态
    private void updateOrderPayStatus(List<String> orderIdList, Integer payStatus) {
        OrderPaymentDetailDO orderPaymentDetailDO = new OrderPaymentDetailDO();
        orderPaymentDetailDO.setPayStatus(payStatus);
        if (orderIdList.size() == 1) {
            orderPaymentDetailDAO.updateByOrderId(orderPaymentDetailDO, orderIdList.get(0));
        } else {
            orderPaymentDetailDAO.updateBatchByOrderIds(orderPaymentDetailDO, orderIdList);
        }
    }

    //保存订单操作日志
    private void saveOrderOperateLog(String orderId, Integer operateType, Integer preOrderStatus, Integer currentStatus, String remark) {
        OrderOperateLogDO orderOperateLogDO = new OrderOperateLogDO();
        orderOperateLogDO.setOrderId(orderId);
        orderOperateLogDO.setOperateType(operateType);
        orderOperateLogDO.setPreStatus(preOrderStatus);
        orderOperateLogDO.setCurrentStatus(currentStatus);
        orderOperateLogDO.setRemark(remark);
        orderOperateLogDAO.save(orderOperateLogDO);
    }

    //保存子订单操作日志
    private void saveSubOrderOperateLog(Integer subCurrentOrderStatus, List<OrderInfoDO> subOrderInfoDOList) {
        List<OrderOperateLogDO> tempSubOrderOperateLogDOList = new ArrayList<>();
        for (OrderInfoDO subOrderInfo : subOrderInfoDOList) {
            String subOrderId = subOrderInfo.getOrderId();
            Integer subPreOrderStatus = subOrderInfo.getOrderStatus();

            //订单状态变更日志
            OrderOperateLogDO subOrderOperateLogDO = new OrderOperateLogDO();
            subOrderOperateLogDO.setOrderId(subOrderId);
            subOrderOperateLogDO.setOperateType(OrderOperateTypeEnum.PAID_ORDER.getCode());
            subOrderOperateLogDO.setPreStatus(subPreOrderStatus); // CREATED -> PAID
            subOrderOperateLogDO.setCurrentStatus(subCurrentOrderStatus);
            subOrderOperateLogDO.setRemark("订单支付回调操作,子订单状态变更" + subOrderOperateLogDO.getPreStatus() + "-" + subOrderOperateLogDO.getCurrentStatus());
            tempSubOrderOperateLogDOList.add(subOrderOperateLogDO);
        }

        //新增子订单状态变更日志
        if (!tempSubOrderOperateLogDOList.isEmpty()) {
            orderOperateLogDAO.saveBatch(tempSubOrderOperateLogDOList);
        }
    }
    ...
}

14.自动关单功能业务流程梳理

15.XXL-Job分布式调度系统架构原理分析

复制代码
https://www.xuxueli.com/xxl-job/

16.基于XXL-Job分布式调度的定时关单功能实现

复制代码
//自动取消超时订单任务
@Component
public class AutoCancelExpiredOrderTask {
    //订单管理DAO组件
    @Autowired
    private OrderInfoDAO orderInfoDAO;

    @Autowired
    private OrderAfterSaleService orderAfterSaleService;

    @Autowired
    private OrderProperties orderProperties;

    //执行任务逻辑
    @XxlJob("autoCancelExpiredOrderTask")
    public void execute() throws Exception {
        int shardIndex = Optional.ofNullable(XxlJobHelper.getShardIndex()).orElse(0);
        int totalShardNum = Optional.ofNullable(XxlJobHelper.getShardTotal()).orElse(0);
        String param = XxlJobHelper.getJobParam();

        //扫描当前时间 - 订单超时时间 -> 前的一小段时间范围(时间范围用配置中心配置)
        //比如当前时间11:40,订单超时时间是30分钟,扫描11:09:00 -> 11:10:00这一分钟的未支付订单,
        //缺点:有一个订单超过了30 + 1 = 31分钟,都没有被处理(取消),这笔订单就永久待支付
        for (OrderInfoDO order : orderInfoDAO.listAllUnPaid()) {
            if (totalShardNum <= 0) {
                //不进行分片
                doExecute(order);
            } else {
                //分片
                int hash = hash(order.getOrderId()) % totalShardNum;
                if (hash == shardIndex) {
                    doExecute(order);
                }
            }
        }
        XxlJobHelper.handleSuccess();
    }

    private void doExecute(OrderInfoDO order) {
        if (new Date().getTime() - order.getExpireTime().getTime() >= orderProperties.getExpireTime()) {
            //超过30min未支付
            CancelOrderRequest request = new CancelOrderRequest();
            request.setOrderId(order.getOrderId());
            request.setUserId(order.getUserId());
            request.setBusinessIdentifier(order.getBusinessIdentifier());
            request.setOrderType(order.getOrderType());
            request.setCancelType(OrderCancelTypeEnum.TIMEOUT_CANCELED.getCode());
            request.setOrderStatus(order.getOrderStatus());
            try {
                orderAfterSaleService.cancelOrder(request);
            } catch (Exception e) {
                log.error("AutoCancelExpiredOrderTask execute error:", e);
            }
        }
    }

    private int hash(String orderId) {
        //解决取模可能为负数的情况
        return orderId.hashCode() & Integer.MAX_VALUE;
    }
}

17.订单系统与XXL-Job配合运行原理

18.根据XXL-Job源码来验证关单分布式调度原理

复制代码
@Configuration
public class XxlJobConfig {
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        log.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        return xxlJobSpringExecutor;
    }
}

public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton, DisposableBean {
    private static final Logger logger = LoggerFactory.getLogger(XxlJobSpringExecutor.class);

    //start
    @Override
    public void afterSingletonsInstantiated() {
        //init JobHandler Repository
        /*initJobHandlerRepository(applicationContext);*/
        //init JobHandler Repository (for method)
        initJobHandlerMethodRepository(applicationContext);
        //refresh GlueFactory
        GlueFactory.refreshInstance(1);
        //super start
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    ...
}

public class XxlJobExecutor  {
    private static final Logger logger = LoggerFactory.getLogger(XxlJobExecutor.class);
    ...

    public void start() throws Exception {
        //init logpath
        XxlJobFileAppender.initLogPath(logPath);
        //init invoker, admin-client
        initAdminBizList(adminAddresses, accessToken);
        //init JobLogFileCleanThread
        JobLogFileCleanThread.getInstance().start(logRetentionDays);
        //init TriggerCallbackThread
        TriggerCallbackThread.getInstance().start();
        //init executor-server
        initEmbedServer(address, ip, port, appname, accessToken);
    }

    //初始化Netty客户端,监听9999端口
    private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {
        //fill ip port
        port = port>0?port: NetUtil.findAvailablePort(9999);
        ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();
        //generate address
        if (address==null || address.trim().length()==0) {
            String ip_port_address = IpUtil.getIpPort(ip, port);   // registry-address:default use address to registry , otherwise use ip:port if address is null
            address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
        }
        //accessToken
        if (accessToken==null || accessToken.trim().length()==0) {
            logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
        }
        //start
        embedServer = new EmbedServer();
        embedServer.start(address, port, appname, accessToken);
    }
    ...
}

public class EmbedServer {
    private static final Logger logger = LoggerFactory.getLogger(EmbedServer.class);
    private ExecutorBiz executorBiz;
    private Thread thread;

    public void start(final String address, final int port, final String appname, final String accessToken) {
        executorBiz = new ExecutorBizImpl();
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //param
                EventLoopGroup bossGroup = new NioEventLoopGroup();
                EventLoopGroup workerGroup = new NioEventLoopGroup();
                ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
                    0,
                    200,
                    60L,
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>(2000),
                    new ThreadFactory() {
                        @Override
                        public Thread newThread(Runnable r) {
                            return new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode());
                        }
                    },
                    new RejectedExecutionHandler() {
                        @Override
                        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                            throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
                        }
                    });
                try {
                    //start server
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    bootstrap.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel channel) throws Exception {
                                channel.pipeline()
                                    .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS))  // beat 3N, close if idle
                                    .addLast(new HttpServerCodec())
                                    .addLast(new HttpObjectAggregator(5 * 1024 * 1024))  // merge request & reponse to FULL
                                    .addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
                            }
                        })
                        .childOption(ChannelOption.SO_KEEPALIVE, true);
                    //bind
                    ChannelFuture future = bootstrap.bind(port).sync();
                    logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);
                    //注册执行器
                    startRegistry(appname, address);
                    //wait util stop
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    ...
                } finally {
                    //stop
                    try {
                        workerGroup.shutdownGracefully();
                        bossGroup.shutdownGracefully();
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        });

        //daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
        thread.setDaemon(true);    
        thread.start();
    }
    ...

    public static class EmbedHttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
        ...
        @Override
        protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
            //request parse
            //final byte[] requestBytes = ByteBufUtil.getBytes(msg.content());    // byteBuf.toString(io.netty.util.CharsetUtil.UTF_8);
            String requestData = msg.content().toString(CharsetUtil.UTF_8);
            String uri = msg.uri();
            HttpMethod httpMethod = msg.method();
            boolean keepAlive = HttpUtil.isKeepAlive(msg);
            String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);
            //invoke
            bizThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //do invoke
                    Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);
                    //to json
                    String responseJson = GsonTool.toJson(responseObj);
                    //write response
                    writeResponse(ctx, keepAlive, responseJson);
                }
            });
        }

        private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
            //valid
            if (HttpMethod.POST != httpMethod) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
            }
            if (uri==null || uri.trim().length()==0) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
            }
            if (accessToken!=null && accessToken.trim().length()>0 && !accessToken.equals(accessTokenReq)) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
            }

            //services mapping
            try {
                if ("/beat".equals(uri)) {
                    return executorBiz.beat();
                } else if ("/idleBeat".equals(uri)) {
                    IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                    return executorBiz.idleBeat(idleBeatParam);
                } else if ("/run".equals(uri)) {
                    TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                    return executorBiz.run(triggerParam);
                } else if ("/kill".equals(uri)) {
                    KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                    return executorBiz.kill(killParam);
                } else if ("/log".equals(uri)) {
                    LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                    return executorBiz.log(logParam);
                } else {
                    return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
            }
        }
        ...
    }
    ...
}
相关推荐
东阳马生架构2 天前
订单初版—7.支付和履约实现的重构文档
订单系统
ALLSectorSorft4 个月前
打车APP订单系统逻辑梳理与实现
小程序·订单系统·打车app