当前位置: 首页 > news >正文

订单服务-----遇到的问题及解决方案

订单服务的问题及解决方案

问题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&#xff1a;Feign远程调用时丢失请求头编辑出现这个Feign远程调用时丢失请求头的问题是因为Feign在远程调用的时候会创建一个新的请求&#xff0c;但是这个新的请求里啥都没有&#xff0c;没有cookie值&#xff0c;而这个cookie值里有成功登录后…...

项目经理如何度量项目?及项目度量指标实例【静说】

度量项目是项目经理的一个重要职责&#xff0c;通过度量项目&#xff0c;项目经理可以了解项目的进展情况&#xff0c;及时发现问题并采取相应的措施&#xff0c;以确保项目能够按时、按质、按预算完成。 分享给大家一些常见的项目度量指标&#xff1a; 1. 项目进度&#xff…...

我们应该如何优雅的处理 React 中受控与非受控

引言 大家好&#xff0c;我是19组清风。有段时间没有和大家见面了&#xff0c;最近因为有一些比较重要的事情&#xff08;陪女朋友和换了新公司&#xff09;在忙碌所以销声匿迹了一小段时间&#xff0c; 后续会陆陆续续补充之前构建 & 编译系列中缺失的部分&#xff0c;提…...

力扣热题100Day06:20. 有效的括号,21. 合并两个有序链表,22. 括号生成

20. 有效的括号 题目链接&#xff1a;20. 有效的括号 - 力扣&#xff08;Leetcode&#xff09; 思路&#xff1a;使用栈 &#xff08;1&#xff09;遇到左括号就将其对应的右括号压入到栈中 &#xff08;2&#xff09;如果遇到右括号 a. 如果弹出的元素与当前不等&#xff…...

【Yolov5】保姆级别源码讲解之-推理部分detect.py文件

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

无重叠区间-力扣435-java贪心策略

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

Python使用VTK对容积超声图像进行体绘制(三维重建)

目录VTK简介什么是体绘制&#xff1f;体绘制效果图流程CodeQ&AReferenceVTK简介 VTK&#xff08;Visualization Toolkit&#xff09;是一个用于3D计算机图形学、图像处理和可视化的开源软件包。它包括一组C类和工具&#xff0c;可以让用户创建和处理复杂的3D图形和数据可视…...

JAVA设计模式之工厂模式讲解

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

近万字概述L3及以上自动驾驶故障运行和故障安全机制

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

kafka入门到精通

文章目录一、kafka概述&#xff1f;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模糊查询

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

57 - 深入解析任务调度

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

CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设

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

Elasticsearch7.8.0版本进阶——段合并

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

Java版贪食蛇游戏

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

2023年度数学建模竞赛汇总

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

了解Python语言和版本

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

nvm (node版本管理工具)安装的详细步骤,并解决安装过程中遇到的问题

1、下载NVM&#xff0c;跳转下载链接后&#xff0c;如下图&#xff0c;下载红框后解压文件 2、安装 注意&#xff1a;双击安装之后&#xff0c;会有两个地址选择&#xff0c; 1、地址中不能存在空格 2、不要放在C盘中&#xff0c;后面需要改个设置文件&#xff0c;安装到C盘的…...

朴素贝叶斯笔记

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

【GUI】用于电动助力车性能分析的GUI(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密…...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

Spring Boot 实现流式响应(兼容 2.7.x)

在实际开发中&#xff0c;我们可能会遇到一些流式数据处理的场景&#xff0c;比如接收来自上游接口的 Server-Sent Events&#xff08;SSE&#xff09; 或 流式 JSON 内容&#xff0c;并将其原样中转给前端页面或客户端。这种情况下&#xff0c;传统的 RestTemplate 缓存机制会…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

2023赣州旅游投资集团

单选题 1.“不登高山&#xff0c;不知天之高也&#xff1b;不临深溪&#xff0c;不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

短视频矩阵系统文案创作功能开发实践,定制化开发

在短视频行业迅猛发展的当下&#xff0c;企业和个人创作者为了扩大影响力、提升传播效果&#xff0c;纷纷采用短视频矩阵运营策略&#xff0c;同时管理多个平台、多个账号的内容发布。然而&#xff0c;频繁的文案创作需求让运营者疲于应对&#xff0c;如何高效产出高质量文案成…...

Yolov8 目标检测蒸馏学习记录

yolov8系列模型蒸馏基本流程&#xff0c;代码下载&#xff1a;这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中&#xff0c;**知识蒸馏&#xff08;Knowledge Distillation&#xff09;**被广泛应用&#xff0c;作为提升模型…...