订单服务------技术点及亮点

news2024/11/19 17:47:06

大技术

线程池来实现异步任务(亮点1)

  /**
     * 去结算确认页时封装订单确认页返回需要用的数据
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {

        //构建OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //获取当前用户登录的信息(直接获取)
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //获取当前线程请求头信息(用于解决Feign异步调用丢失上下文的问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        /**开启第一个异步任务来远程查询所有的收获地址列表**/
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题)
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);

        /**开启第二个异步任务来远程查询购物车所有选中的购物项**/
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

            //每一个线程都来共享之前的请求数据(用于解决Feign异步调用丢失上下文的问题)
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
            /** 开启第三个异步任务来远程批量查询所有商品的库存是否有货**/
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream().map((itemVo -> itemVo.getSkuId())).collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
           //SkuStockVo就是下面的SkuHasStockVo类
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});

            if (skuStockVos != null && skuStockVos.size() > 0) {
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);

        //3、查询用户积分
        Integer integration = memberResponseVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、价格数据由OrderConfirmVo的getTotal方法自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        //防重令牌一个放到redis里  USER_ORDER_TOKEN_PREFIX = "order:token"
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        //防重令牌一个放到前台页面(不过隐藏了)
        confirmVo.setOrderToken(token);

        //阻塞异步线程,只有两个异步都完成了才可以进行下一步
        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    }

@Autowired

private ThreadPoolExecutor executor; //线程池

CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {

/**开启第一个异步任务来远程查询所有的收获地址列表**/

}, executor);

CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {

/**开启第二个异步任务来远程查询购物车所有选中的购物项**/

}, executor).thenRunAsync(() -> {

/** 开启第三个异步任务来远程批量查询所有商品的库存是否有货**/

},executor);

//阻塞异步线程,只有两个异步都完成了才可以进行下一步

CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

注意:addressFuture和cartInfoFuture是两个同时执行的异步任务,而cartInfoFuture里面又开了一个thenRunAsync异步任务,这个异步任务是必须在cartInfoFuture执行完后才可以执行,有先后顺序

防重令牌存到Redis中并且通过lure脚本保证令牌的原子性(亮点2)

防重令牌的格式为key:order:token:+用户id,value:防重令牌的值

//防重令牌(防止表单重复提交)
//为用户设置一个token,三十分钟过期时间(存在redis)
String token = UUID.randomUUID().toString().replace("-", "");
//防重令牌一个放到redis里  USER_ORDER_TOKEN_PREFIX = "order:token"
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);

在点击去结算按钮后生成一个防重令牌,然后放到redis中和当到订单页面(隐藏起来,用户看不到),这个防重令牌是为了防止用户因为网络延迟等原因连续多次点击提交订单按钮,如果不做这个功能那就会生成多个一样的订单

/**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();
//通过lure脚本原子验证令牌和删除令牌(执行脚本后返回long类型的0或1,0表示令牌验证失败,1表示成功)
//Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId())表示存到redis里的值
//   orderToken是页面传过来的令牌(表示当前需要验证的值)
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
        Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
        orderToken);

if (result == 0L) {
    //令牌验证失败
} else {
    //令牌验证成功

这里的防重令牌删除的时候需要考虑下面的问题

  • 先执行业务后删除令牌
  • 先删除令牌后执行业务

如果是先执行业务的话那是不行的,因为在并发的情况下,会有多个请求同时来校验令牌,然后这个令牌还没有删除,那么多个请求就都执行了业务代码,这不合理

如果先删除令牌在执行业务更合理一些,但是也会有问题,加入有两个请求同时校验令牌,同时删除令牌,同时执行业务代码,那还是有问题,所以要保证令牌的校验和删除是原子性操作,也就是只能有一个线程来执行令牌的校验和删除,其他线程只能等待,这样就可以解决上面的问题,这也是为什么要用lure脚本来保证令牌的校验和删除是原子性的原因

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";这句是lure脚本用来保证在redis中校验和删除令牌的原子性

含义:如果在redis中get的第一个key和argv值(也就是要和redis中比较的值)相同那就删除key,删除key成功返回1,如果不相同就返回0

使用RabbitMQ来解库存的分布式事务问题

问题就是提交订单submitOrder这个方法的所有代码实现的业务都要保持原子性,如果出现任何异常都要进行数据回滚,例如这个方法中是先保存订单和订单项的信息到数据库对应的表格中,然后再调用远程服务去锁库存,如果锁库存成功后再执行下面的代码出现异常或者电脑嗝屁了这类情况那前面保存好的订单和订单项就应该数据回滚(也就是删除刚才录入的数据),同时库存服务也应该解库存。一般情况下只需要在submitOrder方法上加一个@Transactional注解就可以了,但是这个注解只能回滚本地的服务,不能回滚库存服务中的锁库存(也就是这个注解可以实现出现异常后删除刚才录入数据库的订单和订单项的数据,但是没办法把锁库存的数量给改回原来的数量,因为锁库存是库存服务的,不是订单服务的,所以没法靠@Transactional注解来实现回滚)

解决办法

这里采用的是通过RabbitMQ的延时队列来实现保持数据的最终一致性,也就是保持数据库的数据最后的一致性,而不是像@Transactional注解一样立马就回滚,立马保持数据的一致性(这里可以理解成@Transactional 是出现异常后飞快的把数据恢复到原来的样子,而保持数据的最终一致性是过了一段时间后才恢复到原来的样子)

那RabbitMQ的延时队列是怎样来实现数据的最终一致性的呢?

简单说下,其实就是用户下订单后就给延时队列发送消息,如果这个订单的状态在指定的时间过后还是待付款就自动取消这个订单,取消了这个订单后就立马发消息给队列去解库存,可以看出这里就不在乎你到底是异常导致的没支付成功还是用户没付款导致的,再或者是电脑嗝屁了,无论是哪种情况都不管,我只管这个订单有没有在指定的时间内把订单的状态改成已支付状态,只要是过了指定的时间订单的状态还是待付款的话那我就给你回滚 数据,不但之前已经存到数据库的订单和订单项数据我都给你删了,而且库存也给你解了,就是这么霸气!从而这样来保持数据库的最终一致性!

图解:用户点击提交订单按钮后会创建一个订单,把这个订单作为一个消息发给topic交换机order-event-exchange,交换机根据路由键order.create.order路由到延时队列order.delay.queue,然后消息在延时队列里等待指定的时间,当时间过期后把这个消息通过交换机order-event-exchange识别他的新的路由键order.release.order路由到新的队列order.release.order.queue(取消订单的队列),这个队列是用于处理用户提交订单但是没有支付订单的情况,然后订单就会被自动取消,但是这里要考虑一个情况(这个特殊情况是需要额外的处理的),那就是取消订单操作因为网络卡顿导致解库存操作先执行的话,这样就导致导致卡顿的订单永远都不能解锁库存,所以订单取消后立马发消息给order-event-exchange交换机,交换机把这个消息通过路由键order.release.other发到stock.release.stock.queue队列(解库存的队列),这个队列其中有个监听方法就是来监听这个消息的,只要监听到这个消息就会立马执行解库存,还有一种就是正常解库存的操作,当订单成功创建后就进行锁库存,锁库存成功后就会发消息给stock-event-exchange交换机,交换机根据路由键stock.locked把消息路由到stock.delay.queue延时队列,时间过期后就把消息根据路由键stock.release路由到stock.release.stock.queue队列(解库存的队列),然后这个消息队列的消息是被一个专门解库存的监听器来监听(注意这里有两种解库存的监听方法,一个是自动解库存的监听,一个是订单服务的订单取消后立马解库存的监听)

具体实现

1、在MyRabbitMQConfig配置类中创建队列、交换机、队列和交换机的绑定关系

订单服务和库存服务都是创建一个主题交换机,两个队列,一个队列是用来存放消息的,另外一个队列是用来存放死信的(也就是死了的消息,这里并没有直接处理掉,而是放到这个队列里)

整体思路

首先生产者发送一个消息给topic交换机order-event-exchange,交换机根据路由键order.create.order路由到延时队列order.delay.queue,然后消息在延时队列里等待指定的时间,当时间过期后还没有被消费就会被当成死信,然后把这个消息通过交换机order-event-exchange识别他的新的路由键order.release.order路由到新的队列order.release.order.queue

package com.saodai.saodaimall.order.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;


/**
 * RabbitMQ配置类
 * 整体思路
 * 首先生产者发送一个消息给topic交换机order-event-exchange,交换机根据路由键order.create.order路由到延时队列order.delay.queue
 * 然后消息在延时队列里等待指定的时间,当时间过期后还没有被消费就会被当成死信,然后把这个消息通过交换机order-event-exchange识别他的新的
 * 路由键order.release.order路由到新的队列order.release.order.queue
 *
 * 这里只有一个交换机,两个队列,一个队列是用来存放消息的,另外一个队列是用来存放死信的(也就是死了的消息,这里并没有直接处理掉,而是放到这个队列里)
 **/

@Configuration
public class MyRabbitMQConfig {

   

    /**
     *
     *创建延时队列
     *延时队列是通过参数来设置的
     * arguments.put("x-dead-letter-exchange", "order-event-exchange");前面的固定的前缀,表示这个队列延时后的消息
     * @return
     */ 
    @Bean
    public Queue orderDelayQueue() {
        //用map构造参数
        HashMap<String, Object> arguments = new HashMap<>();
        //指定延时后的消息的交换机(x-dead-letter-exchange是固定的前缀,order-event-exchange是自定义的交换机)
        arguments.put("x-dead-letter-exchange", "order-event-exchange");
        //死信路由(也就是消息如果超时了就会被当作死信,然后通过路由键order.release.order路由到指定的消息队列)
        arguments.put("x-dead-letter-routing-key", "order.release.order");
        //设置消息过期时间
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
        /*
            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;
    }

    /**
     * 死信队列(也就是到了这个队列的都是要死的消息)
     *
     * @return
     */
    @Bean
    public Queue orderReleaseQueue() {

        Queue queue = new Queue("order.release.order.queue", true, false, false);

        return queue;
    }

    /**
     * TopicExchange
     *创建主题类型的交换机
     * @return
     */
    @Bean
    public Exchange orderEventExchange() {
        /*
         *   String name,
         *   boolean durable,
         *   boolean autoDelete,
         *   Map<String, Object> arguments
         * */
        return new TopicExchange("order-event-exchange", true, false);

    }


    /**
     * 创建交换机和队列的捆绑关系(延时队列捆绑)
     * @return
     */
    @Bean
    public Binding orderCreateBinding() {
        /*
         * String destination, 目的地(队列名或者交换机名字)
         * DestinationType destinationType, 目的地类型(Queue、Exhcange)
         * String exchange,
         * String routingKey,
         * Map<String, Object> arguments
         * */
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    /**
     * 创建交换机和队列的捆绑关系(死信队列捆绑)
     * @return
     */
    @Bean
    public Binding orderReleaseBinding() {

        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

    /**
     * 订单释放直接和库存释放进行绑定
     * @return
     */
    @Bean
    public Binding orderReleaseOtherBinding() {

        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }


    /**
     * 商品秒杀队列
     * @return
     */
    @Bean
    public Queue orderSecKillOrrderQueue() {
        Queue queue = new Queue("order.seckill.order.queue", true, false, false);
        return queue;
    }

    @Bean
    public Binding orderSecKillOrrderQueueBinding() {
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map<String, Object> arguments
        Binding binding = new Binding(
                "order.seckill.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.seckill.order",
                null);

        return binding;
    }


}
  /**
     *
     *创建延时队列
     * @return
     */ 
    @Bean
    public Queue orderDelayQueue() {
        //用map构造参数
        HashMap<String, Object> arguments = new HashMap<>();
        //指定延时后的消息的交换机(x-dead-letter-exchange是固定的前缀,order-event-exchange是自定义的交换机)
        arguments.put("x-dead-letter-exchange", "order-event-exchange");
        //死信路由(也就是消息如果超时了就会被当作死信,然后通过路由键order.release.order路由到指定的消息队列)
        arguments.put("x-dead-letter-routing-key", "order.release.order");
        //设置消息过期时间
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
        /*
            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;
    }

创建特殊的延时队列只需要传入一个map类型的参数进去就可以让普通队列成为一个延时队列

 //指定延时后的消息的交换机(x-dead-letter-exchange是固定的前缀,order-event-exchange是自定义的交换机)
arguments.put("x-dead-letter-exchange", "order-event-exchange");
//死信路由(也就是消息如果超时了就会被当作死信,然后通过路由键order.release.order路由到指定的消息队列)
arguments.put("x-dead-letter-routing-key", "order.release.order");
//设置消息过期时间
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟

x-dead-letter-exchange这个key是rabbitMQ封装好了的固定的前缀,表示这个队列延时后的消息指定使用order-event-exchange这个交换机,这个交换机来把延时的消息进行传输,x-dead-letter-routing-key也是rabbitMQ封装好了的固定的前缀,表示这个队列延时后的消息使用的新路由键是order.release.order,x-message-ttl也是rabbitMQ封装好了的固定的前缀,表示设置延时队列的延时时间是多少,也就是上面的三个key值都是封装好的固定前缀,后面的值才是自定义的

package com.saodai.saodaimall.ware.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;


/**
 * RabbitMQ配置类(一个交换机,两个队列,两个绑定,跟订单服务的基本一样,详细介绍看订单服务的队列)
 *
 */
@Configuration
public class MyRabbitMQConfig {

    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * RabbitMQ要第一次连接上发现没有队列或者交换机才会创建,所以如果没有下面的代码运行会发现官网中并没有创建交换机和队列
     * 下面代码就是模拟监听,这样就可以连接上RabbitMQ,然后可以创建交换机和队列
     * 但是后面要注释掉(自动解锁库存时这里也会监听队列导致多一个消费者,所以要注释掉)
     */
//     @RabbitListener(queues = "stock.release.stock.queue")
//     public void handle(Message message) {
//
//     }


    /**
     * 库存服务默认的交换机
     * @return
     */
    @Bean
    public Exchange stockEventExchange() {
        //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
        return topicExchange;
    }


    /**
     * 普通队列
     * @return
     */
    @Bean
    public Queue stockReleaseStockQueue() {
        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        Queue queue = new Queue("stock.release.stock.queue", true, false, false);
        return queue;
    }


    /**
     *  延迟队列
     * @return
     */
    @Bean
    public Queue stockDelay() {

        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        // 消息过期时间 2分钟
        arguments.put("x-message-ttl", 120000);

        Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
        return queue;
    }



    /**
     * 交换机与普通队列绑定
     * @return
     */
    @Bean
    public Binding stockLocked() {
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map<String, Object> arguments
        Binding binding = new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);

        return binding;
    }



    /**
     * 交换机与延迟队列绑定
     * @return
     */
    @Bean
    public Binding stockLockedBinding() {
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }


}

2、理解订单服务和库存服务的RabbitMQ队列的图

(1)订单服务使用RabbitMQ的整个过程:

1、订单创建成功后发送消息给topic主题交换机order-event-exchange,交换机根据order.create.order路由键把消息路由到order.delay.queue延时队列(订单创建成功是指OrderServiceImpl类中的submitOrder方法执行成功后给order.delay.queue队列发送消息)

rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

2、消息在order.delay.queue延时队列里等待指定的时间,当时间过期后还没有被消费就会被当成死信,然后把这个消息通过order-event-exchange交换机的新的路由键order.release.order路由到order.release.order.queue队列(过期后的路由键和交换机设置是由订单服务的MyRabbitMQConfig配置的)

注意:这里order.delay.queue队列只是作为延时队列来使用的(正常情况是会有队列的监听器来监听这个队列的消息然后消费掉,但是在这个场景中是没有消费者来消费这个队列的消息的,因为这个队列只需要延时就可以了,并不需要消费者,这个队列的消息等待指定的时间后就会被送到order.release.order.queue队列里,从而达到延时队列的效果)

 //交换机(x-dead-letter-exchange是固定的,order-event-exchange是自定义的交换机)
arguments.put("x-dead-letter-exchange", "order-event-exchange");
//死信路由(也就是消息如果超时了就会被当作死信,然后通过路由键order.release.order路由到指定的消息队列)
arguments.put("x-dead-letter-routing-key", "order.release.order");
//设置消息过期时间
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟

3、设置一个监听器用来消费order.release.order.queue队列的消息(这个队列的消息都是已经超时了的消息,也就是模拟用户生成订单后没有支付的订单,所以要写个监听器来取消之前生成的订单)

package com.saodai.saodaimall.order.listener;

import com.rabbitmq.client.Channel;
import com.saodai.saodaimall.order.entity.OrderEntity;
import com.saodai.saodaimall.order.service.OrderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * 订单监听器,监听的是队列order.release.order.queue(定时关闭订单)
 * 但凡被这个监听器监听到的消息都是过期的死信
 **/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {

    @Autowired
    private OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
        try {
            //关闭订单
            orderService.closeOrder(orderEntity);
            //消费者的手动ack确认这条消息被成功消费了
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }

    }

}
    /**
     * 关闭订单(这个方法由OrderCloseListener监听器调用)
     * 这个方法被调用说明这个订单已经过了指定的时间还没有付款
     * 所谓的关闭订单其实就是修改订单的状态,修改成已取消就行了
     * @param orderEntity  前面生成订单时发送给RabbitMQ队列的消息orderEntity
     */
    @Override
    public void closeOrder(OrderEntity orderEntity) {

        //关闭订单之前先查询一下数据库,判断此订单状态是否已支付
        OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().
                eq("order_sn",orderEntity.getOrderSn()));

        //   CREATE_NEW(0,"待付款")(说明这个订单已经过了指定的时间还没有付款)
        if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {
            //如果是待付款状态就可以进行关单
            OrderEntity orderUpdate = new OrderEntity();
            orderUpdate.setId(orderInfo.getId());
            //把待付款修改成已取消的状态即可CANCLED(4,"已取消")
            orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(orderUpdate);

            /**
               这里要考虑一个情况(这个特殊情况是需要下面的额外处理)
             * 防止订单服务卡顿,导致订单状态消息一直改不了,也就是上面的代码因为卡顿导致没有执行
               解库存服务先执行,查订单状态发现不是取消状态,然后什么都不处理
             * 导致卡顿的订单,永远都不能解锁库存
             * 所以订单释放直接和库存释放进行绑定
             */
            // 发送消息给MQ
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderInfo, orderTo);

            try {
                //订单释放直接和库存释放进行绑定
                /**
                 * 订单取消后立马发消息给交换机,交换机把这个消息通过路由键order.release.other发到队列stock.release.stock.queue
                 * 这个路由设置是由MyRabbitMQConfig中的orderReleaseOtherBinding方法进行绑定的
                 */
                rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
            } catch (Exception e) {
                //TODO 定期扫描数据库,重新发送失败的消息
            }
        }
    }
  • 关闭订单之前先查询一下数据库,判断此订单状态是否已支付
  • 关闭订单其实就是修改订单的状态,修改成已取消
  • 这里要考虑一个情况(这个特殊情况是需要额外的处理的)
    • 按理来说是订单服务的取消订单操作是在解库存操作的前面的,也就是一般先会取消订单操作后再去解库存操作,但是如果取消订单操作因为网络卡顿导致解库存操作先执行的话就会出现下面的情况:
      • 解库存的实现逻辑又是先来看看订单的状态是不是已取消,如果是已取消才会去解库存,否则就不会执行解库存操作了,上面的情况就会出现解库存操作来看订单状态的时候发现订单状态是待支付,不是已取消状态, 所以就不执行解库存操作,由于解库存操作只会来查看一次,所以就会导致卡顿的订单,永远都不能解锁库存
      • 解决办法:订单取消后立马发消息给order-event-exchange交换机,交换机把这个消息通过路由键order.release.other发到stock.release.stock.queue队列,这个队列其中有个监听方法就是来监听这个消息的,只要监听到这个消息就会立马执行解库存
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
package com.saodai.common.to;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;

/**
*订单类
*/

@Data
public class OrderTo {

    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 订单号
     */
    private String orderSn;
    /**
     * 使用的优惠券
     */
    private Long couponId;
    /**
     * create_time
     */
    private Date createTime;
    /**
     * 用户名
     */
    private String memberUsername;
    /**
     * 订单总额
     */
    private BigDecimal totalAmount;
    /**
     * 应付总额
     */
    private BigDecimal payAmount;
    /**
     * 运费金额
     */
    private BigDecimal freightAmount;
    /**
     * 促销优化金额(促销价、满减、阶梯价)
     */
    private BigDecimal promotionAmount;
    /**
     * 积分抵扣金额
     */
    private BigDecimal integrationAmount;
    /**
     * 优惠券抵扣金额
     */
    private BigDecimal couponAmount;
    /**
     * 后台调整订单使用的折扣金额
     */
    private BigDecimal discountAmount;
    /**
     * 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
     */
    private Integer payType;
    /**
     * 订单来源[0->PC订单;1->app订单]
     */
    private Integer sourceType;
    /**
     * 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
     */
    private Integer status;
    /**
     * 物流公司(配送方式)
     */
    private String deliveryCompany;
    /**
     * 物流单号
     */
    private String deliverySn;
    /**
     * 自动确认时间(天)
     */
    private Integer autoConfirmDay;
    /**
     * 可以获得的积分
     */
    private Integer integration;
    /**
     * 可以获得的成长值
     */
    private Integer growth;
    /**
     * 发票类型[0->不开发票;1->电子发票;2->纸质发票]
     */
    private Integer billType;
    /**
     * 发票抬头
     */
    private String billHeader;
    /**
     * 发票内容
     */
    private String billContent;
    /**
     * 收票人电话
     */
    private String billReceiverPhone;
    /**
     * 收票人邮箱
     */
    private String billReceiverEmail;
    /**
     * 收货人姓名
     */
    private String receiverName;
    /**
     * 收货人电话
     */
    private String receiverPhone;
    /**
     * 收货人邮编
     */
    private String receiverPostCode;
    /**
     * 省份/直辖市
     */
    private String receiverProvince;
    /**
     * 城市
     */
    private String receiverCity;
    /**
     * 区
     */
    private String receiverRegion;
    /**
     * 详细地址
     */
    private String receiverDetailAddress;
    /**
     * 订单备注
     */
    private String note;
    /**
     * 确认收货状态[0->未确认;1->已确认]
     */
    private Integer confirmStatus;
    /**
     * 删除状态【0->未删除;1->已删除】
     */
    private Integer deleteStatus;
    /**
     * 下单时使用的积分
     */
    private Integer useIntegration;
    /**
     * 支付时间
     */
    private Date paymentTime;
    /**
     * 发货时间
     */
    private Date deliveryTime;
    /**
     * 确认收货时间
     */
    private Date receiveTime;
    /**
     * 评价时间
     */
    private Date commentTime;
    /**
     * 修改时间
     */
    private Date modifyTime;

}

(2)库存服务使用RabbitMQ的整个过程

锁库存成功后就会发消息给stock-event-exchange交换机,交换机根据路由键stock.locked把消息路由到stock.delay.queue延时队列(跟上面一样,这个延时队列的消息不会被消费掉),时间过期后就把消息根据路由键stock.release路由到stock.release.stock.queue队列,然后这个消息队列的消息是被一个专门解库存的监听器来监听(注意这里有两种解库存的监听方法,一个是自动解库存的监听,一个是订单服务的订单取消后立马解库存的监听)

rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
package com.saodai.saodaimall.ware.listener;

import com.rabbitmq.client.Channel;
import com.saodai.common.to.OrderTo;
import com.saodai.common.to.mq.StockLockedTo;
import com.saodai.saodaimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;


/**
 * RabbitMQ的监听器
 * 这里有两个监听方法,这两个监听识别的依据是看传入的是StockLockedTo还是OrderTo
 * 一个是监听的库存自动解锁
 * 一个是监听订单取消后库存解锁
 */

@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    /**
     ** 监听库存自动解锁
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        log.info("******收到解锁库存的信息******");
        try {

            System.out.println("******收到解锁库存的信息******");
            //当前消息是否被第二次及以后(重新)派发过来了
            // Boolean redelivered = message.getMessageProperties().getRedelivered();

            //解锁库存
            wareSkuService.unlockStock(to);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /**
     *
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
     *导致卡顿的订单,永远都不能解锁库存
     * 订单释放直接和库存释放进行绑定
     * @param orderTo
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {

        log.info("******收到订单关闭,准备解锁库存的信息******");

        try {
            wareSkuService.unlockStock(orderTo);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }


}
package com.saodai.common.to.mq;

import lombok.Data;

/**
 * 发送到mq消息队列的to
 **/

@Data
public class StockLockedTo {

    /** 库存工作单的id **/
    private Long id;

    /** 工作单详情的所有信息  StockDetailTo对象内容就是上面的WareOrderTaskDetailEntity **/
    private StockDetailTo detailTo;
}

解锁库存的思路

首先查询数据库的库存详细工作单表看看有没有成功锁定库存(如果成功锁库存了会有对应的一条记录),如果没有那就说明库存没有锁成功,那自然就不需要解锁了

  • 库存详细工作单表有这条记录那就证明库存锁定成功了
    • 具体需不需要解库存还要先看订单状态
      • 先查询有没有这个订单,没有这个订单必须解锁库存(可能出现因为有异常造成的数据回滚导致订单不存在的情况,但是库存锁成功了)
      • 有这个订单,不一定解锁库存,要根据订单的状态来决定是否解库存
        • 订单状态是已取消状态,说明是用户没有支付订单过期了,那就必须解锁库存
        • 订单状态是已支付状态,说明是用户支付成功了,那就不能解锁库存
      • 除了判断上面的情况,还有考虑当前库存详细工作单的状态,只有满足订单状态是已取消状态并且是已锁定的状态那才可以解库存
        • 已锁定:解锁库存
        • 已解锁 :不能再解锁
 /**
     * (这个方法是由StockReleaseListener监听器调用的)
     * 锁库存失败后的自动解锁(也就是回溯)
     * @param to
     */
    @Override
    public void unlockStock(StockLockedTo to) {

        //获取库存详细工作单类
        StockDetailTo detail = to.getDetailTo();
        //库存详细工作单的id
        Long detailId = detail.getId();
		//WareOrderTaskDetailEntity是库存详细工作单类
        WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
        if (taskDetailInfo != null) {
            //查出wms_ware_order_task工作单的信息
            Long id = to.getId();
            //订单锁库存工作单(获取哪个订单要锁库存)
            WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
            //获取订单号查询订单状态
            String orderSn = orderTaskInfo.getOrderSn();
            //远程查询订单信息
            R orderData = orderFeignService.getOrderStatus(orderSn);
            if (orderData.getCode() == 0) {
                //订单数据返回成功
                OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});

                /**
                 *     CREATE_NEW(0,"待付款"),
                 *     PAYED(1,"已付款"),
                 *     SENDED(2,"已发货"),
                 *     RECIEVED(3,"已完成"),
                 *     CANCLED(4,"已取消"),
                 *     SERVICING(5,"售后中"),
                 *     SERVICED(6,"售后完成");
                 */
                //订单不存在(因为有异常造成的数据回滚导致订单不存在)或者订单状态是取消状态(orderInfo.getStatus() == 4)才可解库存
                if (orderInfo == null || orderInfo.getStatus() == 4) {
                    //当前库存工作单详情状态1,已锁定,只有当前库存工作单详情状态未解锁才可以解锁
                    if (taskDetailInfo.getLockStatus() == 1) {
                        //调用真正接库存的方法unLockStock
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }
            } else {
                //消息拒绝以后重新放在队列里面,让别人继续消费解锁
                //远程调用服务失败
                throw new RuntimeException("远程调用服务失败");
            }
        } else {
            //无需解锁
        }
    }

        /**
     * 真正解锁库存的方法(自动解库存)
     * @param skuId 需要解锁库存的商品id
     * @param wareId  需要解锁库存的库存仓库id
     * @param num  需要解锁库存的商品数量
     * @param taskDetailId   库存工作单详情id
     */
    public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {

        //库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
        wareSkuDao.unLockStock(skuId,wareId,num);

        //更新工作单的状态
        WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
        taskDetailEntity.setId(taskDetailId);
        //setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
        taskDetailEntity.setLockStatus(2);
        wareOrderTaskDetailService.updateById(taskDetailEntity);

    }


    /**
     * 订单取消了就立马解库存
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
     * 导致卡顿的订单,永远都不能解锁库存
     * @param orderTo
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void unlockStock(OrderTo orderTo) {

        String orderSn = orderTo.getOrderSn();
        //查一下最新的库存解锁状态,防止重复解锁库存
        WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);

        //按照工作单的id找到所有 没有解锁的库存,进行解锁(lock_status=1表示已锁定库存)
        Long id = orderTaskEntity.getId();
        List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", id).eq("lock_status", 1));

        for (WareOrderTaskDetailEntity taskDetailEntity : list) {
            //解锁库存
            unLockStock(taskDetailEntity.getSkuId(),
                    taskDetailEntity.getWareId(),
                    taskDetailEntity.getSkuNum(),
                    taskDetailEntity.getId());
        }

    }

自动解库存

/**
     * (这个方法是由StockReleaseListener监听器调用的)
     * 锁库存失败后的自动解锁(也就是回溯)
     * @param to
     */
    @Override
    public void unlockStock(StockLockedTo to) {

        //获取库存详细工作单类
        StockDetailTo detail = to.getDetailTo();
        //库存详细工作单的id
        Long detailId = detail.getId();
		//WareOrderTaskDetailEntity是库存详细工作单类
        WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);
        if (taskDetailInfo != null) {
            //查出wms_ware_order_task工作单的信息
            Long id = to.getId();
            //订单锁库存工作单(获取哪个订单要锁库存)
            WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);
            //获取订单号查询订单状态
            String orderSn = orderTaskInfo.getOrderSn();
            //远程查询订单信息
            R orderData = orderFeignService.getOrderStatus(orderSn);
            if (orderData.getCode() == 0) {
                //订单数据返回成功
                OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});

                /**
                 *     CREATE_NEW(0,"待付款"),
                 *     PAYED(1,"已付款"),
                 *     SENDED(2,"已发货"),
                 *     RECIEVED(3,"已完成"),
                 *     CANCLED(4,"已取消"),
                 *     SERVICING(5,"售后中"),
                 *     SERVICED(6,"售后完成");
                 */
                //订单不存在(因为有异常造成的数据回滚导致订单不存在)或者订单状态是取消状态(orderInfo.getStatus() == 4)才可解库存
                if (orderInfo == null || orderInfo.getStatus() == 4) {
                    //当前库存工作单详情状态1,已锁定,只有当前库存工作单详情状态未解锁才可以解锁
                    if (taskDetailInfo.getLockStatus() == 1) {
                        //调用真正接库存的方法unLockStock
                        unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);
                    }
                }
            } else {
                //消息拒绝以后重新放在队列里面,让别人继续消费解锁
                //远程调用服务失败
                throw new RuntimeException("远程调用服务失败");
            }
        } else {
            //无需解锁
        }
    }

        /**
     * 真正解锁库存的方法(自动解库存)
     * @param skuId 需要解锁库存的商品id
     * @param wareId  需要解锁库存的库存仓库id
     * @param num  需要解锁库存的商品数量
     * @param taskDetailId   库存工作单详情id
     */
    public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {

        //库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
        wareSkuDao.unLockStock(skuId,wareId,num);

        //更新工作单的状态
        WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
        taskDetailEntity.setId(taskDetailId);
        //setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
        taskDetailEntity.setLockStatus(2);
        wareOrderTaskDetailService.updateById(taskDetailEntity);

    }
  • 自动解库存的具体实现流程
    • 获取库存详细工作单的id
package com.saodai.common.to.mq;

import lombok.Data;

/**
 * 发送到mq消息队列的to
 **/

@Data
public class StockLockedTo {

    /** 库存工作单的id **/
    private Long id;

    /** 工作单详情的所有信息 **/
    private StockDetailTo detailTo;
}
package com.saodai.common.to.mq;

import lombok.Data;

/**
 * 其实就是库存工作单详情实体类(具体给订单的哪个商品锁库存)
 **/

@Data
public class StockDetailTo {

    private Long id;
    /**
     * sku_id
     */
    private Long skuId;
    /**
     * sku_name
     */
    private String skuName;
    /**
     * 购买个数
     */
    private Integer skuNum;
    /**
     * 工作单id
     */
    private Long taskId;

    /**
     * 仓库id
     */
    private Long wareId;

    /**
     * 锁定状态
     */
    private Integer lockStatus;

}
    • 查询数据库有没有这个库存详细工作单类
package com.saodai.saodaimall.ware.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 库存工作单详情(具体给订单的哪个商品锁库存)
 */

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * sku_id
	 */
	private Long skuId;
	/**
	 * sku_name
	 */
	private String skuName;
	/**
	 * 购买个数
	 */
	private Integer skuNum;
	/**
	 * 工作单id
	 */
	private Long taskId;

	/**
	 * 仓库id
	 */
	private Long wareId;

	/**
	 * 锁定状态
	 */
	private Integer lockStatus;

}
    • 查询订单锁库存工作单(获取哪个订单要锁库存)
package com.saodai.saodaimall.ware.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 订单锁库存工作单(表示我准备要给哪个订单锁库存了)
 */
@Data
@TableName("wms_ware_order_task")
public class WareOrderTaskEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * order_id
	 */
	private Long orderId;
	/**
	 * order_sn
	 */
	private String orderSn;
	/**
	 * 收货人
	 */
	private String consignee;
	/**
	 * 收货人电话
	 */
	private String consigneeTel;
	/**
	 * 配送地址
	 */
	private String deliveryAddress;
	/**
	 * 订单备注
	 */
	private String orderComment;
	/**
	 * 付款方式【 1:在线付款 2:货到付款】
	 */
	private Integer paymentWay;
	/**
	 * 任务状态
	 */
	private Integer taskStatus;
	/**
	 * 订单描述
	 */
	private String orderBody;
	/**
	 * 物流单号
	 */
	private String trackingNo;
	/**
	 * create_time
	 */
	private Date createTime;
	/**
	 * 仓库id
	 */
	private Long wareId;
	/**
	 * 工作单备注
	 */
	private String taskComment;

}
    • 根据订单号远程查询订单
package com.saodai.saodaimall.ware.vo;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;



@Data
public class OrderVo {

    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 订单号
     */
    private String orderSn;
    /**
     * 使用的优惠券
     */
    private Long couponId;
    /**
     * create_time
     */
    private Date createTime;
    /**
     * 用户名
     */
    private String memberUsername;
    /**
     * 订单总额
     */
    private BigDecimal totalAmount;
    /**
     * 应付总额
     */
    private BigDecimal payAmount;
    /**
     * 运费金额
     */
    private BigDecimal freightAmount;
    /**
     * 促销优化金额(促销价、满减、阶梯价)
     */
    private BigDecimal promotionAmount;
    /**
     * 积分抵扣金额
     */
    private BigDecimal integrationAmount;
    /**
     * 优惠券抵扣金额
     */
    private BigDecimal couponAmount;
    /**
     * 后台调整订单使用的折扣金额
     */
    private BigDecimal discountAmount;
    /**
     * 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
     */
    private Integer payType;
    /**
     * 订单来源[0->PC订单;1->app订单]
     */
    private Integer sourceType;
    /**
     * 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
     */
    private Integer status;
    /**
     * 物流公司(配送方式)
     */
    private String deliveryCompany;
    /**
     * 物流单号
     */
    private String deliverySn;
    /**
     * 自动确认时间(天)
     */
    private Integer autoConfirmDay;
    /**
     * 可以获得的积分
     */
    private Integer integration;
    /**
     * 可以获得的成长值
     */
    private Integer growth;
    /**
     * 发票类型[0->不开发票;1->电子发票;2->纸质发票]
     */
    private Integer billType;
    /**
     * 发票抬头
     */
    private String billHeader;
    /**
     * 发票内容
     */
    private String billContent;
    /**
     * 收票人电话
     */
    private String billReceiverPhone;
    /**
     * 收票人邮箱
     */
    private String billReceiverEmail;
    /**
     * 收货人姓名
     */
    private String receiverName;
    /**
     * 收货人电话
     */
    private String receiverPhone;
    /**
     * 收货人邮编
     */
    private String receiverPostCode;
    /**
     * 省份/直辖市
     */
    private String receiverProvince;
    /**
     * 城市
     */
    private String receiverCity;
    /**
     * 区
     */
    private String receiverRegion;
    /**
     * 详细地址
     */
    private String receiverDetailAddress;
    /**
     * 订单备注
     */
    private String note;
    /**
     * 确认收货状态[0->未确认;1->已确认]
     */
    private Integer confirmStatus;
    /**
     * 删除状态【0->未删除;1->已删除】
     */
    private Integer deleteStatus;
    /**
     * 下单时使用的积分
     */
    private Integer useIntegration;
    /**
     * 支付时间
     */
    private Date paymentTime;
    /**
     * 发货时间
     */
    private Date deliveryTime;
    /**
     * 确认收货时间
     */
    private Date receiveTime;
    /**
     * 评价时间
     */
    private Date commentTime;
    /**
     * 修改时间
     */
    private Date modifyTime;

}
    • 进行双重判断
      • 先判断订单不存在(因为有异常造成的数据回滚导致订单不存在)或者订单状态是取消状态
      • 在判断当前库存工作单详情状态是不是1,1表示已锁定,只有当前库存工作单详情状态未解锁才可以解锁
    • 调用unLockStock方法实现真正的解库存(自动解库存)
      • 更新库存的数量(还原)
      • 更新工作单的状态为已解锁
 /**
     * 真正解锁库存的方法(自动解库存)
     * @param skuId 需要解锁库存的商品id
     * @param wareId  需要解锁库存的库存仓库id
     * @param num  需要解锁库存的商品数量
     * @param taskDetailId   库存工作单详情id
     */
    public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {

        //库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
        wareSkuDao.unLockStock(skuId,wareId,num);

        //更新工作单的状态
        WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
        taskDetailEntity.setId(taskDetailId);
        //setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
        taskDetailEntity.setLockStatus(2);
        wareOrderTaskDetailService.updateById(taskDetailEntity);

    }
<!--    解锁库存-->
<update id="unLockStock">
  UPDATE wms_ware_sku
  SET stock_locked = stock_locked - #{num}
  WHERE
  sku_id = ${skuId}
  AND ware_id = #{wareId}
</update>

 

手动解库存

  • 订单服务的订单取消后立马解库存的具体逻辑
    • 首先通过订单号查询订单锁库存工作单
    • 通过订单锁库存工作单的id去库存详细工作单去找对应的锁库存的记录,看有没有记录并且锁库存的状态是已锁定的状态,防止多次重复解库存(其中库存详细工作单中的工作id的值就是订单锁库存工作单的id的值)
    • 最后调用真正的解库存方法来解库存
   /**
     * 真正解锁库存的方法(自动解库存)
     * @param skuId 需要解锁库存的商品id
     * @param wareId  需要解锁库存的库存仓库id
     * @param num  需要解锁库存的商品数量
     * @param taskDetailId   库存工作单详情id
     */
    public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {

        //库存解锁(其实就是修改wms_ware_sku表中的stock_locked的值,之前锁库存锁了多少个就减去多少个)
        wareSkuDao.unLockStock(skuId,wareId,num);

        //更新工作单的状态
        WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
        taskDetailEntity.setId(taskDetailId);
        //setLockStatus(2)表示变为已解锁(1表示已锁定,2表示已解锁,3表示减扣)
        taskDetailEntity.setLockStatus(2);
        wareOrderTaskDetailService.updateById(taskDetailEntity);

    }


    /**
     * 订单取消了就立马解库存
     * 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理
     * 导致卡顿的订单,永远都不能解锁库存
     * @param orderTo
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void unlockStock(OrderTo orderTo) {

        String orderSn = orderTo.getOrderSn();
        //查一下最新的库存解锁状态,防止重复解锁库存
        WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);

        //按照工作单的id找到所有 没有解锁的库存,进行解锁(lock_status=1表示已锁定库存)
        Long id = orderTaskEntity.getId();
        List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", id).eq("lock_status", 1));

        for (WareOrderTaskDetailEntity taskDetailEntity : list) {
            //解锁库存
            unLockStock(taskDetailEntity.getSkuId(),
                    taskDetailEntity.getWareId(),
                    taskDetailEntity.getSkuNum(),
                    taskDetailEntity.getId());
        }

    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/72216.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小红书推广笔记怎么合作?这里都给大家梳理好啦

截止 2021 年 11 月&#xff0c;小红书月活已达到 2 亿。其中72% 是 90 后群体&#xff0c;50% 的用户在一二线城市。 这些用户流量有着高消费、爱时尚、爱分享、追求品质生活的特点&#xff0c;所以小红书逐渐成为众多品牌方种草推广的必争之地。 小红书推广笔记怎么合作的呢…

FPGA学习笔记(十)IP核之PLL锁相环的学习总结

系列文章目录 一、FPGA学习笔记&#xff08;一&#xff09;入门背景、软件及时钟约束 二、FPGA学习笔记&#xff08;二&#xff09;Verilog语法初步学习(语法篇1) 三、FPGA学习笔记&#xff08;三&#xff09; 流水灯入门FPGA设计流程 四、FPGA学习笔记&#xff08;四&…

pytest自动化测试框架详解+mark标记+fixture夹具

介绍 pytest是python的一种单元测试框架&#xff0c;同自带unittest框架类似&#xff0c;功能&#xff0c;效率更强大 特点&#xff1a; 1. 非常容易上手&#xff0c;入门简单&#xff0c;丰富的文档 2. 支持参数化 3. 执行测试用例的过程中&#xff0c;跳过某些用例&#x…

Python环境搭建

将向大家介绍如何在本地搭建Python开发环境。 Python可应用于多平台包括 Linux 和 Mac OS X。 你可以通过终端窗口输入 "python" 命令来查看本地是否已经安装Python以及Python的安装版本。 Unix (Solaris, Linux, FreeBSD, AIX, HP/UX, SunOS, IRIX, 等等。) W…

Dev-C++ 的下载、安装和配置

Dev-C 的下载、安装和配置 Dev-C 下载 我们直接打开 https://sourceforge.net/projects/orwelldevcpp/ 点击 Download 进行下载即可&#xff0c;如下图所示&#xff1a; 下载速度也不错&#xff0c;很快就可以下载完成了。 不像某些博主&#xff0c;为了引流量&#xff0c;…

Cadence Allegro PCB设计88问解析(二十二) 之 Allegro中放入元器件

一个学习信号完整性仿真的layout工程师 在之前的文章和大家介绍过Allegro中导入网表&#xff0c;那么导入网表成功之后&#xff0c;还在Allegro在进行一步就是放入导入成功的器件。下面和大家简单介绍具体的操作过程&#xff1a; 第一步&#xff1a;添加版外形outline 我们在…

DAX:SUMMARIZE的秘密

作为一个查询函数&#xff0c;SUMMARIZE 执行三个操作&#xff1a; 它可以按表本身或相关表的任何列对表进行分组&#xff1b;它可以创建新列&#xff0c;在行上下文和过滤上下文中计算表达式&#xff1b;它可以产生不同级别的小计(subtotal)。 在 SUMMARIZE 的三个主要操作中&…

PHP代码审计系列(三)

PHP代码审计系列&#xff08;三&#xff09; 本系列将收集多个PHP代码安全审计项目从易到难&#xff0c;并加入个人详细的源码解读。此系列将进行持续更新。 sql闭合绕过 源码如下 <?phpif($_POST[user] && $_POST[pass]) {$conn mysql_connect("*******…

关于操作PPL进程引发的一些问题

背景 之前发布过一篇关于procexp的利用&#xff0c;但是最近有brother说&#xff0c;最新的版本不能利用了&#xff0c;然后就去下载了最新版本的。 发现判断了是不是受保护进程&#xff0c;不是就拒绝。 这里简单看一下这个函数&#xff0c;发现对比的是_PS_PROTECTION,其实就…

windows cmd指令记录、监听进程是否存在、exe注册到服务中开机后台运行

cmd指令记录 mstsc&#xff1a;远程桌面 regedit&#xff1a;打开注册表 control userpasswords2&#xff1a;去掉开机登录界面 shell:startup&#xff1a;进入登录界面后自启程序 services.msc&#xff1a;启动、终止并设置服务管理界面 shutdown -r -t 0&#xff1a;-r…

一步一步学Iceberg-对比Flink+Kafka和Flink+Iceberg的差异

一 FlinkKafka做实时数仓的弊端 我们以前&#xff08;包括现在&#xff09;还有用FlinkKafka做实时数仓&#xff0c;以及日志传输。Kafka本身的存储成本很高&#xff0c;并且数据保留时间有时效性&#xff0c;一旦消费积压&#xff0c;数据达到过期事件后&#xff0c;就会导致…

[附源码]JAVA毕业设计文件发布系统(系统+LW)

[附源码]JAVA毕业设计文件发布系统&#xff08;系统LW&#xff09; 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; …

Talk预告 | 剑桥大学在读博士生苏熠暄:对比搜索(Contrastive Search)—当前最优的文本生成算法

本期为TechBeat人工智能社区第461期线上Talk&#xff01; 北京时间12月8日(周四)20:00&#xff0c;剑桥大学在读博士生——苏熠暄的Talk将准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “对比搜索&#xff08;Contrastive Search&#xff09;—当前最优的…

【Keras计算机视觉】Faster R-CNN神经网络实现目标检测实战(附源码和数据集 超详细)

需要源码请点赞关注收藏后评论区留言私信~~~ 一、目标检测的概念 目标检测是计算机视觉和数字图像处理的一个热门方向&#xff0c;广泛应用于机器人导航、智能视频监控、工业检测、航空航天等诸多领域&#xff0c;通过计算机视觉减少对人力资本的消耗&#xff0c;具有重要的现…

抗疫专题网页设计 致敬最美逆行者网页制作 疫情感动人物静态HTML网页

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

开发必装最实用工具软件与网站

声明&#xff1a;有些不管你是不是程序员都可以装的软件&#xff0c;我都备注了 我是一名java开发工程师嘛&#xff0c;就给大家分享一下实用的软件 这里我就不介绍idea、postman 如果只是代码编辑&#xff0c;可以看推荐一下我使用的开发工具_我认不到你的博客-CSDN博客 存放…

【软件测试】资深测试告诉你做好测试需求分析是有多么重要......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 测试需求到底是什么…

iwebsec靶场 文件上传漏洞通关笔记2-第02关 文件名过滤绕过

目录 第02关 文件名过滤绕过 1.打开靶场 2.源码分析 3.上传info.pHp 4.获取上传脚本地址 5.访问上传脚本 第02关 文件名过滤绕过 1.打开靶场 iwebsec 靶场漏洞库iwebsechttp://iwebsec.com:81/upload/02.php 随手上传脚本info.php&#xff0c;如下所示提示不能上传php文…

日志:logback、枚举

logback下载步骤&#xff1a; logback官网https://logback.qos.ch/index.html教程http://t.csdn.cn/xSK0I 点击SLF4J API进去&#xff0c;注意看右上角的标题是有变化的&#xff0c;是什么目录下就会显示什么目录&#xff0c;点击下载 然后一样点击右下角的Maven 下载这三个&a…

12月8日:thinkphp中的杂项

缓存 缓存概念及类型&#xff1a;什么是缓存 - 与非网 缓存(cache)一般存放在runtime目录下&#xff0c;页面请求一次将请求内容进行缓存&#xff0c;下一次再次访问&#xff0c;会直接访问到缓存内容&#xff0c;不会再次请求服务器&#xff0c;减少服务器压力 场景&#xff…