使用Rabbitmq死信队列解锁库存

news2024/12/26 12:12:28

一、库存解锁的场景

RabbitMQ库存解锁的场景有很多,以下是一些常见的场景:

  • 订单取消和订单回滚。下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存。

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

订单系统需要通知库存系统,如果想要解锁库存,可以通过stock.locked路由键发送一个消息给交换机stock-event-exchange,消息内容包括哪个订单、哪些商品、多少库存

二、流程:

如图:
在这里插入图片描述

  1. 锁定库存成功,发送消息(库存工作单详情)给stock-event-exchange交换机,路由键stock.locked
  2. stock-event-exchange交换机将消息交给stock.delay.queue队列,路由键stock.locked,存活时间50分钟
  3. 50分钟后,stock.delay.queue队列队列的消息交给死信交换机stock-event-exchange,路由键stock.release
  4. 死信交换机stock-event-exchange将消息叫给stock.release.stock.queue死信队列,路由键stock.release
  5. 使用监听器监听stock.release.stock.queue死信队列,如果发生支付异常或者业务调用失败,导致订单回滚,进行库存解锁

三、RabbitMQ代码实现:

1. 主机、端口、属性配置

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    # 虚拟主机
    virtual-host: /kmall
    # 开启发送端发送确认,无论是否到达broker都会触发回调【发送端确认机制+本地事务表】
    publisher-confirm-type: correlated
    # 开启发送端抵达队列确认,消息未被队列接收时触发回调【发送端确认机制+本地事务表】
    publisher-returns: true
    # 消息在没有被队列接收时是否强行退回
    template:
      mandatory: true
    # 消费者手动确认模式,关闭自动确认,否则会消息丢失
    listener:
      simple:
        acknowledge-mode: manual

2. 开启rabbitMQ

// 开启rabbit
@EnableRabbit
// 开启feign
@EnableFeignClients(basePackages = "com.koo.modules.ware.feign")
// 开启服务注册功能
@EnableDiscoveryClient
@SpringBootApplication
public class WareApplication {

    public static void main(String[] args) {
        SpringApplication.run(WareApplication.class, args);
    }

}

3. 配置rabbitMQ模板与消息格式

@Configuration
public class MyRabbitConfig {


    @Primary
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        // TODO 封装RabbitTemplate
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        initRabbitTemplate(rabbitTemplate);
        return rabbitTemplate;
    }

    @Bean
    public MessageConverter messageConverter() {
        // 使用json序列化器来序列化消息,发送消息时,消息对象会被序列化成json格式
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 定制RabbitTemplate
     * 1、服务收到消息就会回调
     * 1、spring.rabbitmq.publisher-confirms: true
     * 2、设置确认回调
     * 2、消息正确抵达队列就会进行回调
     * 1、spring.rabbitmq.publisher-returns: true
     * spring.rabbitmq.template.mandatory: true
     * 2、设置确认回调ReturnCallback
     * <p>
     * 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
     */
    //@PostConstruct   // (MyRabbitConfig对象创建完成以后,执行这个方法)
    public void initRabbitTemplate(RabbitTemplate rabbitTemplate) {
        /**
         * 发送消息触发confirmCallback回调
         * @param correlationData:当前消息的唯一关联数据(如果发送消息时未指定此值,则回调时返回null)
         * @param ack:消息是否成功收到(ack=true,消息抵达Broker)
         * @param cause:失败的原因
         */
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            System.out.println("发送消息触发confirmCallback回调" +
                    "\ncorrelationData ===> " + correlationData +
                    "\nack ===> " + ack + "" +
                    "\ncause ===> " + cause);
            System.out.println("=================================================");
        });

        /**
         * 消息未到达队列触发returnCallback回调
         * 只要消息没有投递给指定的队列,就触发这个失败回调
         * @param message:投递失败的消息详细信息
         * @param replyCode:回复的状态码
         * @param replyText:回复的文本内容
         * @param exchange:接收消息的交换机
         * @param routingKey:接收消息的路由键
         */
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 需要修改数据库 消息的状态【后期定期重发消息】
            System.out.println("消息未到达队列触发returnCallback回调" +
                    "\nmessage ===> " + message +
                    "\nreplyCode ===> " + replyCode +
                    "\nreplyText ===> " + replyText +
                    "\nexchange ===> " + exchange +
                    "\nroutingKey ===> " + routingKey);
            System.out.println("==================================================");
        });
    }
}

4. 初始化交换机与队列,绑定交换机与队列

@Configuration
public class MyRabbitMQConfig {

    /**
     * 交换机
     * Topic,可以绑定多个队列
     */
    @Bean
    public Exchange stockEventExchange() {
        //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
        return new TopicExchange("stock-event-exchange", true, false);
    }

    /**
     * 死信队列
     * 释放库存
     */
    @Bean
    public Queue stockReleaseStockQueue() {
        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        return new Queue("stock.release.stock.queue", true, false, false);
    }

    /**
     * 延时队列
     * 锁定库存
     */
    @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");
        // 消息过期时间 1.5分钟
        arguments.put("x-message-ttl", 90000);
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    /**
     * 绑定:交换机与死信队列
     * 释放库存
     */
    @Bean
    public Binding stockLocked() {
        //String destination, DestinationType destinationType, String exchange, String routingKey,
        // 			Map<String, Object> arguments
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }

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

四、业务实现:

1. 库存表与实体类

CREATE TABLE `kmall-ware`.`wms_ware_sku`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id',
  `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id',
  `stock` int(11) NULL DEFAULT NULL COMMENT '库存数',
  `sku_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku_name',
  `stock_locked` int(11) NULL DEFAULT 0 COMMENT '锁定库存',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `sku_id`(`sku_id`) USING BTREE,
  INDEX `ware_id`(`ware_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品库存' ROW_FORMAT = Dynamic;
@Data
@TableName("wms_ware_sku")
public class WareSkuEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * id
	 */
	@TableId
	private Long id;
	/**
	 * sku_id
	 */
	private Long skuId;
	/**
	 * 仓库id
	 */
	private Long wareId;
	/**
	 * 库存数
	 */
	private Integer stock;
	/**
	 * sku_name
	 */
	private String skuName;
	/**
	 * 锁定库存
	 */
	private Integer stockLocked;

}

2. 库存工作单与明细

CREATE TABLE `kmall-ware`.`wms_ware_order_task`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `order_id` bigint(20) NULL DEFAULT NULL COMMENT 'order_id',
  `order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'order_sn',
  `consignee` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人',
  `consignee_tel` char(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人电话',
  `delivery_address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配送地址',
  `order_comment` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单备注',
  `payment_way` tinyint(1) NULL DEFAULT NULL COMMENT '付款方式【 1:在线付款 2:货到付款】',
  `task_status` tinyint(2) NULL DEFAULT NULL COMMENT '任务状态',
  `order_body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单描述',
  `tracking_no` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '物流单号',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT 'create_time',
  `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id',
  `task_comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '工作单备注',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 35 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '库存工作单' ROW_FORMAT = Dynamic;
CREATE TABLE `kmall-ware`.`wms_ware_order_task_detail`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id',
  `sku_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku_name',
  `sku_num` int(11) NULL DEFAULT NULL COMMENT '购买个数',
  `task_id` bigint(20) NULL DEFAULT NULL COMMENT '工作单id',
  `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id',
  `lock_status` int(1) NULL DEFAULT NULL COMMENT '1-已锁定  2-已解锁  3-扣减',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 50 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '库存工作单' ROW_FORMAT = Dynamic;

实体类:

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

}
@NoArgsConstructor
@AllArgsConstructor
@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;
	/**
	 * 1-已锁定  2-已解锁  3-扣减
	 */
	private Integer lockStatus;

}

3. 订单与明细包装类

@Data
public class OrderItemVO {

    private Long skuId;

    private Boolean check;

    private String title;

    private String image;

    /**
     * 商品套餐属性
     */
    private List<String> skuAttrValues;

    private BigDecimal price;

    private Integer count;

    private BigDecimal totalPrice;

    /** 商品重量 **/
    private BigDecimal weight = new BigDecimal("0.085");
}
@Data
public class WareSkuLockTO {

    private String orderSn;

    /** 需要锁住的所有库存信息 **/
    private List<OrderItemVO> locks;

}

4. 锁定库存

4.1 控制层

@PostMapping(value = "/lock/order")
 public R orderLockStock(@RequestBody WareSkuLockTO lockTO) {
     try {
         wareSkuService.orderLockStock(lockTO);
         return R.ok();
     } catch (NoStockException e) {
         return R.error(NO_STOCK_EXCEPTION.getCode(), NO_STOCK_EXCEPTION.getMsg());
     }
 }

4.2 实现层

库存锁定,sql执行锁定锁定

  1. 往库存工作单存储当前锁定(本地事务表)
  2. 封装待锁定库存项Map
  3. 查询(库存 - 库存锁定 >= 待锁定库存数)的仓库
  4. 判断是否查询到仓库
  5. 将查询出的仓库数据封装成Map,key:skuId val:wareId
  6. 判断是否为每一个商品项至少匹配了一个仓库
  7. 所有商品都存在有库存的仓库 锁定库存
  8. 锁定成功,跳出循环
  9. 创建库存锁定工作单消息(每一件商品一条消息)
  10. 往库存工作单详情存储当前锁定(本地事务表)
  11. 发送库存锁定成功消息
 @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockTO lockTO) {
        // 按照收货地址找到就近仓库,锁定库存(暂未实现)
        // 采用方案:获取每项商品在哪些仓库有库存,轮询尝试锁定,任一商品锁定失败回滚

        // 1.往库存工作单存储当前锁定(本地事务表)
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(lockTO.getOrderSn());
        orderTaskService.save(taskEntity);

        // 2.封装待锁定库存项Map
        Map<Long, OrderItemVO> lockItemMap = lockTO.getLocks().stream().collect(Collectors.toMap(key -> key.getSkuId(), val -> val));
        // 3.查询(库存 - 库存锁定 >= 待锁定库存数)的仓库
        List<WareSkuEntity> wareSkuEntities = baseMapper.selectListHasSkuStock(lockItemMap.keySet());
        List<WareSkuEntity> wareList = wareSkuEntities.stream().filter(entity -> (entity.getStock() - entity.getStockLocked()) >= lockItemMap.get(entity.getSkuId()).getCount()).collect(Collectors.toList());
        // 4.判断是否查询到仓库
        if (CollectionUtils.isEmpty(wareList)) {
            // 匹配失败,所有商品项没有库存
            Set<Long> skuIds = lockItemMap.keySet();
            throw new NoStockException(skuIds);
        }
        // 5.将查询出的仓库数据封装成Map,key:skuId  val:wareId
        Map<Long, List<WareSkuEntity>> wareMap = wareList.stream().collect(Collectors.groupingBy(key -> key.getSkuId()));
        // 6.判断是否为每一个商品项至少匹配了一个仓库
        List<WareOrderTaskDetailEntity> taskDetails = new ArrayList<>();// 库存锁定工作单详情
        Map<Long, StockLockedTO> lockedMessageMap = new HashMap<>();// 库存锁定工作单消息
        if (wareMap.size() < lockTO.getLocks().size()) {
            // 匹配失败,部分商品没有库存
            Set<Long> skuIds = lockItemMap.keySet();
            skuIds.removeAll(wareMap.keySet());// 求商品项差集
            throw new NoStockException(skuIds);
        } else {
            // 所有商品都存在有库存的仓库
            // 7.锁定库存
            for (Map.Entry<Long, List<WareSkuEntity>> entry : wareMap.entrySet()) {
                Boolean skuStocked = false;
                Long skuId = entry.getKey();// 商品
                OrderItemVO item = lockItemMap.get(skuId);
                Integer count = item.getCount();// 待锁定个数
                List<WareSkuEntity> hasStockWares = entry.getValue();// 有足够库存的仓库
                for (WareSkuEntity ware : hasStockWares) {
                    Long num = baseMapper.lockSkuStock(skuId, ware.getWareId(), count);
                    if (num == 1) {
                        // 8. 锁定成功,跳出循环
                        skuStocked = true;
                        // 创建库存锁定工作单详情(每一件商品锁定详情)
                        WareOrderTaskDetailEntity taskDetail = new WareOrderTaskDetailEntity(null, skuId,
                                item.getTitle(), count, taskEntity.getId(), ware.getWareId(),
                                WareOrderTaskConstant.LockStatusEnum.LOCKED.getCode());
                        taskDetails.add(taskDetail);
                        //9。 创建库存锁定工作单消息(每一件商品一条消息)
                        StockDetailTO detailMessage = new StockDetailTO();
                        BeanUtils.copyProperties(taskDetail, detailMessage);
                        StockLockedTO lockedMessage = new StockLockedTO(taskEntity.getId(), detailMessage);
                        lockedMessageMap.put(skuId, lockedMessage);
                        break;
                    }
                }
                if (!skuStocked) {
                    // 匹配失败,当前商品所有仓库都未锁定成功
                    throw new NoStockException(skuId);
                }
            }
        }

        // 10.往库存工作单详情存储当前锁定(本地事务表)
        orderTaskDetailService.saveBatch(taskDetails);

        // 11.发送消息
        for (WareOrderTaskDetailEntity taskDetail : taskDetails) {
            StockLockedTO message = lockedMessageMap.get(taskDetail.getSkuId());
            message.getDetail().setId(taskDetail.getId());// 存储库存详情ID
            rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", message);
        }
        return true;
    }

5、监听死信队列stock.release.stock.queue

/**
 * 解锁库存,监听死信队列
 *
 * @author: charlin
 **/
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Component
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 库存解锁(监听死信队列)
     * 场景:
     * 1.下订单成功【需要解锁】(订单过期未支付、被用户手动取消、其他业务调用失败(订单回滚))
     * 2.下订单失败【无需解锁】(库存锁定失败(库存锁定已回滚,但消息已发出))
     * <p>
     * 注意:需要开启手动确认,不要删除消息,当前解锁失败需要重复解锁
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTO locked, Message message, Channel channel) throws IOException {
        log.debug("库存解锁,库存工作单详情ID:" + locked.getDetail().getId());
        //当前消息是否重新派发过来
        // Boolean redelivered = message.getMessageProperties().getRedelivered();
        try {
            // 解锁库存
            wareSkuService.unLockStock(locked);
            // 解锁成功,手动确认
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 解锁失败,消息入队
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }

    /**
     * 客户取消订单,监听到消息
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTO orderTo, Message message, Channel channel) throws IOException {
        log.debug("订单关闭准备解锁库存,订单号:" + orderTo.getOrderSn());
        try {
            wareSkuService.unLockStock(orderTo);
            // 手动删除消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 解锁失败 将消息重新放回队列,让别人消费
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
}

6. 库存解锁

 /**
     * 库存解锁
     */
    @Override
    public void unLockStock(StockLockedTO locked) throws Exception {
        StockDetailTO taskDetailTO = locked.getDetail();// 库存工作单详情TO
        WareOrderTaskDetailEntity taskDetail = orderTaskDetailService.getById(taskDetailTO.getId());// 库存工作单详情Entity
        if (taskDetail != null) {
            // 1.工作单未回滚,需要解锁
            WareOrderTaskEntity task = orderTaskService.getById(locked.getId());// 库存工作单Entity
            R r = orderFeignService.getOrderByOrderSn(task.getOrderSn());// 订单Entity
            if (r.getCode() == 0) {
                // 订单数据返回成功
                OrderTO order = r.getData(new TypeReference<OrderTO>() {
                });
                if (order == null || OrderConstant.OrderStatusEnum.CANCLED.getCode().equals(order.getStatus())) {
                    // 2.订单已回滚 || 订单未回滚已取消状态
                    if (WareOrderTaskConstant.LockStatusEnum.LOCKED.getCode().equals(taskDetail.getLockStatus())) {
                        // 订单已锁定状态,需要解锁(消息确认)
                        unLockStock(taskDetailTO.getSkuId(), taskDetailTO.getWareId(), taskDetailTO.getSkuNum(), taskDetailTO.getId());
                    } else {
                        // 订单其他状态,不可解锁(消息确认)
                    }
                }
            } else {
                // 订单远程调用失败(消息重新入队)
                throw new Exception();
            }
        } else {
            // 3.无库存锁定工作单记录,已回滚,无需解锁(消息确认)
        }
    }

    /**
     * 库存解锁
     * 订单解锁触发,防止库存解锁消息优先于订单解锁消息到期,导致库存无法解锁
     */
    @Transactional
    @Override
    public void unLockStock(OrderTO order) {
        String orderSn = order.getOrderSn();// 订单号
        // 1.根据订单号查询库存锁定工作单
        WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
        // 2.按照工作单查询未解锁的库存,进行解锁
        List<WareOrderTaskDetailEntity> taskDetails = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", task.getId())
                .eq("lock_status", WareOrderTaskConstant.LockStatusEnum.LOCKED.getCode()));// 并发问题
        // 3.解锁库存
        for (WareOrderTaskDetailEntity taskDetail : taskDetails) {
            unLockStock(taskDetail.getSkuId(), taskDetail.getWareId(), taskDetail.getSkuNum(), taskDetail.getId());
        }
    }

    /**
     * 库存解锁
     * 1.sql执行释放锁定
     * 2.更新库存工作单状态为已解锁
     *
     * @param skuId
     * @param wareId
     * @param count
     */
    public void unLockStock(Long skuId, Long wareId, Integer count, Long taskDetailId) {
        // 1.库存解锁
        baseMapper.unLockStock(skuId, wareId, count);

        // 2.更新工作单的状态 已解锁
        WareOrderTaskDetailEntity taskDetail = new WareOrderTaskDetailEntity();
        taskDetail.setId(taskDetailId);
        taskDetail.setLockStatus(WareOrderTaskConstant.LockStatusEnum.UNLOCKED.getCode());
        orderTaskDetailService.updateById(taskDetail);
    }

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

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

相关文章

《面试1v1》Redis持久化

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…

css基础知识七:元素水平垂直居中的方法有哪些?如果元素不定宽高呢?

一、背景 在开发中经常遇到这个问题&#xff0c;即让某个元素的内容在水平和垂直方向上都居中&#xff0c;内容不仅限于文字&#xff0c;可能是图片或其他元素 居中是一个非常基础但又是非常重要的应用场景&#xff0c;实现居中的方法存在很多&#xff0c;可以将这些方法分成…

3.用python写网络爬虫,下载缓存

目录 3.1 为链接爬虫添加缓存支持 3.2 磁盘缓存 3.2.1 实现 3.2.2缓存测试 3.2.3节省磁盘空间 3.2.4 清理过期数据 3.2.5缺点 3.3 数据库缓存 3.3.1 NoSQL 是什么 3.3.2 安装 MangoDB 3.3.3 MongoDB 概述 3.3.4 MongoDB 缓存实现 3.3.5 压缩 3.3.6 缓存测试 3.4 本章…

变分自编码(VAE,Variational Auto-Encoder)知识点速览

目录 1. 主要思想 2. 训练和推理过程 3. 编码器和解码器的结构 4. 主要用途 5. 相较于 auto-encoder 的优劣 1. 主要思想 变分自编码器&#xff08;Variational AutoEncoder&#xff0c;简称VAE&#xff09;是一种生成模型&#xff0c;它通过对数据的隐含表示&#xff08;l…

红日靶场(一)外网到内网速通

红日靶场&#xff08;一&#xff09; 下载地址&#xff1a;http://vulnstack.qiyuanxuetang.net/vuln/detail/2/ win7:双网卡机器 win2003:域内机器 win2008域控 web阶段 访问目标机器 先进行一波信息收集&#xff0c;扫一下端口和目录 扫到phpmyadmin&#xff0c;还有一堆…

韶音和cleer、南卡对比哪个好?韶音、南卡、Cleer开放式耳机横评

现如今&#xff0c;不管是通勤路上还是在家休闲娱乐&#xff0c;又或者是运动时&#xff0c;经常能看见很多人佩戴着耳机听音乐&#xff0c;但是&#xff0c;经常佩戴耳机听音乐的小伙伴都知道&#xff0c;入耳式耳机佩戴久了&#xff0c;容易造成耳部酸痛感、胀痛感&#xff0…

Android 安卓开发语言kotlin与Java该如何选择

一、介绍 如今在Android开发中&#xff0c;应用层开发语言主要是Java和Kotlin&#xff0c;Kotlin是后来加入的&#xff0c;主导的语言还是Java。kotlin的加入仿佛让会kotlin语言的开发者更屌一些&#xff0c;其实不然。 有人说kotlin的引入是解决开发者复杂的逻辑&#xff0c;并…

【VSCode】设置关键字高亮的插件 | Highlight Word

目录 一、概述二、安装 highlight-words 插件三、配置 highlight-words 插件3.1 默认配置3.2 修改 settings.json 配置文件 四、设置高亮快捷键F8五、效果演示 一、概述 本文主要介绍在 VSCode 看代码时&#xff0c;怎样使某个单词高亮显示&#xff0c;主要通过以下三步实现&am…

Docker的run流程

底层原理 Docker怎么工作&#xff1f; Docker为什么比VM虚拟机块&#xff1f; 1.Docker有比虚拟机更少的抽象层 2.docker利用的是宿主机的内核&#xff0c;vm需要是Guest OS 所以说&#xff0c;新建一个容器的时候&#xff0c;docker不需要像虚拟机一样加载一个系统内核&am…

[conda]tf_agents和tensorflow-gpu安装傻瓜式教程

1.打开终端或Anaconda Prompt&#xff08;Windows用户&#xff09;。 2.输入以下命令创建新的Python环境&#xff1a; conda create --name <env_name> python<version>其中&#xff0c;<env_name>是您想要创建的环境名称&#xff0c;<version>是您想…

保留纵向连续性的迭代次数估算方法

( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入有3个节点&#xff0c;训练集AB各由5张二值化的图片组成&#xff0c;让B全是0&#xff0c;让差值结构中有6个1.其中有3组 差值结构 A-B 迭代次数 行分布 列分布 0 1 1 0 1 1 3*5*1*2*0-0*0*0*0*0 3977.834 0 1 …

springboot中自定义JavaBean返回的json对象属性名称大写变小写问题

文章目录 springboot中自定义JavaBean返回的json对象属性名称大写变小写问题一、继承类二、手动添加Get方法三、JsonProperty四、spring-boot json(jackson)属性命名策略 springboot中自定义JavaBean返回的json对象属性名称大写变小写问题 开发过程中发现查询返回的数据出现自…

模拟电路系列分享-运放的关键参数3

目录 概要 整体架构流程 技术名词解释 1.输入电压范围 2.优劣范围: 3.理解 技术细节 1.共模抑制比 2.优劣范围 3.理解 小结 概要 提示&#xff1a;这里可以添加技术概要 实际运放与理想运放具有很多差别。理想运放就像一个十全十美的人&#xff0c;他学习100 分&#xff0c;寿…

chatgpt赋能python:Python中的相加功能函数:介绍、应用和示例

Python中的相加功能函数&#xff1a;介绍、应用和示例 Python是一个功能强大的编程语言&#xff0c;拥有许多强大的内置函数和模块。其中一个非常常见的功能是相加或者加法操作。让我们看一下Python中的相加功能函数。 什么是相加&#xff1f; 简而言之&#xff0c;相加是将…

6.17、进程与线程

比如&#xff0c;一边游戏&#xff0c;一边qq聊天&#xff0c;一边听歌&#xff0c;怎么设计&#xff1f; 进程 进程&#xff08;process&#xff09;&#xff1a;程序的一次执行过程&#xff0c;或是正在内存中运行的应用程序。如&#xff1a;运行中的QQ&#xff0c;运行中…

二叉树的基本操作(如何计算二叉树的结点个数,二叉树的高度)

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f…

内网隧道代理技术(八)之Python 反弹Shell

Python 反弹Shell Python介绍 Python由荷兰数学和计算机科学研究学会的吉多范罗苏姆于1990年代初设计&#xff0c;作为一门叫做ABC语言的替代品。Python提供了高效的高级数据结构&#xff0c;还能简单有效地面向对象编程。Python语法和动态类型&#xff0c;以及解释型语言的本…

月薪2万,被新同事15秒气走。

今年&#xff0c;AIGC掀起了巨浪&#xff0c;身边不少人感到前所未有的焦虑&#xff1a; 朋友圈好友晒出的AI美图&#xff0c;仅需15秒&#xff0c;竟比我2周的设计更出色&#xff1b; 公司用AI写的文案&#xff0c;转化率提升了10%&#xff0c;可能要优化人员了; 职场危机提前…

Boost序列化全解析

程序开发中&#xff0c;序列化是经常需要用到的。像一些相对高级语言&#xff0c;比如JAVA, C#都已经很好的支持了序列化&#xff0c;那么C呢&#xff1f;当然一个比较好的选择就是用Boost&#xff0c;这个号称C准标准库的东西。 什么时候需要序列化呢&#xff1f;举个例子&am…

可视化的工时管理:让项目进度真实可见

在现代项目管理中&#xff0c;工时表软件作为一种强大而有效的工具&#xff0c;能够帮助团队更好地管理项目进度。无论是大小型项目&#xff0c;正确使用工时表软件都可以提高团队的效率和项目的可追踪性。本文将介绍一些关键步骤&#xff0c;以帮助企业利用工时表软件来管理项…