订单服务-----遇到的问题及解决方案
订单服务的问题及解决方案
问题1:Feign远程调用时丢失请求头


编辑
出现这个Feign远程调用时丢失请求头的问题是因为Feign在远程调用的时候会创建一个新的请求,但是这个新的请求里啥都没有,没有cookie值,而这个cookie值里有成功登录后的信息,所以由于新请求中没有cookie值就会被购物车服务的登录拦截器给拦截了
package com.saodai.saodaimall.order.config;import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/**
*feign拦截器功能(用于解决feign远程调用时请求头丢失的问题)
*在配置类里写的拦截器是不需要手动注册到springMVC中,因为用注解他会自动注册
**/@Configuration
public class GuliFeignConfig {@Bean("requestInterceptor")public RequestInterceptor requestInterceptor() {RequestInterceptor requestInterceptor = new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {//1、使用RequestContextHolder拿到刚进来的请求数据ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {//老请求HttpServletRequest request = requestAttributes.getRequest();if (request != null) {//2、同步请求头的数据(主要是cookie)//获取老请求的cookie值String cookie = request.getHeader("Cookie");//把老请求的cookie值放到新请求上来,进行一个同步,template表示新请求template.header("Cookie", cookie);}}}};return requestInterceptor;}}

加个RequestInterceptor拦截器重写apply方法把老请求的cookie值设置到新请求中去,这样就解决了这个问题
问题2:Feign异步情况丢失上下文问题


编辑


编辑
导致Feign异步情况丢失上下文问题是因为Feign在远程调用服务的时候数据都是放在TreadLocal(RequestContextHolder获取的请求信息,而RequestContextHolder是用TreadLocal做的)中的,TreadLocal中同一个线程共享数据是没有什么问题的,也就是原先同步的时候是没有问题,但是由于为了提高效率开了异步任务,自然异步任务的线程跟原来的线程不是同一个线程,就会出现丢失上下的情况
@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {//获取当前线程请求头信息(包括上下文)RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();/**开启第一个异步任务**/CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {//把当前线程请求头信息(包括上下文)设置为异步线程中的请求头信息(包括上下文)RequestContextHolder.setRequestAttributes(requestAttributes);//异步任务}, threadPoolExecutor);}
}

解决办法就是把原来线程的的上下文设置到每一个异步线程的上下文中即可
问题3:解库存的分布式事务问题
问题就是提交订单submitOrder这个方法的所有代码实现的业务都要保持原子性,如果出现任何异常都要进行数据回滚,例如这个方法中是先保存订单和订单项的信息到数据库对应的表格中,然后再调用远程服务去锁库存,如果锁库存成功后再执行下面的代码出现异常或者电脑嗝屁了这类情况那前面保存好的订单和订单项就应该数据回滚(也就是删除刚才录入的数据),同时库存服务也应该解库存。一般情况下只需要在submitOrder方法上加一个@Transactional注解就可以了,但是这个注解只能回滚本地的服务,不能回滚库存服务中的锁库存(也就是这个注解可以实现出现异常后删除刚才录入数据库的订单和订单项的数据,但是没办法把锁库存的数量给改回原来的数量,因为锁库存是库存服务的,不是订单服务的,所以没法靠@Transactional注解来实现回滚)
解决办法
这里采用的是通过RabbitMQ的延时队列来实现保持数据的最终一致性,也就是保持数据库的数据最后的一致性,而不是像@Transactional注解一样立马就回滚,立马保持数据的一致性(这里可以理解成@Transactional 是出现异常后飞快的把数据恢复到原来的样子,而保持数据的最终一致性是过了一段时间后才恢复到原来的样子)
那RabbitMQ的延时队列是怎样来实现数据的最终一致性的呢?


编辑
简单说下,其实就是用户下订单后就给延时队列发送消息,如果这个订单的状态在指定的时间过后还是待付款就自动取消这个订单,取消了这个订单后就立马发消息给队列去解库存,可以看出这里就不在乎你到底是异常导致的没支付成功还是用户没付款导致的,再或者是电脑嗝屁了,无论是哪种情况都不管,我只管这个订单有没有在指定的时间内把订单的状态改成已支付状态,只要是过了指定的时间订单的状态还是待付款的话那我就给你回滚 数据,不但之前已经存到数据库的订单和订单项数据我都给你删了,而且库存也给你解了,就是这么霸气!从而这样来保持数据库的最终一致性!
具体实现
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*/ @Beanpublic 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*/@Beanpublic Queue orderReleaseQueue() {Queue queue = new Queue("order.release.order.queue", true, false, false);return queue;}/*** TopicExchange*创建主题类型的交换机* @return*/@Beanpublic Exchange orderEventExchange() {/** String name,* boolean durable,* boolean autoDelete,* Map<String, Object> arguments* */return new TopicExchange("order-event-exchange", true, false);}/*** 创建交换机和队列的捆绑关系(延时队列捆绑)* @return*/@Beanpublic 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*/@Beanpublic Binding orderReleaseBinding() {return new Binding("order.release.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.release.order",null);}/*** 订单释放直接和库存释放进行绑定* @return*/@Beanpublic Binding orderReleaseOtherBinding() {return new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.release.other.#",null);}/*** 商品秒杀队列* @return*/@Beanpublic Queue orderSecKillOrrderQueue() {Queue queue = new Queue("order.seckill.order.queue", true, false, false);return queue;}@Beanpublic Binding orderSecKillOrrderQueueBinding() {//String destination, DestinationType destinationType, String exchange, String routingKey,// Map<String, Object> argumentsBinding binding = new Binding("order.seckill.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.seckill.order",null);return binding;}}/****创建延时队列* @return*/ @Beanpublic 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*/@Beanpublic MessageConverter messageConverter() {return new Jackson2JsonMessageConverter();}/*** RabbitMQ要第一次连接上发现没有队列或者交换机才会创建,所以如果没有下面的代码运行会发现官网中并没有创建交换机和队列* 下面代码就是模拟监听,这样就可以连接上RabbitMQ,然后可以创建交换机和队列* 但是后面要注释掉(自动解锁库存时这里也会监听队列导致多一个消费者,所以要注释掉)*/
// @RabbitListener(queues = "stock.release.stock.queue")
// public void handle(Message message) {
//
// }/*** 库存服务默认的交换机* @return*/@Beanpublic Exchange stockEventExchange() {//String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsTopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);return topicExchange;}/*** 普通队列* @return*/@Beanpublic Queue stockReleaseStockQueue() {//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsQueue queue = new Queue("stock.release.stock.queue", true, false, false);return queue;}/*** 延迟队列* @return*/@Beanpublic 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*/@Beanpublic Binding stockLocked() {//String destination, DestinationType destinationType, String exchange, String routingKey,// Map<String, Object> argumentsBinding binding = new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.release.#",null);return binding;}/*** 交换机与延迟队列绑定* @return*/@Beanpublic 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 {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic 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*/@Overridepublic 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);/**这里要考虑一个情况(这个特殊情况是需要下面的额外处理)* 防止订单服务卡顿,导致订单状态消息一直改不了,也就是上面的代码因为卡顿导致没有执行解库存服务先执行,查订单状态发现不是取消状态,然后什么都不处理* 导致卡顿的订单,永远都不能解锁库存* 所以订单释放直接和库存释放进行绑定*/// 发送消息给MQOrderTo 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 {@Autowiredprivate WareSkuService wareSkuService;/**** 监听库存自动解锁*/@RabbitHandlerpublic 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*/@RabbitHandlerpublic 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*/@Overridepublic void unlockStock(StockLockedTo to) {//获取库存详细工作单类StockDetailTo detail = to.getDetailTo();//库存详细工作单的idLong 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) {//调用真正接库存的方法unLockStockunLockStock(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)@Overridepublic 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*/@Overridepublic void unlockStock(StockLockedTo to) {//获取库存详细工作单类StockDetailTo detail = to.getDetailTo();//库存详细工作单的idLong 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) {//调用真正接库存的方法unLockStockunLockStock(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*/@TableIdprivate 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*/@TableIdprivate 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_skuSET stock_locked = stock_locked - #{num}WHEREsku_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)@Overridepublic 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());}}
相关文章:

订单服务-----遇到的问题及解决方案
订单服务的问题及解决方案问题1:Feign远程调用时丢失请求头编辑出现这个Feign远程调用时丢失请求头的问题是因为Feign在远程调用的时候会创建一个新的请求,但是这个新的请求里啥都没有,没有cookie值,而这个cookie值里有成功登录后…...

项目经理如何度量项目?及项目度量指标实例【静说】
度量项目是项目经理的一个重要职责,通过度量项目,项目经理可以了解项目的进展情况,及时发现问题并采取相应的措施,以确保项目能够按时、按质、按预算完成。 分享给大家一些常见的项目度量指标: 1. 项目进度ÿ…...

我们应该如何优雅的处理 React 中受控与非受控
引言 大家好,我是19组清风。有段时间没有和大家见面了,最近因为有一些比较重要的事情(陪女朋友和换了新公司)在忙碌所以销声匿迹了一小段时间, 后续会陆陆续续补充之前构建 & 编译系列中缺失的部分,提…...

力扣热题100Day06:20. 有效的括号,21. 合并两个有序链表,22. 括号生成
20. 有效的括号 题目链接:20. 有效的括号 - 力扣(Leetcode) 思路:使用栈 (1)遇到左括号就将其对应的右括号压入到栈中 (2)如果遇到右括号 a. 如果弹出的元素与当前不等ÿ…...

【Yolov5】保姆级别源码讲解之-推理部分detect.py文件
推理部分之detect.py文件讲解1.下载Yolov5的源码2. 主函数讲解3.文件标头的注释4. main函数的5. run函数5.1 第一块参数部分5.2第二块,传入数据预处理5.3 第三块创建文件夹5.4 第四块 加载模型的权重5.5 第五块 Dataloader 加载模块5.6 第六块 推理部分 Run inferen…...

无重叠区间-力扣435-java贪心策略
一、题目描述给定一个区间的集合 intervals ,其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。示例 1:输入: intervals [[1,2],[2,3],[3,4],[1,3]]输出: 1解释: 移除 [1,3] 后,剩下的区间没有重叠。…...

Python使用VTK对容积超声图像进行体绘制(三维重建)
目录VTK简介什么是体绘制?体绘制效果图流程CodeQ&AReferenceVTK简介 VTK(Visualization Toolkit)是一个用于3D计算机图形学、图像处理和可视化的开源软件包。它包括一组C类和工具,可以让用户创建和处理复杂的3D图形和数据可视…...

JAVA设计模式之工厂模式讲解
目录 前言 开始表演 前言 Java中使用工厂模式的主要原因是为了实现代码的灵活性和可维护性。工厂模式是一种创建型设计模式,它提供了一种将对象的创建和使用进行分离的方式。具体来说,工厂模式可以将对象的创建过程封装在一个独立的工厂类中ÿ…...

近万字概述L3及以上自动驾驶故障运行和故障安全机制
本文描述了对ADS的FO和FS机制的评估方法。当系统不能按预期运行时,ADS将使用FO和FS机制。这些机制使ADS能够在最大程度上达到使车辆及其乘员脱离危险的MRC。定义、测试和验证实现MRC的FO和FS策略是确保ADS安全运行和部署的重要步骤。 MRC在SAE J3016中被定义为: 用户或ADS在…...

kafka入门到精通
文章目录一、kafka概述?1.定义1.2消息队列1.2.1 传统消息队列的使用场景1.2.2 消息队列好处1.2.3 消息队列两种模式1.3 kafka基础架构二、kafka快速入门1.1使用docker-compose安装kafka1.2测试访问kafka-manager1.3 查看kafka版本号1.4 查看zookeeper版本号1.5 扩展…...

es-09模糊查询
模糊查询 前缀搜索:prefix 概念:以xx开头的搜索,不计算相关度评分。 注意: 前缀搜索匹配的是term,而不是field。前缀搜索的性能很差前缀搜索没有缓存前缀搜索尽可能把前缀长度设置的更长 语法: GET <ind…...

57 - 深入解析任务调度
---- 整理自狄泰软件唐佐林老师课程 文章目录1. 问题1.1 思考1.2 实例分析:问题分析及解决2. 深入讨论2.1 任务调度的定义2.2 关于调度算法的分类2.3 什么时候进行任务调度2.4 任务的分类2.5 关于优先级调度2.6 问题2.7 调度算法的终极目标2.8 课后扩展1. 问题 系统…...

CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设
CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设 苏勇,2023年2月 文章目录CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设引言硬件外设模块系统概要总线接口单元 - 寄存器清单数据结构 - 消息缓冲区MB初始化过…...

Elasticsearch7.8.0版本进阶——段合并
目录一、段的概述1.1、段的概念1.2、段的缺点1.3、如何解决段数量暴增问题二、段合并的流程三、段合并的注意事项一、段的概述 1.1、段的概念 每一 段 本身都是一个倒排索引。 1.2、段的缺点 由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量…...

Java版贪食蛇游戏
技术:Java等摘要:近年来Java作为一种新的编程语言,以其简单性、可移植性和平台无关性等优点,得到了广泛地应用,特别是Java与万维网的完美结合,使其成为网络编程和嵌入式编程领域的首选编程语言。MyEclipse是…...

2023年度数学建模竞赛汇总
本人7年数学建模竞赛经验,历史获奖率百分之百。团队成员都是拿过全国一等奖的硕博,有需要数模竞赛帮助的可以私信我。 下面主要列几年一些比较有含金量的数学建模竞赛(按比赛时间顺序) 1. 美国大学生数学建模竞赛 报名时间&…...

了解Python语言和版本
1.1 任务1了解Python语言和版本 Python 语言的名字来自于一个著名的电视剧"Monty Pythons Flying Cireus",Python之父 Guido van Rossum是这部电视剧的狂热爱好者,所以把他设计的语言命名为Python。 Python 是一门跨平台、开源、免费的解释型高级动态编…...

nvm (node版本管理工具)安装的详细步骤,并解决安装过程中遇到的问题
1、下载NVM,跳转下载链接后,如下图,下载红框后解压文件 2、安装 注意:双击安装之后,会有两个地址选择, 1、地址中不能存在空格 2、不要放在C盘中,后面需要改个设置文件,安装到C盘的…...

朴素贝叶斯笔记
贝叶斯公式在A 条件成立下,B的概率等于B的概率*在B条件成立下,A的概率/A的概率,推导假设一个学校中男生占总数的60%,女生占总数的40%。并且男生总是穿长裤,女生则一半穿长裤、一半穿裙子。1.正向概率。随机选取一个学生…...

【GUI】用于电动助力车性能分析的GUI(Matlab代码实现)
👨🎓个人主页:研学社的博客💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密…...

Android:反编译apk踩坑/apktool/dex2jar/JDGUI
需求描述 想要反编译apk文件,搜到了这篇博客:Android APK反编译就这么简单 详解(附图),非常有参考价值~但其中的工具下载链接都已404,而本杂鱼实际操作的过程中也出现了亿点点点点点点的问题,于…...

React 跨域的配置
1、为什么会出现跨域? 浏览器遵循同源政策(同源策略三要素:协议相同、域名相同、端口相同) 2、配置跨域代理 使用中间件 http-proxy-middleware(安装依赖) npm install http-proxy-middleware 创建setupP…...

Elasticsearch7.8.0版本进阶——持久化变更
目录一、持久化变更的概述二、事务日志(translog)三、持久化变更完整流程四、事务日志(translog)的作用五、事务日志(translog)的目的一、持久化变更的概述 没有用 fsync 把数据从文件系统缓存刷ÿ…...

CF Edu 127 A-E vp补题
CF Edu 127 A-D vp补题 继续每日一vp,今天晚上有课,时间不太多,回去就直接vp。前三题比较简单,过了之后排名rk2000,然后就去洗澡了。d题没怎么认真思考,其实也可做。最后rk4000。发挥还行,b题罚…...

剑指 Offer 05. 替换空格
摘要 剑指 Offer 05. 替换空格 一、字符替换 由于每次替换从1个字符变成3个字符,使用字符数组可方便地进行替换。建立字符数组地长度为 s 的长度的3倍,这样可保证字符数组可以容纳所有替换后的字符。 获得 s 的长度 length创建字符数组 array&#x…...

通过操作Cortex-A7核,串口输入相应的命令,控制LED灯进行工作
1.通过操作Cortex-A7核,串口输入相应的命令,控制LED灯进行工作 例如在串口输入led1on,开饭led1灯点亮 2.例如在串口输入led1off,开饭led1灯熄灭 3.例如在串口输入led2on,开饭led2灯点亮 4.例如在串口输入led2off,开饭led2灯熄灭 5.例如在串口输入led…...

Python实现某du文库vip内容下载,保存成PDF
前言 是谁,是谁在网页上搜索往年考试卷题答案的时候只能阅读前两页的选择题,是谁在搜几千字的文档资料只能看25%,是谁在百度文库找七找八的时候所有的东西都要付费才能继续看… 我先说 是我自己 我又不经常用,只有偶尔需要看看…...

vue3.0 模板语法
文章目录前言:1. 内容渲染指令1.1 v-text1.2 {{ }}插值表达式1.3 v-html2. 双向绑定指令2.1 v-model2.2 v-model的修饰符3. 属性绑定指令3.1 动态绑定多个属性值3.2 绑定class和style属性4.条件渲染指令4.1 v-if、v-else-if、v-else4.2 v-show4.3 v-if与v-show的区别…...

【GlobalMapper精品教程】054:标签(标注)功能案例详解
同ArcGIS标注一样,globalmapper提供了动态标注的功能,称为标签,本文详解标签的使用方法。 文章目录 一、标签配置二、创建标签图层三、标签图层选项1. 标签字段2. 标签样式3. 标签格式4. 标签语言5. 标签优先级一、标签配置 在配置页面的【矢量显示】→标签选项卡下,有标签…...

超详细树状数组讲解(+例题:动态求连续区间和)
树状数组的作用:快速的对数列的一段范围求和快速的修改数列的某一个数为什么要使用树状数组:大家从作用中看到快速求和的时候可能会想到为什么不使用前缀和只需要预处理一下就可以在O(1)的时间复杂度下实行对于数列的一段范围的和但是我们可以得到当我们…...