【订单服务】库存解锁和关单

消息队列流程图

在这里插入图片描述

监听库存解锁

下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁

配置队列和交换机

@Configuration
public class MyRabbitConfig {

    /**
     * 使用json序列化机制,将消息转为json格式
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 模拟消费者监听队列
     */
//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void listener(Channel channel, Message message){
//
//    }

    //声明交换机
    @Bean
    public Exchange stockEventExchange(){
        return new TopicExchange("stock-event-exchange",true,false,null);
    }

    //声明队列
    @Bean
    public Queue stockDelayQueue(){
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",120000);
        Queue queue = new Queue("stock.delay.queue",true,false,false,arguments);
        return queue;
    }

    @Bean
    public Queue stockReleaseStockQueue(){
        Queue queue = new Queue("stock.release.stock.queue",true,false,false,null);
        return queue;
    }

    //声明绑定
    @Bean
    public Binding stockDelayQueueBinding(){
        return new Binding(
                "stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }

    @Bean
    public Binding stockReleaseStockQueueBinding(){
        return new Binding(
                "stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }
}

第一步:发送库存锁定成功消息到mq

告诉mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品

/**
     * 为某个订单锁定库存
     * 一个商品在多个仓库有库存,返回锁定成功还是失败的结果
     *
     * @Transactional 保证事务可以失败后(抛异常)回滚。默认只要是运行时异常,都会回滚
     * <p>
     * 库存解锁的场景
     * 1、下订单成功,订单过期没有支付、被系统自动或手动取消,都要解锁库存
     * 2、下订单成功,库存锁定成功,接下来的业务失败,导致订单回滚,之前锁定的库存就要自动解锁
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        //获取订单号
        String orderSn = vo.getOrderSn();
        //保存库存工作单的详情,声明哪个具体的订单要锁库存,订单号是唯一可识别
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(orderSn);
        wareOrderTaskService.save(taskEntity);

        //TODO 1、找到每个商品在哪些仓库有库存
        //获取所有要锁库存的商品,订单号下所有的订单
        List<OrderItemVo> locks = vo.getLocks();
        //返回 列表(skuId、num、wareId) list(指定商品在不同库存的加购数量)
        List<SkuWareHasStock> skuWareHasStocks = locks.stream().map((item) -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            Integer num = item.getCount();
            stock.setNum(num);
            //查询skuId对应的wareId 表wms_ware_sku
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());
        //TODO 2、锁定库存
        //默认所有的库存都锁定
        Boolean allLock = true;
        for (SkuWareHasStock skuWareHasStock : skuWareHasStocks) {
            //默认skuId商品没锁定库存
            Boolean skuStocked = false;
            //获取要锁定库存的商品
            Long skuId = skuWareHasStock.getSkuId();
            //查询到此商品所在哪些仓库里
            List<Long> wareIds = skuWareHasStock.getWareId();
            if (wareIds == null || wareIds.size() == 0) {
                //如果指定skuId的商品没有仓库里有库存,就抛出异常
                throw new NoStockException(skuId);
            }

            //TODO 3、如果每一个商品都锁定成功,将当前商品锁定的工作单记录表发送给mq
            //如果有库存,遍历每一个有此skuId商品的仓库Id
            for (Long wareId : wareIds) {
                //锁住库存,传入 商品id、仓库id、锁库存数量,指明哪个skuId的商品,要在哪个仓库,更新多少库存
                //返回0行受影响就是没成功,1行受影响就是成功
                Integer num = skuWareHasStock.getNum();
                Long count = wareSkuDao.lockSkuStock(skuId, wareId, num);
                if (count == 1) {
                    //如果当前库存锁定成功
                    skuStocked = true;
                    //TODO 4、告诉mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
                    //封装库存订单详情表,并添加到数据库中,保存库存工作详情单
                    WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity(null, skuId, "", num, taskEntity.getId(), wareId, 1);
                    wareOrderTaskDetailService.save(detailEntity);
                    // StockLockedTo 是 mq专用传输类
                    //设置 【id】 封装工作单的实体类
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    //设置【detailTo】  封装工作详情单的实体类
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    //因为detailEntity和stockDetailTo 属性相同,可以对拷
                    BeanUtils.copyProperties(detailEntity, stockDetailTo);
                    // lockedTo 封装了 库存订单工作表id和详情表实体类
                    lockedTo.setDetailTo(stockDetailTo);
                    //mq发送,只要有一个库存锁定,就发送给mq
                    rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
                    //订单和库存都锁定成功了,就调出for循环
                    break;
                } else {
                    //如果当前仓库锁失败了,说明此仓库库存不足,就尝试下一个仓库

                }
            }
            //如果当前商品的所有仓库都没有锁住,说明所有仓库都没有此skuId商品的库存了
            if (skuStocked == false) {
                //如果指定skuId的商品没有仓库里有库存,就抛出异常
                throw new NoStockException(skuId);
            }
        }
        //如果上面整个for循环都没有抛出异常,说明是全部skuId的商品都锁定库存了
        return true;
    }

	//内部类
    @Getter
    @Setter
    class SkuWareHasStock {
        private Long skuId; //商品
        private Integer num; //购买数量
        private List<Long> wareId; //仓库id
    }

第二步:mq监听队列消息

/**
 * 监听mq的消息,释放锁库存
 * 生产者发送消息--交换机--队列(延迟2分钟无人消费)--根据loutingKey和交换机到达--交换机--队列(被监听)
 *
 */
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    WareSkuService wareSkuService;

    /**
     * 解锁库存
     * 当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
     * @param to  库存锁定成功后,通知mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleStockLockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        System.out.println("2分钟后收到解锁库存的消息,调用解锁方法");
        try {
            wareSkuService.unlockStock(to);
            /**
             * 手动应答,消费者读取完mq消息后,要给生产者一个答复,执行成功了就回复成功,不是批量回复
             * 1、消息的标记 tag
             * 2、是否批量应答 multiple false代表不批量应答信道中的消息  true代表批量
             */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //有异常就拒绝消费,将消息继续扔到队列里,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

第三步:库存解锁业务实现

根据mq监听队列得到的信息,用来解锁库存操作

/**
     * 库存自动解锁:
     * 下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     */
    @Override
    public void unlockStock(StockLockedTo to) {

        //获取到库存详情表vo vo=实体类
        StockDetailTo detailTo = to.getDetailTo();
        //获取库存工作详情表的id
        Long detailId = detailTo.getId();
        //解锁
        //1.查询数据库关于这个订单的锁定库存信息
        // 有:证明锁定成功了
        //    解锁订单情况
        //        1.没有这个订单,不必解锁库存
        //        2.有这个订单。
        ///           订单状态:已取消,解锁库存状态
        //                    没取消,不能解锁
        // 没有:库存锁定失败了,库存回滚了,这种情况无需解锁。
        //根据id查询数据库 库存工作详情表的实体类
        WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
        //如果从数据库中可以查询出数据,说明库存已经被扣减,就需要解锁库存,释放和恢复仓库的商品数量
        if (byId != null) {
            //如果工作详情表有数据,就解锁
            //库存工作单的id   wms_ware_order_task
            Long id = to.getId();
            //根据工作单id查询到工作表实体类
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
            //从工作表中查询到订单号
            String orderSn = taskEntity.getOrderSn();
            //远程调用订单服务,根据订单号查询订单实体类,再查询状态,如果是取消状态,才可以解锁
            R r = orderFeignService.getOrderStatus(orderSn);
            if (r.getCode() == 0) {
                //订单数据返回成功
                OrderVo data = r.getData(new TypeReference<OrderVo>() {});
                //订单已经被取消或被取消状态或没有订单信息,就解锁库存
                if (data == null || data.getStatus() == 4) {
                    //如果当前订单是库存已锁定,才可以解锁
                    if (byId.getLockStatus() == 1) {
                        Long skuId = detailTo.getSkuId();
                        Long wareId = detailTo.getWareId();
                        Integer skuNum = detailTo.getSkuNum();
                        unLockStock(skuId, wareId, skuNum, detailId);
                    }
                }
            } else {
                throw new RuntimeException("远程服务异常");
            }
        }
    }

    /**
     * 解锁库存
     * 哪个商品、哪个仓库、数量、库存工作详情表id
     */
    public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
        //库存解锁
        wareSkuDao.unLockStock(skuId, wareId, num, taskDetailId);
        //更新库存工作详情表的状态为已解锁库存
        WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity();
        detailEntity.setId(taskDetailId);
        detailEntity.setLockStatus(2); //变为已解锁
        //根据id更新 lock_status 锁状态为 已解锁
        wareOrderTaskDetailService.updateById(detailEntity);
    }

定时关单

流程图分析

1、订单创建成功消息——loutingKey(order-create-order)——交换机(order-event-exchange)

/**
* TODO 订单创建成功给mq发送消息
* 默认订单锁定成功,库存锁定也成功,但后面的业务方法错误,因为有@Transactional本地事务,造成订单未下单回滚了,但是库存已经扣除了,没有回滚需要解锁库存(根据工作表和工作详情表),在库存服务里添加监听器监听队列,自动解锁库存
*/
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

2、交换机(order-event-exchange)—— 延时队列(order-delay-queue)——交换机(order-event-exchange)

交换机将订单创建成功的消息发送到延时队列里,30分钟后自动又将订单创建成功的消息发送到交换机里

3、交换机(order-event-exchange)—— loutingKey(order-release-order)——普通队列(order-release-order-queue)

/**
 * 监听mq消息,自动关单
 */
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws Exception{
        System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());

        try {
            orderService.closeOrder(entity);
            /**
             * 手动应答,消费者读取完mq消息后,要给生产者一个答复
             * 1、消息的标记 tag
             * 2、是否批量应答 multiple false代表不批量应答信道中的消息  true代表批量
             */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //如果失败,信道就拒绝消费消息,将此消息扔到队列中,让其他人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}


//进入orderService.closeOrder(entity);
@Override
    public void closeOrder(OrderEntity entity) {
    	//查询当前数据库中订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        //待付款状态需要关闭订单,状态0,待付款
        if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
            //关单:将指定订单号的订单表的状态更改,因为数据库里的订单表和传过来的订单表状态可能不一样,所以要新建订单表
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            //设置订单为已取消订单状态,状态4,已关闭
            update.setStatus(OrderStatusEnum.CANCELED.getCode());
            //将订单状态更新到数据库
            this.updateById(update);

4、释放订单服务消息———— loutingKey(order-release-other)——交换机(order-event-exchange)

@Override
    public void closeOrder(OrderEntity entity) {
        //查询当前数据库中订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        //待付款状态需要关闭订单,状态0,待付款
        if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
            //关单:将指定订单号的订单表的状态更改,因为数据库里的订单表和传过来的订单表状态可能不一样,所以要新建订单表
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            //设置订单为已取消订单状态,状态4,已关闭
            update.setStatus(OrderStatusEnum.CANCELED.getCode());
            //将订单状态更新到数据库
            this.updateById(update);
            /**
             * 创建 mq 专用传输类 ,等于 OrderEntity
             * 订单关闭后,也应该主动发一个消息,告诉服务有一个订单释放了
             * 绑定订单交换机和库存队列,订单释放和库存释放进行绑定
             * 队列发送给交换机信息,需要手动人工用 rabbitTemplate 工具
             * 交换机给队列发信息,自动无须人工手动即可发送
             */
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            //发送给mq一个order
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }
    }

5、交换机(order-event-exchange)—— loutingKey(order-release-other.#)——库存释放队列(stock-release-stock-queue)

/**
 * 监听mq的消息,释放锁库存
 * 生产者发送消息--交换机--队列(延迟2分钟无人消费)--根据loutingKey和交换机到达--交换机--队列(被监听)
 *
 */
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    WareSkuService wareSkuService;

    /**
     * 解锁库存
     * 当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
     * @param to  库存锁定成功后,通知mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleStockLockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        System.out.println("2分钟后收到解锁库存的消息,调用解锁方法");
        try {
            wareSkuService.unlockStock(to);
            /**
             * 手动应答,消费者读取完mq消息后,要给生产者一个答复,执行成功了就回复成功,不是批量回复
             * 1、消息的标记 tag
             * 2、是否批量应答 multiple false代表不批量应答信道中的消息  true代表批量
             */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //有异常就拒绝消费,将消息继续扔到队列里,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /**
     * 库存服务监听 订单服务的关单信息--------------------关单---------------
     * @param orderTo  orderEntity
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo,Message message,Channel channel) throws IOException {
        try {
        	//监听到订单服务的消息后,库存服务开始解锁库存
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
           channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }

    }

当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存

配置队列和交换机

/**
 * mq的配置类
 * 1、只要rabbitmq控制台已经有了我们要创建的队列、要想修改只能删除重新生成队列
 */
@Configuration
public class MyMqConfig {

    /**
     * 消费者监听消费队列 order.release.order.queue
     */
//    @RabbitListener(queues = "order.release.order.queue")
//    public void listener(OrderEntity entity, Channel channel,Message message) throws Exception{
//        System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());
//        /**
//         * 手动应答,消费者读取完mq消息后,要给生产者一个答复
//         * 1、消息的标记 tag
//         * 2、是否批量应答 multiple false代表不批量应答信道中的消息  true代表批量
//         */
//        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
//    }

    //声明延迟队列,指定时间内未消费变为死信
    @Bean
    public Queue orderDelayQueue (){
        //此队列携带参数(交换机、loutingKey、过期时间)
        Map<String,Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange","order-event-exchange");
        arguments.put("x-dead-letter-routing-key","order.release.order");
        arguments.put("x-message-ttl",60000);
        //Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
        Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
        return queue;
    }

    //普通队列,没有参数
    @Bean
    public Queue orderReleaseOrderQueue (){
        Queue queue = new Queue("order.release.order.queue", true, false, false, null);
        return queue;
    }

    //声明普通交换机(没有参数)
    @Bean
    public Exchange orderEventExchange(){
        //TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        return new TopicExchange("order-event-exchange",true,false,null);
    }

    //声明延迟队列和交换机绑定
    @Bean
    public Binding orderCreateOrderBinding(){
        //Binding(String destination, DestinationType destinationType, String exchange, String routingKey,Map<String, Object> arguments)
        return new Binding(
                "order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

	//声明普通队列和交换机绑定
    @Bean
    public Binding orderReleaseOrderBinding(){
        return new Binding(
                "order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

    //声明库存队列和订单交换机绑定 订单释放和库存释放绑定
    @Bean
    public Binding OrderReleaseOtherBinding(){
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }
}

第一步:发送订单成功消息到mq

到了第5步,订单创建成功,发送消息到mq

@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        //将订单提交页面的数据放入此 threadLocal 中,用于共享数据
        orderSubmitVoThreadLocal.set(vo);
        //调用 ThreadLocal,获取同一线程的数据,得到当前登录的用户
        MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get();
        //生成提交订单后返回的封装类
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        //没有抛出异常就设置状态码为0,代表成功
        response.setCode(0);
        //1、验证令牌【令牌的对比和删除必须保证原子性】
        //对比防重删令牌,返回0和1,0代表令牌失败、1代表令牌删除成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //从页面获取防重令牌
        String orderToken = vo.getOrderToken();
        //redis执行lua脚本,将页面输入令牌和redis中保存的令牌做对比,验证成功返回1,失败返回0  order:token+用户id
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if (result == 0l){
            //验证失败
            response.setCode(1);
            return response;
        }else {
            //1、令牌验证成功,解决重复提交订单成功问题,下单、创建订单、验令牌、验价格、锁库存
            OrderCreateTo order = createOrder();
            //2、验价,获取到应付金额
            BigDecimal payAmount = order.getOrder().getPayAmount();
            //获取页面提交的应付金额
            BigDecimal payPrice = vo.getPayPrice();
            //如果数据库订单里的应付金额和页面提交的金额差值小于0.01,就验证价格成功,因为是省略到小数点后两位,如果算相等的话,可能会出现 0.12和0.123456...
            double value = Math.abs(payAmount.subtract(payPrice).doubleValue());
            //如果验价成功
            if (value < 0.01){
                //TODO 三、保存订单
                saveOrder(order);
                // 四、库存锁定,stock_locked 锁定库存数 <= stock 库存数
                //库存锁定,只要有异常回滚订单数据  (订单号,所有订单项)
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                //封装订单号
                wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> locks = order.getOrderItems().stream().map((item) -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                //封装订单项
                wareSkuLockVo.setLocks(locks);
                //TODO 4、远程锁库存
                //远程调用库存服务调用锁定库存方法
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0){
                    //如果锁定成功
                    response.setOrder(order.getOrder());
                    //TODO 5、远程扣减积分
                    //模拟异常,订单数据库回滚,但是库存数据库不回滚,本地事务只能对自己的服务有效
                    //int i = 1/0;
                    
                    /**
                     * TODO 订单创建成功给mq发送消息
                     * 默认订单锁定成功,库存锁定也成功,但后面的业务方法错误,因为有@Transactional本地事务,造成订单未下单回滚了,但是库存已经扣除了,没有回滚
                     * 需要解锁库存(根据工作表和工作详情表),在库存服务里添加监听器监听队列,自动解锁库存
                     */
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

                    return response;
                }else {
                    response.setCode(3);
                    return response;
                }
            }else {
                //如果验价失败
                response.setCode(2);
                return response;
            }
        }
    }

第二步:监听队列

/**
 * 监听mq消息,自动关单
 */
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws Exception{
        System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());

        try {
            orderService.closeOrder(entity);
            /**
             * 手动应答,消费者读取完mq消息后,要给生产者一个答复
             * 1、消息的标记 tag
             * 2、是否批量应答 multiple false代表不批量应答信道中的消息  true代表批量
             */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //如果失败,信道就拒绝消费消息,将此消息扔到队列中,让其他人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

}

第三步:关单业务实现

关单:就是到 oms_order 表里将指定订单的状态改变即可

	//OrderServiceImpl类
	@Override
    public void closeOrder(OrderEntity entity) {
        //查询当前订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        //待付款状态需要关闭订单,状态0,待付款
        if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
        	//关单:将指定订单号的订单表的状态更改,因为数据库里的订单表和传过来的订单表状态可能不一样,所以要新建订单表
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            //设置订单已取消订单状态 状态4,已取消
            update.setStatus(OrderStatusEnum.CANCELED.getCode());
            //将订单状态更新到数据库
            this.updateById(update);
            /**
             * 创建 mq 专用传输类 ,等于 OrderEntity
             * 订单关闭后,也应该主动发一个消息,告诉服务有一个订单释放了
             * 绑定订单交换机和库存队列,订单释放和库存释放进行绑定
             * 队列发送给交换机信息,需要手动人工用 rabbitTemplate 工具
             * 交换机给队列发信息,自动无须人工手动即可发送
             */
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity,orderTo);
            //发送给mq一个order
            rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
        }
    }

第四步:库存服务监听队列

/**
 * 监听mq的消息,释放锁库存
 * 生产者发送消息--交换机--队列(延迟2分钟无人消费)--根据loutingKey和交换机到达--交换机--队列(被监听)
 *
 */
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    WareSkuService wareSkuService;

    /**
     * 解锁库存
     * 当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
     * @param to  库存锁定成功后,通知mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleStockLockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        System.out.println("2分钟后收到解锁库存的消息,调用解锁方法");
        try {
            wareSkuService.unlockStock(to);
            /**
             * 手动应答,消费者读取完mq消息后,要给生产者一个答复,执行成功了就回复成功,不是批量回复
             * 1、消息的标记 tag
             * 2、是否批量应答 multiple false代表不批量应答信道中的消息  true代表批量
             */
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            //有异常就拒绝消费,将消息继续扔到队列里,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /**
     * 库存服务监听 订单服务的关单信息,订单服务关单后,又发送了一条信息给交换机,交换机发送到了库存的释放队列上
     * @param orderTo  orderEntity
     * @param message  
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo,Message message,Channel channel) throws IOException {
        try {
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
           channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

第五步:解锁业务实现

	/**
     * 订单解锁
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期,
     * @param orderTo
     */
    @Transactional
    @Override
    public void unlockStock(OrderTo orderTo) {
        //获取订单号
        String orderSn = orderTo.getOrderSn();
        //获取工作单,防止重复解锁库存
        WareOrderTaskEntity taskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
        //查询工作单id
        Long id = taskEntity.getId();
        //从库存详情工作表里,根据工作单id找到所有没有解锁的库存,然后进行解锁,工作单id可能会重复
        List<WareOrderTaskDetailEntity> entities = wareOrderTaskDetailService.list(
                new QueryWrapper<WareOrderTaskDetailEntity>()
                        .eq("task_id", id)
                        .eq("lock_status", 1));
        for (WareOrderTaskDetailEntity entity : entities) {
            unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
        }
    }

/**
     * 解锁库存
     * 哪个商品、哪个仓库、数量、库存工作详情表id
     */
    public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
        //库存解锁
        wareSkuDao.unLockStock(skuId, wareId, num, taskDetailId);
        //更新库存工作详情表的状态为已解锁库存
        WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity();
        detailEntity.setId(taskDetailId);
        detailEntity.setLockStatus(2); //变为已解锁
        //根据id更新 lock_status 锁状态为 已解锁
        wareOrderTaskDetailService.updateById(detailEntity);
    }

	<update id="unLockStock">
        update wms_ware_sku
        set stock = stock + #{num},stock_locked = stock_locked - #{num}
        where skuId =#{skuId}
        and wareId =#{wareId}
    </update>
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值