【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【23】【订单服务】


持续学习&持续更新中…

守破离


【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【23】【订单服务】

  • 订单中心
  • 订单信息
    • 用户信息
    • 订单基础信息
    • 商品信息
    • 优惠信息
    • 支付信息
    • 物流信息
  • 订单状态
  • 订单流程
    • 订单创建与支付
    • 逆向流程
  • 订单确认页
  • Feign远程调用丢失请求头问题
  • Feign异步情况丢失上下文问题
  • 下订单
  • 关订单
  • 解锁库存
  • 收单
  • 加密-对称加密—不安全
  • 加密-非对称加密
  • 参考

订单中心

电商系统涉及到 3 流,分别是信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。

订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

在这里插入图片描述

订单信息

用户信息

用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成

用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。

用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等

订单基础信息

订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。

  • 订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
  • 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订 单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候, 父子订单就是为后期做拆单准备的。
  • 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候 可以对订单编号的每个字段进行统一定义和诠释。
  • 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
  • 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等

商品信息

商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。

优惠信息

优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券 篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。

为什么把优惠信息单独拿出来而不放在支付信息里面呢?

因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

支付信息

  • 支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支 付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
  • 支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。 支付方式有时候可能有两个——余额支付+第三方支付。
  • 商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促 销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等 之和;实付金额,用户实际需要付款的金额。
  • 用户实付金额=商品总金额+运费-优惠总金额

物流信息

物流信息包括配送方式,物流公司,物流单号,物流状态

物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

订单状态

  1. 待付款
    用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超 时后将自动取消订单,订单变更关闭状态。

  2. 已付款/待发货
    用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS 系统,仓库进行调拨,配货,分拣,出库等操作。

  3. 待收货/已发货
    仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物 品物流状态

  4. 已完成
    用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态 5. 售后中
    用户在付款后申请退款,或商家发货后用户申请退换货。
    售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待 商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后 订单 状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

  5. 已取消
    付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。

订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。

不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。

不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程

正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。 而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图:

在这里插入图片描述

订单创建与支付

  1. 订单创建前需要预览订单,选择收货信息等
  2. 订单创建需要锁定库存,库存有才可创建,否则不能创建
  3. 订单创建后超时未支付需要解锁库存
  4. 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
  5. 支付的每笔流水都需要记录,以待查账
  6. 订单创建,支付成功等状态都需要给 MQ 发送消息,方便其他系统感知订阅

逆向流程

  1. 修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息, 优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
  2. 订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订 单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的限时处理,尤其是在前面说的下单减库存的情形下面,可以保证快速的释放库存。 另外需要需要处理的是促销优惠中使用的优惠券,权益等视平台规则,进行相应补回给用户。
  3. 退款,在待发货订单状态下取消订单时,分为缺货退款和用户申请退款。如果是 全部退款则订单更新为关闭状态,若只是做部分退款则订单仍需进行进行,同时生 成一条退款的售后订单,走退款流程。退款金额需原路返回用户的账户。
  4. 发货后的退款,发生在仓储货物配送,在配送过程中商品遗失,用户拒收,用户 收货后对商品不满意,这样情况下用户发起退款的售后诉求后,需要商户进行退款 的审核,双方达成一致后,系统更新退款状态,对订单进行退款操作,金额原路返 回用户的账户,同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果 发生双方协调不一致情况下,可以申请平台客服介入。在退款订单商户不处理的情 况下,系统需要做限期判断,比如 5 天商户不处理,退款单自动变更同意退款。

订单确认页

在这里插入图片描述

Feign远程调用丢失请求头问题

用户访问订单确认页面会来到OrderWebController的toTrade方法,在这之前,我们通过LoginUserInterceptor对用户请求进行拦截,判断用户是否登录,如果用户登陆了会把登录的用户信息放到ThreadLocal中,方便之后的service等使用:

@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//登录了就放行MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if (attribute != null) {loginUser.set(attribute);return true;}//没登录就去登录request.getSession().setAttribute("msg", "请先进行登录");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {@AutowiredLoginUserInterceptor interceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(interceptor).addPathPatterns("/**");}}

通过 LoginUserInterceptor 拦截器后,来到OrderWebController的toTrade方法,我们会通过orderService.confirmOrder();去获取用户的确认订单信息,我们还得通过Feign的远程调用去获取一些信息,代码如下:

    @Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {OrderConfirmVo confirmVo = new OrderConfirmVo();final MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();//1、远程查询所有的收货地址列表List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());confirmVo.setAddress(address);//2、远程查询购物车所有选中的购物项List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();confirmVo.setItems(items);//3、查询用户积分Integer integration = memberRespVo.getIntegration();confirmVo.setIntegration(integration);//4、其他数据自动计算CompletableFuture.allOf(getAddressFuture, cartFuture).get();return confirmVo;}

但是我们会发现cartFeignService.getCurrentUserCartItems()这句代码会返回空结果。这就是因为出现了Feign远程调用丢失请求头问题:

在这里插入图片描述

Feign在远程调用之前会构造新请求,在构造请求过程中,会调用很多类型为RequestInterceptor的拦截器

在这里插入图片描述

那么我们就可以自定义拦截器,让在Feign构造新请求的时候,通过拦截器让它带上之前请求的请求头信息,就可以解决此问题:

在这里插入图片描述

@Configuration
public class GuliFeignConfig {@Bean("requestInterceptor")public RequestInterceptor requestInterceptor() {return template -> {// 通过RequestContextHolder拿到刚进来的这个请求// 通过RequestContextHolder获取到的RequestAttributes是Spring自动设置的ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest(); //老请求//同步请求头数据,CookieString cookie = request.getHeader("Cookie");template.header("Cookie", cookie); //给新请求同步老请求的header头信息,比如Cookie信息}};}}

Feign异步情况丢失上下文问题

我们发现通过orderService.confirmOrder();去获取用户的确认订单信息,会调用两个Feign的远程请求,这种情况下,为了提高该接口的响应速度,执行效率,提升性能等,我们应该使用异步编排的方式,让两个Feign远程请求同时执行,加快速度。但如果直接开启异步任务又会有新的问题出现:

我们之前会通过RequestContextHolder拿到刚进来的请求 :ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ,然后将其设置给每个Feign创建的新请求,这样通过Feign发出的远程调用请求就可以带上用户通过浏览器发送过来的请求头数据,解决了Feign远程调用丢失请求头这个问题

但是RequestContextHolder 内部是通过 ThreadLocal 共享数据的

在这里插入图片描述

以前同步调用这两个Feign的远程请求是这样工作的:

在这里插入图片描述

发送Feign请求,Feign在构建新请求时会先来到 RequestInterceptor 拦截器,我们在拦截器中会获取并使用 RequestAttributes,由于是同步调用也就是说大家都是同一个线程(Tomcat进来使用同一条线程执行我们的Controller/Service等),使用RequestContextHolder.getRequestAttributes()获取数据时当然可以获取到。

然而直接开启异步任务发送Feign请求,Feign来到 RequestInterceptor 拦截器获取 RequestAttributes 时,由于是不同的线程,当然获取不到之前线程的RequestAttributes对象,也就无法使用了。

所以,开启异步调用Feign时,为了可以获取到之前的请求信息,我们可以这样写:

    @Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {OrderConfirmVo confirmVo = new OrderConfirmVo();final MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();//        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
//        confirmVo.setAddress(address);
////feign在远程调用之前要构造请求,调用很多的拦截器//RequestInterceptor interceptor : requestInterceptors
//        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
//        confirmVo.setItems(items);System.out.println("主线程...." + Thread.currentThread().getId());//获取之前的请求final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {//每一个线程都来共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);//1、远程查询所有的收货地址列表System.out.println("member线程...." + Thread.currentThread().getId());List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());confirmVo.setAddress(address);}, executor);CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {//每一个线程都来共享之前的请求数据RequestContextHolder.setRequestAttributes(requestAttributes);//2、远程查询购物车所有选中的购物项System.out.println("cart线程...." + Thread.currentThread().getId());List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();confirmVo.setItems(items);}, executor);//3、查询用户积分Integer integration = memberRespVo.getIntegration();confirmVo.setIntegration(integration);//4、其他数据自动计算CompletableFuture.allOf(getAddressFuture, cartFuture).get();return confirmVo;}

通过RequestContextHolder.setRequestAttributes(requestAttributes);给每个线程的ThreadLocal都设置上requestAttributes,这样,当我们开启异步任务调用Feign发送请求,Feign在构建请求的时候,来到拦截器,由于我们已经给当前线程的ThreadLocal设置过requestAttributes了,那么我们就可以正常获取到RequestAttributes并使用了。

下订单

在这里插入图片描述

    //本地事务,在分布式系统下,只能控制住自己的回滚,控制不了其他服务的回滚//应该使用分布式事务,但是分布式事务比较复杂,比较复杂的最大原因:网络问题+分布式机器。
//    @GlobalTransactional //    高并发场景,Seata的AT模式不适合@Transactional@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {confirmVoThreadLocal.set(vo);SubmitOrderResponseVo response = new SubmitOrderResponseVo();MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();response.setCode(0);//验证令牌【令牌的获取对比和删除必须保证原子性】//0令牌失败 - 1删除成功
//        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
//        if(orderToken!=null && orderToken.equals(redisToken)){
//            //令牌验证通过
//            redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
//        }else{
//            //不通过
//        }//原子验证令牌和删除令牌【处理接口幂等性】String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";String orderToken = vo.getOrderToken();Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);if (result == 0L) {//令牌验证失败response.setCode(1);return response;} else {//令牌验证成功  //下单:去创建订单,验令牌,验价格,锁库存...//1、创建订单,订单项等信息OrderCreateTo order = createOrder();//2、验价if (Math.abs(order.getOrder().getPayAmount().subtract(vo.getPayPrice()).doubleValue()) < 0.01) { //金额对比// 3、保存订单saveOrder(order);//4、库存锁定。只要有异常回滚订单数据。// 库存锁定需要的数据:订单号,所有订单项(skuId,skuName,num)//4、远程锁库存//TODO 问题1:库存调用成功了,但是网络原因,或者其他原因,导致Feign调用超时了,出现异常,此时:订单回滚,库存不会回滚。R r = wareFeignService.orderLockStock(getLockVo(order));// TODO 为了保证高并发 不使用seata,使用消息队列// 方式1、在这儿可以发消息给库存服务让库存服务回滚// 方式2、库存服务本身也可以使用自动解锁模式(使用延时队列实现定时任务)if (r.getCode() == 0) {//锁成功了response.setOrder(order.getOrder());//TODO 问题2:假如还有个远程扣减积分服务// 该服务出异常 :订单会回滚;由于库存服务已经成功的远程执行,不会回滚。int i = 10/0; //模拟扣减积分出异常//TODO 清除购物车已经下单的商品return response;} else {//锁定失败response.setCode(3);String msg = (String) r.get("msg");throw new NoStockException(msg);}} else {response.setCode(2);return response;}}}

关订单

在这里插入图片描述

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashMap;
import java.util.Map;@Configuration
public class MyMQConfig {//@Bean Binding,Queue,Exchange/*** 容器中的 Binding,Queue,Exchange 都会自动创建(RabbitMQ没有的情况)* RabbitMQ中已有的话 @Bean中声明属性发生了变化也不会覆盖*/@Beanpublic Queue orderDelayQueue() {Map<String,Object> arguments = new HashMap<>();/*** x-dead-letter-exchange: order-event-exchange* x-dead-letter-routing-key: order.release.order* x-message-ttl: 60000*/arguments.put("x-dead-letter-exchange","order-event-exchange");arguments.put("x-dead-letter-routing-key","order.release.order");arguments.put("x-message-ttl",60000); //测试期间1分钟//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsreturn new Queue("order.delay.queue", true, false, false,arguments);}@Beanpublic Queue orderReleaseOrderQueue() {return new Queue("order.release.order.queue", true, false, false);}@Beanpublic Exchange orderEventExchange() {//String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsreturn new TopicExchange("order-event-exchange",true,false);}@Beanpublic Binding orderCreateOrderBinding() {//String destination, DestinationType destinationType, String exchange, String routingKey,//			Map<String, Object> argumentsreturn new Binding("order.delay.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.create.order",null);}@Beanpublic Binding orderReleaseOrderBinding() {return new Binding("order.release.order.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.release.order",null);}
}
    @Transactional@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {//验证令牌【令牌的获取对比和删除必须保证原子性】if (result == 0L) {//令牌验证失败xxx} else {//令牌验证成功  //下单:去创建订单,验令牌,验价格,锁库存...//1、创建订单,订单项等信息OrderCreateTo order = createOrder();//2、验价if (Math.abs(order.getOrder().getPayAmount().subtract(vo.getPayPrice()).doubleValue()) < 0.01) { //金额对比// 3、保存订单saveOrder(order);// 4、锁定库存R r = wareFeignService.orderLockStock(getLockVo(order));if (r.getCode() == 0) {// 锁成功了// 订单创建成功发送消息给MQrabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());xxx} else {//锁定失败xxx}} else {xxx}}}
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {@AutowiredOrderService orderService;@RabbitHandlerpublic void listener(OrderEntity entity, Channel channel, Message message) throws IOException {System.out.println("收到过期的订单信息:准备关闭订单"+entity.getOrderSn()+"==>"+entity.getId());try{orderService.closeOrder(entity);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch (Exception e){channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}
}
    @Overridepublic void closeOrder(OrderEntity entity) {//查询当前这个订单的最新状态OrderEntity orderEntity = this.getById(entity.getId());if (Objects.equals(orderEntity.getStatus(), OrderStatusEnum.CREATE_NEW.getCode())) {//关单OrderEntity update = new OrderEntity();update.setId(entity.getId());update.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(update);}}

我们害怕出现如下图所示问题,所以我们关闭订单后,应该主动发一个消息order.release.other,让解锁库存服务去解锁库存

在这里插入图片描述

    /*** 订单释放直接和库存释放进行绑定*/@Beanpublic Binding orderReleaseOtherBinding() {return new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"order-event-exchange","order.release.other.#",null);}
    @Overridepublic void closeOrder(OrderEntity entity) {//查询当前这个订单的最新状态OrderEntity orderEntity = this.getById(entity.getId());if (Objects.equals(orderEntity.getStatus(), OrderStatusEnum.CREATE_NEW.getCode())) {//关单OrderEntity update = new OrderEntity();update.setId(entity.getId());update.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(update);OrderTo orderTo = new OrderTo();BeanUtils.copyProperties(orderEntity, orderTo);// 主动发一个消息`order.release.other`,让解锁库存服务去解锁库存rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);}}

解锁库存

用户下单成功后先锁定库存,锁库存图示逻辑:

在这里插入图片描述

库存锁定成功,如果订单回滚,为了保证最终一致性,需要库存自动解锁

  • 库存调用成功了,但是网络原因,或者其他原因,导致Feign调用超时了,此时:订单回滚,库存不会回滚。

  • 假如还有个远程扣减积分服务是在订单服务调用成功后调用的,该服务出异常 :订单会回滚;由于库存服务已经成功的远程执行,不会回滚。

在这里插入图片描述

为了保证高并发不使用seata,使用定时任务让库存服务本身自动解锁

在这里插入图片描述

但由于定时任务(比如Spring的 schedule 定时任务轮询数据库):消耗系统内存、增加了数据库的压力、存在较大的时间误差;

在这里插入图片描述

所以使用RabbitMQ的延时队列,使用延时队列,为了方便追溯,可以保存库存工作单的详情。

在这里插入图片描述

创建业务队列/路由器等:

在这里插入图片描述

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;
import java.util.Map;@Configuration
public class GulimallWareMQConfig {
//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void  handle(Message message){
//    }@Beanpublic Exchange stockEventExchange() {return new TopicExchange("stock-event-exchange", true, false);}@Beanpublic Queue stockReleaseStockQueue() {return new Queue("stock.release.stock.queue", true, false, false);}@Beanpublic Queue stockDelayQueue() {Map<String, Object> args = new HashMap<>();args.put("x-dead-letter-exchange", "stock-event-exchange");args.put("x-dead-letter-routing-key", "stock.release");args.put("x-message-ttl", 120000); //测试期间设置2分钟return new Queue("stock.delay.queue", true, false, false, args);}@Beanpublic Binding stockReleaseBinding() {return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.release.#", null);}@Beanpublic Binding stockLockedBinding() {return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.locked", null);}
}

库存解锁逻辑:

/*** 为某个订单锁定库存** @Transactional(rollbackFor = NoStockException.class)* 默认只要是运行时异常都会回滚** 库存解锁的场景* 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存* 2)、下订单成功,库存锁定成功,接下来的其他业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。*/@Transactional@Overridepublic Boolean orderLockStock(WareSkuLockVo vo) {/*** 保存库存工作单的详情。* 方便追溯。*/WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();taskEntity.setOrderSn(vo.getOrderSn());orderTaskService.save(taskEntity);//1、按照下单的收货地址,找到一个就近仓库,锁定库存。//1、找到每个商品在哪个仓库都有库存List<OrderItemVo> locks = vo.getLocks();List<SkuWareHasStock> collect = locks.stream().map(item -> {SkuWareHasStock stock = new SkuWareHasStock();Long skuId = item.getSkuId();stock.setSkuId(skuId);stock.setNum(item.getCount());//查询这个商品在哪里有库存List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);stock.setWareId(wareIds);return stock;}).collect(Collectors.toList());//2、锁定库存for (SkuWareHasStock hasStock : collect) {boolean skuStocked = false;Long skuId = hasStock.getSkuId();List<Long> wareIds = hasStock.getWareId();if (wareIds == null || wareIds.size() == 0) {//没有任何仓库有这个商品的库存throw new NoStockException(skuId);}//1、如果每一个商品都锁定成功,那么就已经将当前商品锁定了几件的工作单详情记录发给了MQ//2、有一个商品锁定失败,库存就会回滚。发送出去的消息,即使要解锁记录,去数据库查不到id,就不用解锁库存for (Long wareId : wareIds) {//成功就返回1,否则就是0Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());if (count == 1) {skuStocked = true;//告诉MQ库存锁定成功WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, null, hasStock.getNum(), taskEntity.getId(), wareId, 1);orderTaskDetailService.save(entity);StockLockedTo lockedTo = new StockLockedTo();lockedTo.setId(taskEntity.getId());StockDetailTo stockDetailTo = new StockDetailTo();BeanUtils.copyProperties(entity, stockDetailTo);//只发id不行,防止回滚以后找不到数据lockedTo.setDetail(stockDetailTo);
//                    rabbitTemplaterabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);break;} else {//当前仓库锁失败,重试下一个仓库}}if (skuStocked == false) {//当前商品所有仓库都没有锁住throw new NoStockException(skuId);}}//3、肯定全部都是锁定成功的return true;}
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {@AutowiredWareSkuService wareSkuService;@RabbitHandlerpublic void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {System.out.println("收到解锁库存的消息...");try{//当前消息是否被第二次及以后(重新)派发过来了。
//            Boolean redelivered = message.getMessageProperties().getRedelivered();wareSkuService.unlockStock(to);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch (Exception e){channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}@RabbitHandlerpublic void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {System.out.println("订单关闭准备解锁库存...");try{wareSkuService.unlockStock(orderTo);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}catch (Exception e){channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}
    @Overridepublic void unlockStock(StockLockedTo to) {StockDetailTo detail = to.getDetail();Long detailId = detail.getId();//解锁//查询数据库关于这个订单的锁定库存信息。//  有:证明库存锁定成功了//    解锁:订单情况。//          1、没有这个订单。必须解锁//          2、有这个订单。不是解锁库存。//                订单状态: 已取消:解锁库存//                          没取消:不能解锁//  没有:库存锁定失败了,库存回滚了。这种情况无需解锁WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);if (byId != null) {Long id = to.getId();WareOrderTaskEntity taskEntity = orderTaskService.getById(id);String orderSn = taskEntity.getOrderSn();//根据订单号查询订单的状态R r = orderFeignService.getOrderStatus(orderSn);if (r.getCode() == 0) {//订单数据返回成功OrderVo data = r.getData(new TypeReference<OrderVo>() {});if (data == null || data.getStatus() == 4) {//订单不存在//订单已经被取消了。才能解锁库存if (byId.getLockStatus() == 1) { //当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);}}} else {//消息拒绝以后重新放到队列里面,让别人继续消费解锁。throw new RuntimeException("远程服务失败");}} else {//无需解锁}}private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {//库存解锁wareSkuDao.unlockStock(skuId, wareId, num);//更新库存工作单的状态WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();entity.setId(taskDetailId);entity.setLockStatus(2);//变为已解锁orderTaskDetailService.updateById(entity);}
    //防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。//导致卡顿的订单,永远不能解锁库存@Transactional@Overridepublic void unlockStock(OrderTo orderTo) {String orderSn = orderTo.getOrderSn();//查一下最新库存的状态,防止重复解锁库存WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);Long id = task.getId();//按照工作单找到所有没有解锁的库存,进行解锁List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));for (WareOrderTaskDetailEntity entity : entities) {unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());}}

收单

  • 订单在支付页,不支付,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。
    • 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
  • 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
    • 订单解锁,手动调用收单
  • 网络阻塞问题,订单支付成功的异步通知一直不到达
    • 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态
  • 其他各种问题
    • 每天晚上闲时下载支付宝对账单,一一进行对账

加密-对称加密—不安全

加密解密使用同一把钥匙

在这里插入图片描述

加密-非对称加密

加密解密使用不同钥匙

除非你知道完整的4把钥匙,否则你就不能模拟完整的通信过程

在这里插入图片描述

参考

雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.


本文完,感谢您的关注支持!


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

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

相关文章

LabVIEW设备检修信息管理系统

开发了基于LabVIEW设计平台开发的设备检修信息管理系统。该系统应用于各种设备的检修基地&#xff0c;通过与基地管理信息系统的连接和数据交换&#xff0c;实现了本地检修工位数据的远程自动化管理&#xff0c;提高了设备的检修效率和安全性。 项目背景 现代设备运维过程中信…

Microsoft Edge(简称Edge)

Microsoft Edge&#xff08;简称Edge&#xff09;是一款由微软开发的网页浏览器&#xff0c;它为用户提供了许多便捷的功能和选项。以下是Edge浏览器的使用方法&#xff1a; 一、基本使用方法 打开Edge浏览器&#xff1a; 可以在Windows的开始菜单中找到“Microsoft Edge”并点…

MySQL 进阶(四)【锁】

1、锁 1.1、锁的概述 锁就不需要多介绍了&#xff0c;多个用户访问共享数据资源&#xff0c;如何保证数据并发访问的一致性、有效性是数据库最重要的问题。同时&#xff0c;锁冲突也是影响一个数据库并发性能最重要的因素。 MySQL 中锁的划分有三类&#xff1a; 全局锁&…

2024-07-12升级问题:Android SDK升级导致 Canvas.FULL_COLOR_LAYER_SAVE_FLAG 等标志位无法使用

Canvas.FULL_COLOR_LAYER_SAVE_FLAG 是一个标志位&#xff0c;用于在 Android 的 Canvas 类中保存画布的颜色层。当使用 saveLayer() 方法时&#xff0c;可以传递这个标志位来指示保存整个颜色层。这样&#xff0c;在恢复画布状态时&#xff0c;颜色层也会被恢复。 工程从Andr…

如何通过网络快速搜寻到自己的STM32设备

目录 一、问题概述 二、解决思路 三、代码实现 1.创建任务 2.UDP广播接收 一、问题概述 以前一直用RS232串口修改设备配置信息&#xff0c;但是现场施工人员的232线太细&#xff0c;经常容易断掉&#xff0c;这次准备用网口去修改&#xff0c;遇到了一个问题&#xff0c;…

C语言学习笔记[24]:循环语句while②

getchar()的使用场景 int main() {char password[20] {0};printf("请输入密码&#xff1a;");//输入 123456 后回车scanf("%s", passwoed);//数组名本身就是数组地址printf("请确认密码&#xff1a;Y/N");int ch getchar();if(Y ch)printf(&…

区块链学习05-web3中solidity和move语言

Solidity 和 Move 语言的比较&#xff1a;Web3 开发中的两种选择 Solidity 和 Move 都是用于开发区块链平台智能合约的编程语言。它们具有一些相似之处&#xff0c;但也存在一些关键差异。 相似之处: Solidity 和 Move 都是图灵完备语言&#xff0c;这意味着它们可以表达计算…

JavaEE:Spring Web简单小项目实践二(用户登录实现)

学习目的&#xff1a; 1、理解前后端交互过程 2、学习接口传参&#xff0c;数据返回以及页面展示 1、准备工作 创建SpringBoot项目&#xff0c;引入Spring Web依赖&#xff0c;添加前端页面到项目中。 前端代码&#xff1a; login.html <!DOCTYPE html> <html lang&…

关于window配置gitlab和gitee平台共存

今天使用gitlab拉取代码突然提示 gitgitlab.xxx.com: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password). 以为是ssh公钥没有配置好&#xff0c;遂又进行了一番配置&#xff0c;实际上并不是这个问题造成的&#xff0c;但还是想记录一下步骤&#xff0c;以…

<Rust><GUI>rust语言GUI库tauri体验:前、后端结合创建一个窗口并修改其样式

前言 本文是rust语言下的GUI库&#xff1a;tauri来创建一个窗口的简单演示&#xff0c;主要说明一下&#xff0c;使用tauri这个库如何创建GUI以及如何添加部件、如何编写逻辑、如何修改风格等&#xff0c;所以&#xff0c;这也是一个专栏&#xff0c;将包括tauri库的多个方面。…

小阿轩yx-zookeeper+kafka群集

小阿轩yx-zookeeperkafka群集 消息队列(Message Queue) 是分布式系统中重要的组件 通用的使用场景可以简单地描述为 当不需要立即获得结果&#xff0c;但是并发量又需要进行控制的时候&#xff0c;差不多就是需要使用消息队列的时候。 消息队列 什么是消息队列 消息(Mes…

【HarmonyOS开发】弹窗交互(promptAction )

实现效果 点击按钮实现不同方式的弹窗showToast showDialog showActionMenu 代码实现 1.引入’ohos.promptAction’ import promptAction from ohos.promptAction;2.通过promptAction 实现系统既定的弹窗 import promptAction from ohos.promptAction;Entry Component st…

鸿蒙语言基础类库:【@system.geolocation (地理位置)】

地理位置 说明&#xff1a; 从API Version 7 开始&#xff0c;该接口不再维护&#xff0c;推荐使用新接口[ohos.geolocation]。本模块首批接口从API version 3开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import geolocation from …

C++客户端Qt开发——常用控件QWidget

四、常用控件 属性 作用 enabled 设置控件是否可使用.true 表⽰可用&#xff0c;false 表示禁用 geometry 位置和尺寸&#xff0c;包含x,y,width,height四个部分 其中坐标是以⽗元素为参考进行设置的. windowTitle 设置widget标题 windowIcon 设置widget图标 windowO…

【STM32 IDE】使用STM32CubeIDE创建一个工程

关于IDE的下载安装和环境配置这里暂且不介绍&#xff0c;我们直接使用STM32F407ZGT6创建工程。 这里需要注意两点&#xff1a; 创建工程时&#xff0c;默认使用最新版本的固件包&#xff08;HAL库&#xff09;&#xff0c;好像还不让更改。如果本地电脑位置没有该版本的包&…

注意力机制中三种掩码技术详解和Pytorch实现

注意力机制是许多最先进神经网络架构的基本组成部分&#xff0c;比如Transformer模型。注意力机制中的一个关键方面是掩码&#xff0c;它有助于控制信息流&#xff0c;并确保模型适当地处理序列。 在这篇文章中&#xff0c;我们将探索在注意力机制中使用的各种类型的掩码&…

【瑞吉外卖 | day07】移动端菜品展示、购物车、下单

文章目录 瑞吉外卖 — day71. 导入用户地址簿相关功能代码1.1 需求分析1.2 数据模型1.3 代码开发 2. 菜品展示2.1 需求分析2.2 代码开发 3. 购物车3.1 需求分析3.2 数据模型3.3 代码开发 4. 下单4.1 需求分析4.2 数据模型4.3 代码开发 瑞吉外卖 — day7 移动端相关业务功能 —…

MySQL 一行记录是怎么存储的

文章目录 1. 文件存放目录 && 组织2. 表空间文件的结构3. InnoDB 行格式4. Compact 行格式记录的额外信息1. 变长字段长度列表2. NULL 值列表3. 记录头信息 记录的真实数据1. 定义的表字段2. 三个隐藏字段 5. varchar(n) 中 n 最大取值为多少&#xff1f;6. 行溢出后&a…

pnpm install安装失败

ERR_PNPM_META_FETCH_FAIL GET https://registry.npmjs.org/commitlint%2Fcli: request to https://registry.npmjs.org/commitlint%2Fcli failed, reason: connect ETIMEDOUT 2606:4700::6810:123:443 1. 检查网络连接 确保你的网络连接正常并且没有被防火墙或代理服务器阻止…

高翔【自动驾驶与机器人中的SLAM技术】学习笔记(二)——带着问题的学习;一刷感受;环境搭建

按照作者在读者寄语中的说法&#xff1a;我们得榨干这本书的知识。 带着问题 为了更好的学习&#xff0c;我们最好带着问题去探索。 第一&#xff1a;核心问题与基础知识 如上图&#xff1a;这本书介绍了SLAM相关的核心问题和基础知识。王谷博士给我们做了梳理&#xff1a;…