大纲
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模式
三.履约系统对拦截履约的处理
一.通过事务消息来更新订单状态+发送释放资产消息
typescript
@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模式
scss
@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);
}
}
}
三.履约系统对拦截履约的处理
scss
@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。虽然可以提升拦截履约接口的性能 + 提高拦截履约的扩展性。但是可能会出现如下的情形:订单状态已取消 + 优惠券已释放 + 已退款,但是订单却还在履约配送中。这种情况下,可能需要订单履约配送各环节需要对订单状态进行判断。
typescript
@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事务消息原理
typescript
@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)取消订单释放资产的代码实现 + 具体业务流程
一.取消订单释放资产的代码实现
typescript
@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)消费释放库存消息时的幂等性处理
typescript
@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)消费释放优惠券消息时的幂等性处理
typescript
@Configuration
public class ConsumerConfig {
@Autowired
private RocketMQProperties rocketMQProperties;
//释放优惠券消息的消费者
@Bean("releaseInventoryConsumer")
public DefaultMQPushConsumer releaseInventoryConsumer(ReleasePropertyListener releasePropertyListener) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.RELEASE_PROPERTY_CONSUMER_GROUP);
consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
consumer.subscribe(RocketMqConstant.CANCEL_RELEASE_PROPERTY_TOPIC, "*");
consumer.registerMessageListener(releasePropertyListener);
consumer.start();
return consumer;
}
}
//监听并消费释放优惠券消息
@Component
public class ReleasePropertyListener implements MessageListenerConcurrently {
@DubboReference(version = "1.0.0")
private MarketApi marketApi;
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try {
for (MessageExt msg : list) {
String content = new String(msg.getBody(), StandardCharsets.UTF_8);
log.info("ReleasePropertyConsumer message:{}", content);
ReleaseUserCouponRequest releaseUserCouponRequest = JSONObject.parseObject(content, ReleaseUserCouponRequest.class);
//释放优惠券
JsonResult<Boolean> jsonResult = marketApi.releaseUserCoupon(releaseUserCouponRequest);
if (!jsonResult.getSuccess()) {
throw new MarketBizException(MarketErrorCodeEnum.CONSUME_MQ_FAILED);
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
log.error("consumer error", e);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
}
@DubboService(version = "1.0.0", interfaceClass = MarketApi.class, retries = 0)
public class MarketApiImpl implements MarketApi {
@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)消费准备退款消息时使用事务消息来进行处理
java
@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)消费实际退款消息失败时会通过重试确保成功
scss
@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时使用事务消息来保证一致性
scss
@RestController
@RequestMapping("/afterSale")
public class AfterSaleController {
...
//用户发起退货售后
@PostMapping("/applyAfterSale")
public JsonResult<Boolean> applyAfterSale(@RequestBody ReturnGoodsOrderRequest returnGoodsOrderRequest) {
//分布式锁
String orderId = returnGoodsOrderRequest.getOrderId();
String key = RedisLockKeyConstants.REFUND_KEY + orderId;
boolean lock = redisLock.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);
}
}
...
}