谷粒商城-分布式高级篇[商城业务-订单服务]

2023-11-05

  1. 谷粒商城-分布式基础篇【环境准备】
  2. 谷粒商城-分布式基础【业务编写】
  3. 谷粒商城-分布式高级篇【业务编写】持续更新
  4. 谷粒商城-分布式高级篇-ElasticSearch
  5. 谷粒商城-分布式高级篇-分布式锁与缓存
  6. 项目托管于gitee

一、页面环境搭建


1、配置动静环境


在服务器的mydata/nginx/html/static 路径下创建一个 order 文件夹,在order路径下分别创建以下几个文件夹,用来存放对应的静态资源

  • detail 文件夹下存放 等待付款的静态资源,
    并将等待付款文件夹下的页面复制到 gulimall-order服务中并命名为 detail.html

    href="		==>		href="/static/order/detail/
    src="			==>		src="/static/order/detail/
    
  • list 文件夹下存放 订单页的静态资源,并将订单页文件夹下的页面复制到 gulimall-order服务中并命名为 list.html

    href="		==>		href="/static/order/list/
    src="			==>		src="/static/order/list/
    
  • confirm 文件夹下存放 结算页的静态资源,并将结算页文件夹下的页面复制到 gulimall-order服务中并命名为 confirm.html

    src="			==>			src="/static/order/confirm/
    href="		==>			href="/static/order/confirm/
    
  • pay 文件夹下存放 收银页的静态资源,并将收银页文件夹下的页面复制到 gulimall-order服务中并命名为 pay.html

    href="		==>		href="/static/order/pay/
    src="			==>		src="/static/order/pay/
    

2、网关路由配置


  1. 修改文件,添加新的域名 vim /etc/hosts
# Gulimall Host Start
127.0.0.1 gulimall.cn
127.0.0.1 search.gulimall.cn
127.0.0.1 item.gulimall.cn
127.0.0.1 auth.gulimall.cn
127.0.0.1 cart.gulimall.cn
127.0.0.1 order.gulimall.cn
# Gulimall Host End
  1. 配置网关路由 gulimall-gateway
- id: gulimall_order_route
  uri: lb://gulimall-order
  predicates:
    - Host=order.gulimall.cn

3、配置加入注册中心Nacos

  1. 已导入依赖

  2. 主启动类加上 @EnableDiscoveryClient 注解

    @EnableDiscoveryClient
    @EnableRabbit
    @SpringBootApplication
    public class GulimallOrderApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(GulimallOrderApplication.class, args);
        }
    }
    
  3. 配置注册中心信息

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-order

4、页面渲染

  1. 导入 thymeleaf的依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  1. 配置清除缓存
spring:
  thymeleaf:
    cache: false


二、整合SpringSession

**第一步、**导入依赖

<!--属性配置的提示工具-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<!-- 整合SpringSession完成Session共享问题-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--引入Redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

第二步、修改配置

spring:
  #SpringSession的存储类型
  session:
    store-type: redis
  #reidis地址
  redis:
    host: 124.222.223.222

# 配置线程池
gulimall:
  thread:
    core-size: 20
    max-size: 200
    keep-alive-time: 10

主启动类是上添加SpingSession自动启动的注解

@EnableRedisHttpSession
@EnableDiscoveryClient
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {

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

}

第三步、导入SpringSession、线程池配置类

  1. 添加SpringSession的配置,添加“com.atguigu.gulimall.order.config.GulimallSessionConfig”类,代码如下

    @Configuration
    public class GulimallSessionConfig {
    
        @Bean
        public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setDomainName("gulimall.cn");
            cookieSerializer.setCookieName("GULISESSION");
    
            return cookieSerializer;
        }
    
        @Bean
        public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
            return new GenericJackson2JsonRedisSerializer();
        }
    }
    
  2. 添加线程池的配置,添加“com.atguigu.gulimall.order.config.MyThreadConfig”类,代码如下

    @Configuration
    public class MyThreadConfig {
    
        @Bean
        public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
            return new ThreadPoolExecutor(pool.getCoreSize(),
                    pool.getMaxSize(),
                    pool.getKeepAliveTime(),
                    TimeUnit.SECONDS,
                    new LinkedBlockingDeque<>(100000),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.AbortPolicy());
        }
    }
    
  3. 线程池配置需要的属性

    添加“com.atguigu.gulimall.order.config.ThreadPoolConfigProperties”类,代码如下

    @ConfigurationProperties(prefix = "gulimall.thread")
    @Component
    @Data
    public class ThreadPoolConfigProperties {
        private Integer coreSize;
        private Integer maxSize;
        private Integer keepAliveTime;
    }
    

第四步、页面调整

  1. 修改商城首页、商品页我的订单地链接地址

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

  1. 获取用户信息

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



三、订单基本概念


1、订单中心

1.1、订单构成

在这里插入图片描述

1.2、订单状态

  1. 代付款
  2. 已付款/待发货
  3. 待收货/已发货
  4. 已完成
  5. 已取消
  6. 售后中

2、订单流程


订单生成 -> 支付订单 -> 卖家发货 -> 确认收货 -> 交易成功

在这里插入图片描述



四、订单登录拦截


需求:去结算、查看订单必须是登录用户之后的,这里编写一个拦截器。

  • 用户登录 放行
  • 用户未登录:跳转到登录页面

1)、修改cartList.html 页面的**“去结算”**的链接地址

在这里插入图片描述

2)、编写Controller 层

Gulimall-order服务中com.atguigu.gulimall.order.web 路径下

package com.atguigu.gulimall.order.web;
@Controller
public class OrderWebController {

    @GetMapping("/toTrade")
    public String toTrade(){
        return "confirm";
    }
}

3)、编写拦截器

Gulimall-order服务中com.atguigu.gulimall.order.interceptoe 路径下

package com.atguigu.gulimall.order.interceptoe;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    /**
     * 用户登录拦截器
     * @param request
     * @param response
     * @param handler
     * @return 
     *      用户登录:放行
     *      用户未登录:跳转到登录页面
     * @throws Exception
     */
    @Override
    public 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;
        } else {
            // 没登录就去登录
            request.getSession().setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.cn/login.html");
            return false;
        }
    }
}

4)、添加拦截器的配置

Gulimall-order服务中com.atguigu.gulimall.order.config 路径下

package com.atguigu.gulimall.order.config;

import com.atguigu.gulimall.order.interceptoe.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Data time:2022/4/11 22:21
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {

    @Autowired
    LoginUserInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

5)、修改gulimall-auth-server的login.html页面接收提醒信息

在这里插入图片描述



五、订单确认页


1、订单确认页的模型抽取

订单确认页需要用的数据

  • 因为存在网路延迟等问题,若一直点下单会下许多。所以我们需要防重令牌

gulimall-order 服务中 com.atguigu.gulimall.order.vo 路径下 VO类:

/**
 * Data time:2022/4/12 09:31
 * StudentID:2019112118
 * Author:hgw
 * Description: 订单确认页需要用的数据
 */
public class OrderConfirmVo {

    /**
     * 收货地址,ums_member_receive_address 表
     */
    @Setter@Getter
    List<MemberAddressVo> addressVos;

    /**
     * 所有选中的购物车项
     */
    @Setter@Getter
    List<OrderItemVo> items;

    // 发票记录。。。

    /**
     * 优惠券信息
     */
    @Setter@Getter
    Integer integration;
    /**
     * 是否有库存
     */
    @Setter@Getter
    Map<Long,Boolean> stocks;

    /**
     * 防重令牌
     */
    @Setter@Getter
    String OrderToken;

    /**
     * @return  订单总额
     * 所有选中商品项的价格 * 其数量
     */
    public BigDecimal getTotal() {
        BigDecimal sum =  new BigDecimal("0");
        if (items != null) {
            for (OrderItemVo item : items) {
                BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                sum = sum.add(multiply);
            }
        }
        return sum;
    }

    /**
     * 应付价格
     */
    //BigDecimal pryPrice;
    public BigDecimal getPryPrice() {
        return getTotal();
    }


    public Integer getCount(){
        Integer i =0;
        if (items!=null){
            for (OrderItemVo item : items) {
               i+=item.getCount();
            }
        }
        return i;
    }
}

收货地址,ums_member_receive_address 表

package com.atguigu.gulimall.order.vo;

@Data
public class OrderConfirmVo {

    /**
     * 收货地址,ums_member_receive_address 表
     */
    List<MemberAddressVo> addressVos;

    /**
     * 所有选中的购物车项
     */
    List<OrderItemVo> items;

    // 发票记录。。。

    /**
     * 优惠券信息
     */
    Integer integration;

    /**
     * 订单总额
     */
    BigDecimal total;

    /**
     * 应付价格
     */
    BigDecimal pryPrice;
}

商品项信息

package com.atguigu.gulimall.order.vo;
@Data
public class OrderItemVo {
    /**
     * 商品Id
     */
    private Long skuId;
    /**
     * 商品标题
     */
    private String title;
    /**
     * 商品图片
     */
    private String image;
    /**
     * 商品套餐信
     */
    private List<String> skuAttr;
    /**
     * 商品价格
     */
    private BigDecimal price;
    /**
     * 数量
     */
    private Integer count;
    /**
     * 小计价格
     */
    private BigDecimal totalPrice;

}

2、订单确认页数据获取


2.1、gulimall-order 订单确认页数据获取 接口编写

  1. Controller 层方法编写
    Gulimall-product 服务中 com.atguigu.gulimall.order.web 路径下 OrderWebController类
@Controller
public class OrderWebController {

    @Autowired
    OrderService orderService;


    @GetMapping("/toTrade")
    public String toTrade(Model model){
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        model.addAttribute("OrderConfirmData",confirmVo);
        return "confirm";
    }
}
  1. Service层实现类方法编写

    • 1、远程查询所有的地址列表
    • 2、远程查询购物车所有选中的购物项
    • 3、查询用户积分
    • 4、其他数据自动计算
    • 5、防重令牌

    Gulimall-product 服务中 com.atguigu.gulimall.order.service.impl 路径下 OrderServiceImpl

@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    @Autowired
    MemberFeignService memberFeignService;

    @Autowired
    CartFeignService cartFeignService;

    @Override
    public OrderConfirmVo confirmOrder() {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        // 1、远程查询所有的地址列表
        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
        confirmVo.setAddressVos(address);

        // 2、远程查询购物车所有选中的购物项
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);

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

        // 4、其他数据自动计算

        // 5、防重令牌

        return confirmVo;
    }

}

2.2、编写Gulimall-common获取会员所有收货地址接口

  1. 编写Controller层接口方法
    Gulimall-member 服务中com.atguigu.gulimall.member.controller路径下 MemberReceiveAddressController 类

    package com.atguigu.gulimall.member.controller;
    
    @RestController
    @RequestMapping("member/memberreceiveaddress")
    public class MemberReceiveAddressController {
        @Autowired
        private MemberReceiveAddressService memberReceiveAddressService;
    
        @GetMapping("/{memberId}/address")
        public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
            return memberReceiveAddressService.getAddress(memberId);
        }
    
  2. Service层实现类 编写 获取会有收货地址列表 方法
    Gulimall-member 服务中com.atguigu.gulimall.member.service.impl路径下 MemberReceiveAddressServiceImpl 实现类

    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
      return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
    }
    
  3. Gulimall-order服务中编写远程调用 gulimall-member服务 feign接口
    Gulimall-order服务中 com.atguigu.gulimall.order.feign 路径下 MemberFeignService 接口

    package com.atguigu.gulimall.order.feign;
    
    @FeignClient("gulimall-member")
    public interface MemberFeignService {
    
        /**
         * 返回会员所有的收货地址列表
         * @param memberId 会员ID
         * @return
         */
        @GetMapping("/member/memberreceiveaddress/{memberId}/address")
        List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
    
    }
    

2.3、编写GuliMall-cart 购物车服务中用户选择的所有购物项


  1. 首先通过用户ID在Redis中查询到购物车中的所有的购物项
  2. 通过 filter 过滤 用户购物车中被选择的购物项
  3. 查询数据库中当前购物项的价格,不能使用之前加入购物车的价格
    • 编写远程 gulimall-product 服务中的 查询sku价格接口

第一步、编写Controller层接口

编写 gulimall-cart 服务中 package com.atguigu.cart.controller; 路径下的 CartController 类:

package com.atguigu.cart.controller;

@Controller
public class CartController {

    @Autowired
    CartService cartService;

    @GetMapping("/currentUserCartItems")
    @ResponseBody
    public List<CartItem> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }
  
  	//....
}

第二步、Service层实现类 获取用户选择的所有购物项方法编写

编写 gulimall-cart 服务中 com.atguigu.cart.service.impl 路径中 CartServiceImpl 类

@Autowired
ProductFeignService productFeignService;

/**
* 获取用户选择的所有购物项
* @return
*/
@Override
public List<CartItem> getUserCartItems() {
  UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
  if (userInfoTo.getUserId() == null) {
    return null;
  } else {
    String cartKey = CART_PREFIX + userInfoTo.getUserId();
    // 获取所有用户选择的购物项
    List<CartItem> collect = getCartItems(cartKey).stream()
      .filter(item -> item.getCheck())
      .map(item->{
        // TODO 1、更新为最新价格
        R price = productFeignService.getPrice(item.getSkuId());
        String data = (String) price.get("data");
        item.setPrice(new BigDecimal(data));
        return item;
      })
      .collect(Collectors.toList());
    return collect;
  }
}

第三步、编写Gulimall-product 服务中获取指定商品的价格接口

Gulimall-product 服务中 com.atguigu.gulimall.product.app 路径下的 SkuInfoController

package com.atguigu.gulimall.product.app;

@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
    @Autowired
    private SkuInfoService skuInfoService;

    /**
     * 获取指定商品的价格
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}/price")
    public R getPrice(@PathVariable("skuId") Long skuId){
        SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
        return R.ok().setData(skuInfoEntity.getPrice().toString());
    }

Gulimall-cart 服务中的 com.atguigu.cart.feign 路径下的远程调用接口 ProductFeignService

package com.atguigu.cart.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {

    //.....

    @GetMapping("/product/skuinfo/{skuId}/price")
    R getPrice(@PathVariable("skuId") Long skuId);
}

第四步、Gulimall-order服务中编写远程调用 gulimall-cart服务 feign接口

Gulimall-order服务中com.atguigu.gulimall.order.feign 路径下的 CartFeignService接口

package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-cart")
public interface CartFeignService {

    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}


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


  • 问题 :Feign远程调用的时候会丢失请求头
  • 解决:加上feign远程调用的请求拦截器。(RequestInterceptor)
    • 因为feign在远程调用之前会执行所有的RequestInterceptor拦截器

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

在 gulimall-order 服务中 com.atguigu.gulimall.order.config 路径下编写Feign配置类:GulimallFeignConfig类 并编写请求拦截器

package com.atguigu.gulimall.order.config;

@Configuration
public class GulimallFeignConfig {

    /**
     * feign在远程调用之前会执行所有的RequestInterceptor拦截器
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes!=null){
                    HttpServletRequest request = attributes.getRequest();
                    // 2、同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给新请求同步了老请求的cookie
                    requestTemplate.header("Cookie",cookie);
                }
            }
        };
    }
}

4、Feign异步调用丢失请求头问题


此时:查询购物项、库存和收货地址都要调用远程服务,串行会浪费大量时间,因此我们进行异步编排优化

  • 问题
    由于 RequestContextHolder底层使用的是线程共享数据 ThreadLocal<RequestAttributes>,我们知道线程共享数据的域是 当前线程下,线程之间是不共享的。所以在开启异步时获取不到老请求的信息,自然也就无法共享cookie了。
  • 解决
    向 RequestContextHolder 线程域中放主线程的域。

在这里插入图片描述

修改 gulimall-order 服务中 com.atguigu.gulimall.order.service.impl 目录下的 OrderServiceImpl 类

在这里插入图片描述

@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    OrderConfirmVo confirmVo = new OrderConfirmVo();
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

    // 获取主线程的域
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    // 1、远程查询所有的地址列表
    CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
        RequestContextHolder.setRequestAttributes(requestAttributes);
        // 将主线程的域放在该线程的域中
        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
        confirmVo.setAddressVos(address);
    }, executor);

    // 2、远程查询购物车所有选中的购物项
    CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
        // 将老请求的域放在该线程的域中
        RequestContextHolder.setRequestAttributes(requestAttributes);
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);
    }, executor);


    // feign在远程调用请求之前要构造

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

    // 4、其他数据自动计算

    // TODO 5、防重令牌
    CompletableFuture.allOf(getAddressFuture,cartFuture).get();
    return confirmVo;
}

5、订单确认页渲染


在这里插入图片描述

修改 gulimall-order 服务中,src/main/resources/templates/路径下的 confirm.html

<!--主体部分-->
<p class="p1">填写并核对订单信息</p>
<div class="section">
   <!--收货人信息-->
   <div class="top-2">
      <span>收货人信息</span>
      <span>新增收货地址</span>
   </div>

   <!--地址-->
   <div class="top-3" th:each="addr:${orderConfirmData.addressVos}">
      <p>[[${addr.name}]]</p><span>[[${addr.name}]]  [[${addr.province}]]  [[${addr.city}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
   </div>
   <p class="p2">更多地址︾</p>
   <div class="hh1"/></div>
<div class="xia">
   <div class="qian">
      <p class="qian_y">
         <span>[[${orderConfirmData.count}]]</span>
         <span>件商品,总商品金额:</span>
         <span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]</span>
      </p>
      <p class="qian_y">
         <span>返现:</span>
         <span class="rmb">  -¥0.00</span>
      </p>
      <p class="qian_y">
         <span>运费: </span>
         <span class="rmb"> &nbsp ¥0.00</span>
      </p>
      <p class="qian_y">
         <span>服务费: </span>
         <span class="rmb"> &nbsp ¥0.00</span>
      </p>
      <p class="qian_y">
         <span>退换无忧: </span>
         <span class="rmb"> &nbsp ¥0.00</span>
      </p>

   </div>

   <div class="yfze">
      <p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.pryPrice,1,2)}]]</span></p>
      <p class="yfze_b">寄送至:  IT-中心研发二部 收货人:</p>
   </div>
   <button class="tijiao">提交订单</button>
</div>

6、订单确认页库存查询


需求:有货、无货

在远程查询购物车所有选中的购物项之后进行 批量查询库存

在这里插入图片描述

1)、在订单确认页数据获取 Service层实现类 OrderServiceImpl 方法中进行批量查询库存

1、修改Gulimall-order 服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

在这里插入图片描述

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取主线程的请求域
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、远程查询所有的地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 将主线程的请求域放在该线程请求域中
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddressVos(address);
        }, executor);

        // 2、远程查询购物车所有选中的购物项
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 将主线程的请求域放在该线程请求域中
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            // 批量查询商品项库存
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wareFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null) {
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        }, executor);

        // feign在远程调用请求之前要构造

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

        // 4、其他数据自动计算

        // TODO 5、防重令牌
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

2)、在gulimall-order 服务中创建商品是否有库存的VO类

在 Gulimall-order 服务中 package com.atguigu.gulimall.order.vo 路径下创建 SkuStockVo 类

package com.atguigu.gulimall.order.vo;
@Data
public class SkuStockVo {
    private Long skuId;
    private Boolean hasStock;
}

3)、gulimall-ware 库存服务中提供 查询库存的接口

  1. gulimall-ware 服务中 com.atguigu.gulimall.ware.controller 路径下的 WareSkuController 类,之前编写过。
package com.atguigu.gulimall.ware.controller;

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;


    // 查询sku是否有库存
    @PostMapping("/hasstock")
    public R getSkusHasStock(@RequestBody List<Long> skuIds){
        // sku_id,stock
        List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);

        return R.ok().setData(vos);
    }
  //....
}
  1. gulimall-order 服务中编写远程调用 gulimall-ware 库存服务中 查询库存 feign接口
    gulimall-order 服务下 com.atguigu.gulimall.order.feign 路径下:WareFeignService
package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-ware")
public interface WareFeignService {

    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);
}

4)、页面效果

[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]

<div class="mi">
   <p>[[${item.title}]]<span style="color: red;"> ¥ [[${#numbers.formatDecimal(item.price,1,2)}]]</span> <span> x[[${item.count}]]</span> <span>[[${orderConfirmData.stocks[item.skuId]?"有货":"无货"}]]</span></p>
   <p><span>0.095kg</span></p>
   <p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>

7、模拟运费效果


需求:选择收货地址,计算物流费

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1paZHi3c-1650101180086)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220412183318234.png)]

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

7.1、选择收货地址页面效果

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

function highlight(){
   $(".addr-item p").css({"border": "2px solid gray"});
   $(".addr-item p[def='1']").css({"border": "2px solid red"});
}
$(".addr-item p").click(function () {
   $(".addr-item p").attr("def","0");
   $(this).attr("def","1");
   highlight();
   // 获取当前地址id
   var addrId = $(this).attr("addrId");
   // 发送ajax获取运费信息
   getFare(addrId);
});
function getFare(addrId) {
   $.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
      console.log(resp);
      $("#fareEle").text(resp.data.fare);
      var total = [[${orderConfirmData.total}]]
      // 设置运费信息
      $("#payPriceEle").text(total*1 + resp.data.fare*1);
      // 设置收货人信息
      $("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
      $("#reveiverEle").text(resp.data.address.name);
   })
}

7.2、后端接口提供根据用户地址ID,返回详细地址并计算物流费h


2、gulimall-ware仓储服务编写 根据用户地址,返回详细地址并计算物流费h

  1. gulimall-ware 服务中 com.atguigu.gulimall.ware.controller路径下 WareInfoController 类
package com.atguigu.gulimall.ware.controller;

@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
    @Autowired
    private WareInfoService wareInfoService;

    @GetMapping("/fare")
    public R getFare(@RequestParam("addrId") Long addrId){
        FareVo fare = wareInfoService.getFare(addrId);
        return R.ok().setData(fare);
    }
  //...
}
  1. gulimall-ware 服务中 com.atguigu.gulimall.ware.service.impl路径下 WareInfoServiceImpl 类
@Override
public FareVo getFare(Long addrId) {

  FareVo fareVo = new FareVo();
  R r = memberFeignService.addrInfo(addrId);
  MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
  });
  if (data!=null) {
    // 简单处理:截取手机号最后一位作为邮费
    String phone = data.getPhone();
    String substring = phone.substring(phone.length() - 1, phone.length());
    BigDecimal bigDecimal = new BigDecimal(substring);
    fareVo.setAddressVo(data);
    fareVo.setFare(bigDecimal);
    return fareVo;
  }
  return null;
}
  1. gulimall-ware 服务中 com.atguigu.gulimall.ware.feign路径下 MemberFeignService远程查询地址详细信息feign接口
package com.atguigu.gulimall.ware.feign;

@FeignClient("gulimall-member")
public interface MemberFeignService {

    /**
     * 根据地址id查询地址的详细信息
     * @param id
     * @return
     */
    @RequestMapping("/member/memberreceiveaddress/info/{id}")
    R addrInfo(@PathVariable("id") Long id);
}
  1. gulimall-ware 服务中 com.atguigu.gulimall.ware.vo路径下的 Vo
@Data
public class FareVo {
    private MemberAddressVo addressVo;
    private BigDecimal fare;
}


六、接口幂等性讨论

1、幂等性概述

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的。

  • 接口幂等性
    接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用,比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也交成了两条这就没有保证接口的幂等性。
  • 哪些情况需要防止:
    • 用户多次点击按钮
    • 用户页面回退再次提交
    • 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
      其他业务情況
  • 幂等性解决方案
    • 1、token机制(令牌机制)本项目采用令牌机制
    • 2、各种锁机制
    • 3、各种唯一性约束
    • 4、防重表
    • 5、全球请求唯一id

在这里插入图片描述

2、添加防重令牌

gulimall-order服务 com.atguigu.gulimall.order.service.impl路径下的 OrderServiceImpl

package com.atguigu.gulimall.order.service.impl;


@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    @Autowired
    MemberFeignService memberFeignService;

    @Autowired
    CartFeignService cartFeignService;

    @Autowired
    ThreadPoolExecutor executor;

    @Autowired
    WareFeignService wareFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取主线程的请求域
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、远程查询所有的地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 将主线程的请求域放在该线程请求域中
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddressVos(address);
        }, executor);

        // 2、远程查询购物车所有选中的购物项
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 将主线程的请求域放在该线程请求域中
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            // 批量查询商品项库存
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wareFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null) {
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        }, executor);

        // feign在远程调用请求之前要构造

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

        // 4、其他数据自动计算

        // TODO 5、防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);

        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

}


七、订单提交

Controller 层编写下单功能接口

gulimall-order 服务 com.atguigu.gulimall.order.web 路径下的 OrderWebController 类,代码如下

/**
 * 下单功能
 * @param vo
 * @return
 */
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
    // 1、创建订单、验令牌、验价格、验库存
    try {
        SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
        if (responseVo.getCode() == 0) {
            // 下单成功来到支付选择页
            model.addAttribute("submitOrderResp",responseVo);
            return "pay";
        } else {
            // 下单失败回到订单确认页重新确认订单信息
            String msg = "下单失败: ";
            switch ( responseVo.getCode()){
                case 1: msg+="订单信息过期,请刷新再次提交";break;
                case 2: msg+="订单商品价格发生变化,请确认后再次提交";break;
                case 3: msg+="商品库存不足";break;
            }
            redirectAttributes.addAttribute("msg",msg);
            return "redirect:http://order.gulimall.cn/toTrade";
        }
    } catch (Exception e){
        if (e instanceof NoStockException) {
            String message = e.getMessage();
            redirectAttributes.addFlashAttribute("msg", message);
        }
        return "redirect:http://order.gulimall.cn/toTrade";
    }

}

7.1、封装订单提交的VO


  1. 页面提交数据 添加“com.atguigu.gulimall.order.vo.OrderSubmitVo”类,代码如下:
@Data
@ToString
public class OrderSubmitVo {
    /**
     * 收货地址Id
     */
    private Long addrId;
    /**
     * 支付方式
     */
    private Integer payType;
    // 无需提交需要购买的商品,去购物车再获取一遍
    // 优惠发票
    /**
     * 防重令牌
     */
    private String orderToken;
    /**
     * 应付价格,验价
     */
    private BigDecimal payPrice;
    /**
     * 订单备注
     */
    private String note;

    /**
     * 用户相关信息,直接去Session取出登录的用户
     */
}
  1. 前端页面 confirm.html 提供数据
<form action="http://order.gulimall.cn/submitOrder" method="post">
   <input id="addrIdInput" type="hidden" name="addrId">
   <input id="payPriceInput" type="hidden" name="payPrice">
   <input type="hidden" name="orderToken" th:value="${orderConfirmData.orderToken}">
   <button class="tijiao" type="submit">提交订单</button>
</form>
function getFare(addrId) {
   // 给表单回填的地址
   $("#addrIdInput").val(addrId);
   $.get("http://gulimall.cn/api/ware/wareinfo/fare?addrId="+addrId,function (resp) {
      console.log(resp);
      $("#fareEle").text(resp.data.fare);
      var total = [[${orderConfirmData.total}]]
      // 设置运费信息
      var pryPrice = total*1 + resp.data.fare*1;
      $("#payPriceEle").text(pryPrice);
      $("#payPriceInput").val(pryPrice);
      // 设置收货人信息
      $("#reciveAddressEle").text(resp.data.address.province+" " + resp.data.address.region+ "" + resp.data.address.detailAddress);
      $("#reveiverEle").text(resp.data.address.name);
   })
}

7.2、原子验令牌


  • 问题:存在网路延时,同时提交从Redis拿到的令牌一直,导致重复提交
  • 解决:令牌的对比和删除必须保证原子性

1)、封装提交订单数据

package com.atguigu.gulimall.order.vo;

@Data
public class SubmitOrderResponseVo {
    private OrderEntity order;
    private Integer code;   //0成功,错误状态码
}

2)、修改 SubmitOrderResponseVo 类编写验证令牌操作

/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @param vo
 * @return
 */
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    SubmitOrderResponseVo response = new SubmitOrderResponseVo();

    // 从拦截器中拿到当前的用户
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    String orderToken = vo.getOrderToken();
    // 原子验证令牌和删除令牌
    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 {
        // 令牌验证成功
        return response;
    }
}

7.3、创建订单、订单项等信息


gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

/**
 * 创建订单、订单项等信息
 * @return
 */
private OrderCreateTo createOrder(){
    OrderCreateTo createTo = new OrderCreateTo();
    // 1、生成一个订单号
    String orderSn = IdWorker.getTimeId();
    // 2、构建一个订单
    OrderEntity orderEntity = buildOrder(orderSn);
    // 3、获取到所有的订单项
    List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
    // 4、计算价格、积分等相关信息
    computePrice(orderEntity,itemEntities);

    createTo.setOrder(orderEntity);
    createTo.setOrderItems(itemEntities);
    return createTo;
}

7.3.1、创建订单


gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

/**
 * 构建订单
 * @param orderSn
 * @return
 */
private OrderEntity buildOrder(String orderSn) {
    MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
    OrderEntity entity = new OrderEntity();
    entity.setOrderSn(orderSn);
    entity.setMemberId(respVp.getId());

    OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
    // 1、获取运费 和 收货信息
    R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
    FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
    });
    // 2、设置运费
    entity.setFreightAmount(fareResp.getFare());
    // 3、设置收货人信息
    entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
    entity.setReceiverProvince(fareResp.getAddress().getProvince());
    entity.setReceiverRegion(fareResp.getAddress().getRegion());
    entity.setReceiverCity(fareResp.getAddress().getCity());
    entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
    entity.setReceiverName(fareResp.getAddress().getName());
    entity.setReceiverPhone(fareResp.getAddress().getPhone());
    // 4、设置订单的相关状态信息
    entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    // 5、默认取消信息
    entity.setAutoConfirmDay(7);
    return entity;
}

1)、创建远程调用 gulimall-ware 服务 计算运费和详细地址方法的接口

gulimall-order服务中 com.atguigu.gulimall.order.feign 路径下的 WareFeignService 类

package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-ware")
public interface WareFeignService {

    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);

    /**
     * 计算运费和详细地址
     * @param addrId
     * @return
     */
    @GetMapping("/ware/wareinfo/fare")
    R getFare(@RequestParam("addrId") Long addrId);
}

2)、创建 计算运费和详细地址方法 信息封装VO

gulimall-order服务中 com.atguigu.gulimall.order.vo 路径下的 FareVo 类

package com.atguigu.gulimall.order.vo;

@Data
public class FareVo {
    private MemberAddressVo address;
    private BigDecimal fare;
}

7.3.2、构造订单项数据


1)、构建订单项数据

gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl 类

/**
 * 构建所有订单项数据
 * @return
 */
private  List<OrderItemEntity> buildOrderItems(String orderSn) {
    // 最后确定每个购物项的价格
    List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
    if (currentUserCartItems != null && currentUserCartItems.size()>0){
        List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
            OrderItemEntity itemEntity = buildOrderItem(cartItem);
            itemEntity.setOrderSn(orderSn);
            return itemEntity;
        }).collect(Collectors.toList());
        return itemEntities;
    }
    return null;
}

/**
 * 构建某一个订单项
 * @param cartItem
 * @return
 */
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
    OrderItemEntity itemEntity = new OrderItemEntity();
    // 1、订单信息:订单号 v
    // 2、商品的spu信息
    Long skuId = cartItem.getSkuId();
    R r = productFeignService.getSpuInfoBySkuId(skuId);
    SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
    });
    itemEntity.setSpuId(data.getId());
    itemEntity.setSpuBrand(data.getBrandId().toString());
    itemEntity.setSpuName(data.getSpuName());
    itemEntity.setCategoryId(data.getCatalogId());
    // 3、商品的sku信息  v
    itemEntity.setSkuId(cartItem.getSkuId());
    itemEntity.setSkuName(cartItem.getTitle());
    itemEntity.setSkuPic(cartItem.getImage());
    itemEntity.setSkuPrice(cartItem.getPrice());
    itemEntity.setSkuQuantity(cartItem.getCount());
    itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
    // 4、优惠信息【不做】
    // 5、积分信息
    itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    // 6、订单项的价格信息
    itemEntity.setPromotionAmount(new BigDecimal("0"));
    itemEntity.setCouponAmount(new BigDecimal("0"));
    itemEntity.setIntegrationAmount(new BigDecimal("0"));
    // 当前订单项的实际金额 总额-各种优惠
    BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
    BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
            subtract(itemEntity.getCouponAmount()).
            subtract(itemEntity.getIntegrationAmount());
    itemEntity.setRealAmount(subtract);
    return itemEntity;
}

2)、gulimall-product服务中编写通过skuId查询spu信息接口

  1. gulimall-product服务 com.atguigu.gulimall.product.app 路径下 SpuInfoController 类,代码如下:
package com.atguigu.gulimall.product.app;

@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
    @Autowired
    private SpuInfoService spuInfoService;


    /**
     * 查询指定sku的spu信息
     * @param skuId
     * @return
     */
    @GetMapping("/skuId/{id}")
    public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
        SpuInfoEntity entity = spuInfoService.getSpuInfoBySkuId(skuId);
        return R.ok().setData(entity);
    }
  1. gulimall-product服务 com.atguigu.gulimall.product.service.impl 路径下 SpuInfoServiceImpl 类,代码如下:
package com.atguigu.gulimall.product.service.impl;

@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
    SkuInfoEntity byId = skuInfoService.getById(skuId);
    Long spuId = byId.getSpuId();
    SpuInfoEntity spuInfoEntity = getById(spuId);
    return spuInfoEntity;
}
  1. gulimall-order服务 com.atguigu.gulimall.order.feign 路径下 ProductFeignService 类,代码如下:
package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-product")
public interface ProductFeignService {

    @GetMapping("/product/spuinfo/skuId/{id}")
    R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}
  1. gulimall-order服务 com.atguigu.gulimall.order.vo 路径下 SpuInfoVo 类,用来接收查询过来的Spu信息;代码如下:
package com.atguigu.gulimall.order.vo;

@Data
public class SpuInfoVo {
    /**
     * 商品id
     */
    @TableId
    private Long id;
    /**
     * 商品名称
     */
    private String spuName;
    /**
     * 商品描述
     */
    private String spuDescription;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     *
     */
    private BigDecimal weight;
    /**
     * 上架状态[0 - 新建,1 - 上架,2-下架]
     */
    private Integer publishStatus;
    /**
     *
     */
    private Date createTime;
    /**
     *
     */
    private Date updateTime;
}

7.3.3、计算价格

/**
 * 计算价格
 * @param orderEntity
 * @param itemEntities
 */
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
    BigDecimal total = new BigDecimal("0.0");
    BigDecimal coupon = new BigDecimal("0.0");
    BigDecimal integration = new BigDecimal("0.0");
    BigDecimal promotion = new BigDecimal("0.0");
    BigDecimal gift = new BigDecimal("0.0");
    BigDecimal growth = new BigDecimal("0.0");
    // 1、订单的总额,叠加每一个订单项的总额信息
    for (OrderItemEntity entity : itemEntities) {
        total = total.add(entity.getRealAmount());
        coupon = coupon.add(entity.getCouponAmount());
        integration = integration.add(entity.getIntegrationAmount());
        promotion = promotion.add(entity.getPromotionAmount());
        gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
        growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
    }
    // 订单总额
    orderEntity.setTotalAmount(total);
    // 应付总额
    orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
    orderEntity.setCouponAmount(coupon);
    orderEntity.setIntegrationAmount(integration);
    orderEntity.setPromotionAmount(promotion);
    // 设置积分等信息
    orderEntity.setIntegration(gift.intValue());
    orderEntity.setGrowth(growth.intValue());
    orderEntity.setDeleteStatus(0);//0 未删除
}

7.4、保存订单数据、锁定库存


在这里插入图片描述

7.4.1、保存订单数据并锁定库存

1)、编写 保存订单数据并锁定库存 逻辑实现代码

/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @param vo
 * @return
 */
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    // 在当前线程共享 OrderSubmitVo
    confirmVoThreadLocal.set(vo);
    SubmitOrderResponseVo response = new SubmitOrderResponseVo();
    // 从拦截器中拿到当前的用户
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    response.setCode(0);

    // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    String orderToken = vo.getOrderToken();
    // 原子验证令牌和删除令牌
    Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
            Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
    if (result == 0L) {
        // 令牌验证失败
        response.setCode(1);
        return response;
    } else {
        // 令牌验证成功
        // 2、创建订单、订单项等信息
        OrderCreateTo order = createOrder();
        // 3、验价
        BigDecimal payAmount = order.getOrder().getPayAmount();
        BigDecimal payPrice = vo.getPayPrice();
        if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
            // 金额对比成功
            // 4、保存订单;
            saveOrder(order);
            // 5、库存锁定,只要有异常回滚订单数据
            // 订单号,所有订单项(skuId,skuName,num)
            WareSkuLockVo lockVo = new WareSkuLockVo();
            lockVo.setOrderSn(order.getOrder().getOrderSn());
            List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
                OrderItemVo itemVo = new OrderItemVo();
                itemVo.setSkuId(item.getSkuId());
                itemVo.setCount(item.getSkuQuantity());
                itemVo.setTitle(item.getSkuName());
                return itemVo;
            }).collect(Collectors.toList());
            lockVo.setLocks(locks);
            // TODO 远程锁库存
            R r = wareFeignService.orderLockStock(lockVo);
            if (r.getCode() == 0) {
                // 锁成功了
                response.setOrder(order.getOrder());
                return response;
            }else {
                // 锁定失败
                throw new NoStockException((String) r.get("msg"));
            }

        } else {
            // 金额对比失败
            response.setCode(2);
            return response;
        }
    }
}

2)、编写超时异常类

gulimall-common服务中com.atguigu.common.exception路径下的 NoStockException 接口:

package com.atguigu.common.exception;

public class NoStockException extends RuntimeException{
    private Long skuId;

    public NoStockException(Long skuId){
        super("商品id:"+skuId+";没有足够的库存了!");
    }

    public NoStockException(String message) {
        super(message);
    }

    @Override
    public String getMessage() {
        return super.getMessage();
    }

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

}

7.4.2、锁定库存


1)、gulimall-order服务中编写远程调用 gulimall-ware (仓储服务) 锁定库存方法 的接口

gulimall-order服务中com.atguigu.gulimall.order.feign路径下的 WareFeignService 接口:

package com.atguigu.gulimall.order.feign;

@FeignClient("gulimall-ware")
public interface WareFeignService {
		//....

    /**
     * 锁定指定订单的库存
     * @param vo
     * @return
     */
    @PostMapping("/ware/waresku/lock/order")
    R orderLockStock(@RequestBody WareSkuLockVo vo);
}

2)、gulimall-ware (仓储服务)中编写 锁定库存 的接口

  1. gulimall-ware服务中com.atguigu.gulimall.ware.controller路径下的 WareSkuController 类:
package com.atguigu.gulimall.ware.controller;

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;

    /**
     * 锁定订单项库存
     * @param vo
     * @return
     */
    @PostMapping("/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo){
        try {
            Boolean stock = wareSkuService.orderLockStock(vo);
            return R.ok();
        } catch (NoStockException e){
            return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(),BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());
        }
    }
  //....
}
  1. gulimall-ware服务中com.atguigu.gulimall.ware.service.impl路径下的 WareSkuServiceImpl 类:
package com.atguigu.gulimall.ware.service.impl;

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {

    @Autowired
    WareSkuDao wareSkuDao;

    @Autowired
    ProductFeignService productFeignService;
		//......

    /**
     * 锁定指定订单的库存
     * @param vo
     * @return
     * (rollbackFor = NoStockException.class)
     * 默认只要是运行时异常都会回滚
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        // 1、每个商品在哪个库存里有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHashStock> collect = locks.stream().map(item -> {
            SkuWareHashStock stock = new SkuWareHashStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            // 查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());

        // 2、锁定库存
        for (SkuWareHashStock hashStock : collect) {
            Boolean skuStocked = false;
            Long skuId = hashStock.getSkuId();
            List<Long> wareIds = hashStock.getWareId();
            if (wareIds == null || wareIds.size()==0){
                // 没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            for (Long wareId : wareIds) {
                // 成功就返回1,否则就返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
                if (count == 1){
                    skuStocked = true;
                    break;
                } else {
                    // 当前仓库锁失败,重试下一个仓库

                }
            }
            if (skuStocked == false){
                // 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
                throw new NoStockException(skuId);
            }
        }

        // 3、运行到这,全部都是锁定成功的
        return true;
    }
    @Data
    class SkuWareHashStock{
        private Long skuId;     // skuid
        private Integer num;    // 锁定件数
        private List<Long> wareId;  // 锁定仓库id
    }

}
  1. 查询这个商品在哪里有库存

gulimall-ware服务中com.atguigu.gulimall.ware.dao路径下的 WareSkuDao 类:

package com.atguigu.gulimall.ware.dao;

@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {

    /**
     * 通过skuId查询在哪个仓库有库存
     * @param skuId
     * @return  仓库的编号
     */
    List<Long> listWareIdHashSkuStock(@Param("skuId") Long skuId);

    /**
     * 锁库存
     * @param skuId
     * @param wareId
     * @param num
     * @return
     */
    Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);

}

gulimall-ware服务中gulimall-ware/src/main/resources/mapper/ware路径下的 WareSkuDao.xml:

<update id="addStock">
    UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>

<select id="listWareIdHashSkuStock" resultType="java.lang.Long">
    SELECT ware_id FROM wms_ware_sku WHERE sku_id=#{skuId} and stock-stock_locked>0;
</select>
  1. 编写异常返回类

gulimall-ware服务中com.atguigu.gulimall.ware.exception路径下的 NoStockException:

package com.atguigu.gulimall.ware.exception;

public class NoStockException extends RuntimeException{
    private Long skuId;
    public NoStockException(Long skuId){
        super("商品id:"+skuId+";没有足够的库存了!");
    }

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }
}

3)、在 错误码和错误信息定义类 BizCodeEnume枚举类中新增 库存 错误码和信息

gulimall-common服务中com.atguigu.common.exception路径下的 BizCodeEnume:

21: 库存

package com.atguigu.common.exception;

public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户名已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已被注册"),
    NO_STOCK_EXCEPTION(21000,"商品库存不足"),
    LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

7.5、前端页面的修改


  1. 订单提交成功,跳转到支付页面 pay.html
<div class="Jdbox_BuySuc">
  <dl>
    <dt><img src="/static/order/pay/img/saoyisao.png" alt=""></dt>
    <dd>
      <span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
      <span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font></span>
    </dd>
    <dd>
      <span>推荐使用</span>
      <span>扫码支付请您在<font>24小时</font>内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限)</span>
      <span>订单详细</span>
    </dd>
  </dl>
</div>

在这里插入图片描述

  1. 订单提交失败,重定项到confirm.html 并回显 失败原因
<p class="p1">填写并核对订单信息 <span style="color: red" th:value="${msg!=null}" th:text="${msg}"></span></p>

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


7.6、主体代码


1、Controller层接口编写

gulimall-order服务中com.atguigu.gulimall.order.web路径下的 OrderWebController:

package com.atguigu.gulimall.order.web;

@Controller
public class OrderWebController {

    @Autowired
    OrderService orderService;

    @GetMapping("/toTrade")
    public String toTrade(Model model) throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        model.addAttribute("orderConfirmData",confirmVo);
        return "confirm";
    }

    /**
     * 下单功能
     * @param vo
     * @return
     */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){
        // 1、创建订单、验令牌、验价格、验库存
        try {
            SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
            if (responseVo.getCode() == 0) {
                // 下单成功来到支付选择页
                model.addAttribute("submitOrderResp",responseVo);
                return "pay";
            } else {
                // 下单失败回到订单确认页重新确认订单信息
                String msg = "下单失败: ";
                switch ( responseVo.getCode()){
                    case 1: msg+="订单信息过期,请刷新再次提交";break;
                    case 2: msg+="订单商品价格发生变化,请确认后再次提交";break;
                    case 3: msg+="商品库存不足";break;
                }
                redirectAttributes.addAttribute("msg",msg);
                return "redirect:http://order.gulimall.cn/toTrade";
            }
        } catch (Exception e){
            if (e instanceof NoStockException) {
                String message = e.getMessage();
                redirectAttributes.addFlashAttribute("msg", message);
            }
            return "redirect:http://order.gulimall.cn/toTrade";
        }

    }
}

2、Service层代码

gulimall-order服务中com.atguigu.gulimall.order.service.impl路径下的 OrderServiceImpl:

package com.atguigu.gulimall.order.service.impl;

import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.exception.NoStockException;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.gulimall.order.constant.OrderConstant;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.enume.OrderStatusEnum;
import com.atguigu.gulimall.order.feign.CartFeignService;
import com.atguigu.gulimall.order.feign.MemberFeignService;
import com.atguigu.gulimall.order.feign.ProductFeignService;
import com.atguigu.gulimall.order.feign.WareFeignService;
import com.atguigu.gulimall.order.interceptoe.LoginUserInterceptor;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.atguigu.gulimall.order.to.OrderCreateTo;
import com.atguigu.gulimall.order.vo.*;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.order.dao.OrderDao;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.service.OrderService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;


@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Autowired
    MemberFeignService memberFeignService;

    @Autowired
    CartFeignService cartFeignService;

    @Autowired
    ThreadPoolExecutor executor;

    @Autowired
    WareFeignService wareFeignService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    OrderItemService orderItemService;



    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取主线程的请求域
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 1、远程查询所有的地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 将主线程的请求域放在该线程请求域中
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddressVos(address);
        }, executor);

        // 2、远程查询购物车所有选中的购物项
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 将主线程的请求域放在该线程请求域中
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            // 批量查询商品项库存
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wareFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data != null) {
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        }, executor);

        // feign在远程调用请求之前要构造

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

        // 4、其他数据自动计算

        // TODO 5、防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);

        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

    /**
     * 下单操作:验令牌、创建订单、验价格、验库存
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        // 在当前线程共享 OrderSubmitVo
        confirmVoThreadLocal.set(vo);
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        // 从拦截器中拿到当前的用户
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        response.setCode(0);

        // 1、验证令牌【令牌的对比和删除必须保证原子性】,通过使用脚本来完成(0:令牌校验失败; 1: 删除成功)
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();
        // 原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if (result == 0L) {
            // 令牌验证失败
            response.setCode(1);
            return response;
        } else {
            // 令牌验证成功
            // 2、创建订单、订单项等信息
            OrderCreateTo order = createOrder();
            // 3、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();
            if (Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
                // 金额对比成功
                // 4、保存订单;
                saveOrder(order);
                // 5、库存锁定,只要有异常回滚订单数据
                // 订单号,所有订单项(skuId,skuName,num)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());
                List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
                    OrderItemVo itemVo = new OrderItemVo();
                    itemVo.setSkuId(item.getSkuId());
                    itemVo.setCount(item.getSkuQuantity());
                    itemVo.setTitle(item.getSkuName());
                    return itemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(locks);
                // TODO 远程锁库存
                R r = wareFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    // 锁成功了
                    response.setOrder(order.getOrder());
                    return response;
                }else {
                    // 锁定失败
                    throw new NoStockException((String) r.get("msg"));
                }

            } else {
                // 金额对比失败
                response.setCode(2);
                return response;
            }
        }
    }

    /**
     * 保存订单、订单项数据
     * @param order
     */
    private void saveOrder(OrderCreateTo order) {
        OrderEntity orderEntity = order.getOrder();
        orderEntity.setModifyTime(new Date());
        this.save(orderEntity);

        List<OrderItemEntity> orderItems = order.getOrderItems();
        orderItemService.saveBatch(orderItems);

    }

    /**
     * 创建订单、订单项等信息
     * @return
     */
    private OrderCreateTo createOrder(){
        OrderCreateTo createTo = new OrderCreateTo();
        // 1、生成一个订单号
        String orderSn = IdWorker.getTimeId();
        // 2、构建一个订单
        OrderEntity orderEntity = buildOrder(orderSn);
        // 3、获取到所有的订单项
        List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
        // 4、计算价格、积分等相关信息
        computePrice(orderEntity,itemEntities);

        createTo.setOrder(orderEntity);
        createTo.setOrderItems(itemEntities);
        return createTo;
    }



    /**
     * 构建订单
     * @param orderSn
     * @return
     */
    private OrderEntity buildOrder(String orderSn) {
        MemberRespVo respVp = LoginUserInterceptor.loginUser.get();
        OrderEntity entity = new OrderEntity();
        entity.setOrderSn(orderSn);
        entity.setMemberId(respVp.getId());

        OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
        // 1、获取运费 和 收货信息
        R fare = wareFeignService.getFare(orderSubmitVo.getAddrId());
        FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
        });
        // 2、设置运费
        entity.setFreightAmount(fareResp.getFare());
        // 3、设置收货人信息
        entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
        entity.setReceiverProvince(fareResp.getAddress().getProvince());
        entity.setReceiverRegion(fareResp.getAddress().getRegion());
        entity.setReceiverCity(fareResp.getAddress().getCity());
        entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
        entity.setReceiverName(fareResp.getAddress().getName());
        entity.setReceiverPhone(fareResp.getAddress().getPhone());
        // 4、设置订单的相关状态信息
        entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
        // 5、默认取消信息
        entity.setAutoConfirmDay(7);
        return entity;
    }

    /**
     * 构建所有订单项数据
     * @return
     */
    private  List<OrderItemEntity> buildOrderItems(String orderSn) {
        // 最后确定每个购物项的价格
        List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
        if (currentUserCartItems != null && currentUserCartItems.size()>0){
            List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
                OrderItemEntity itemEntity = buildOrderItem(cartItem);
                itemEntity.setOrderSn(orderSn);
                return itemEntity;
            }).collect(Collectors.toList());
            return itemEntities;
        }
        return null;
    }

    /**
     * 构建某一个订单项
     * @param cartItem
     * @return
     */
    private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
        OrderItemEntity itemEntity = new OrderItemEntity();
        // 1、订单信息:订单号 v
        // 2、商品的spu信息
        Long skuId = cartItem.getSkuId();
        R r = productFeignService.getSpuInfoBySkuId(skuId);
        SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
        });
        itemEntity.setSpuId(data.getId());
        itemEntity.setSpuBrand(data.getBrandId().toString());
        itemEntity.setSpuName(data.getSpuName());
        itemEntity.setCategoryId(data.getCatalogId());
        // 3、商品的sku信息  v
        itemEntity.setSkuId(cartItem.getSkuId());
        itemEntity.setSkuName(cartItem.getTitle());
        itemEntity.setSkuPic(cartItem.getImage());
        itemEntity.setSkuPrice(cartItem.getPrice());
        itemEntity.setSkuQuantity(cartItem.getCount());
        itemEntity.setSkuAttrsVals(StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(),";"));
        // 4、优惠信息【不做】
        // 5、积分信息
        itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
        // 6、订单项的价格信息
        itemEntity.setPromotionAmount(new BigDecimal("0"));
        itemEntity.setCouponAmount(new BigDecimal("0"));
        itemEntity.setIntegrationAmount(new BigDecimal("0"));
        // 当前订单项的实际金额 总额-各种优惠
        BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
        BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount()).
                subtract(itemEntity.getCouponAmount()).
                subtract(itemEntity.getIntegrationAmount());
        itemEntity.setRealAmount(subtract);
        return itemEntity;
    }

    /**
     * 计算价格
     * @param orderEntity
     * @param itemEntities
     */
    private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> itemEntities) {
        BigDecimal total = new BigDecimal("0.0");
        BigDecimal coupon = new BigDecimal("0.0");
        BigDecimal integration = new BigDecimal("0.0");
        BigDecimal promotion = new BigDecimal("0.0");
        BigDecimal gift = new BigDecimal("0.0");
        BigDecimal growth = new BigDecimal("0.0");
        // 1、订单的总额,叠加每一个订单项的总额信息
        for (OrderItemEntity entity : itemEntities) {
            total = total.add(entity.getRealAmount());
            coupon = coupon.add(entity.getCouponAmount());
            integration = integration.add(entity.getIntegrationAmount());
            promotion = promotion.add(entity.getPromotionAmount());
            gift = gift.add(new BigDecimal(entity.getGiftIntegration()));
            growth = growth.add(new BigDecimal(entity.getGiftGrowth()));
        }
        // 订单总额
        orderEntity.setTotalAmount(total);
        // 应付总额
        orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
        orderEntity.setCouponAmount(coupon);
        orderEntity.setIntegrationAmount(integration);
        orderEntity.setPromotionAmount(promotion);
        // 设置积分等信息
        orderEntity.setIntegration(gift.intValue());
        orderEntity.setGrowth(growth.intValue());
        orderEntity.setDeleteStatus(0);//0 未删除
    }

}


八、分布式事务

8.1、本地事务在分布式下的问题


在这里插入图片描述

问题:

  1. 远程服务假失败:
    远程服务其实成功了,由于网络故障等没有返回。
    导致:订单回滚,库存却扣减
  2. 远程服务执行完成,下面的其他方法出现问题
    导致:已执行的远程请求,肯定不能回滚。数据不一致问题
    在这里插入图片描述

SpringBoot事务的坑:

在同一个类里面,编写两个方法,内部调用的的时候,会导致事务设置失效。原因没有用到代理对象的缘故。

  • 概括:同一个对象内事务方法互调默认失效(事务是加上的,但是事务的设置失效。比如说:设置超时时间),原因:绕过了代理对象
  • 解决:使用代理对象来调用事务方法
    1. 引入 spring-boot-starter-aop ,(帮我们引入了aspectj)
    2. @EnableTransactionManagement(proxyTargetClass = true) :对外暴露代理对象
    3. @EnableAspectJAutoProxy(exposeProxy = true) :开启 aspectj 动态代理功能。
    4. AopContext.currentProxy() : 调用方法

8.2、分布式事务


  • 分布式系统经常出现以下异常:
    • 机器宕机、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失……

8.2.1、分布式cap定理 和 BASE理论

cap定理

CAP 原则又称 CAP 定理,指的是在一个分布式系统中

  • 一致性 (Consistency):
    • 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
  • 可用性 (Availability):
    • 在集群中一部分节点故障后,集群整体是否还能享用客户端的读写请求。(对数据更新具备高可用性)
  • 分区容错性 (Partition tolerance):
    • 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。
      分区容错的意思是:区间通信可能失败。
      比如:一台服务器放在中国,另一台服务器放在美国,这就是两个区,他们之间可能无法通信。

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾

  • CP
  • AP

BASE理论

是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以适当的采取若一致性,即 最终一致性。

BASE 是指:

  • 基本可用(Basically Available)
    • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如:响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
      • 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2秒。
      • 功能上的损失:购物网站在购物高峰(如双十一时),为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
  • 软状态(Soft State)
    • 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。
  • 最终一致性(Nventual Consistency)
    • 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。

  • 对于关系型数据库,要求更新过的数据能够被后续的访问都能看到,这是强一致性
  • 如果能容忍后续的部分或者全部访问不到,则是弱一致性
  • 如果经过一段时间后要求能够访问到更新后的数据,则是最终一致性

8.2.2、分布式事务几种方案


1、2PC 模式

在这里插入图片描述

2、柔性事务-TCC事务补偿型方案

在这里插入图片描述

3、柔性事务-最大努力通知方案

在这里插入图片描述

4、柔性事务-可靠消息+最终一致性方案(异步确保型)

在这里插入图片描述



九、Seata

 *  6Seata控制分布式事务
 *      1)、每一个微服务先必须创建ubdo_log回滚日志表;
 *      2)、安装事务协调器:seata-server: https://github.com/seata/seata/releases
 *      3)、整合
 *          1、导入依赖 :spring-cloud-starter-alibaba-seata seata-all-0.7.1
 *          2、解压启动seata-server;
 *              registry.conf :注册中心配置 修改它: registry type = "nacos"
 *              file.conf:
 *          3、所有想用到分布式事务的微服务 使用 seata DataSourceProxy代理自己的数据源
 *          4、每个微服务都必须要导入
 *                  registry.conf
 *                  file.conf   vgroup_mapping.{application.name}-fescar-service-group = "default"
 *          5、给分布式大事务的入口标注: @GlobalTransactional
 *          6、每一个小事务标注: @Transactional

提前透露:本项目没有采用!

Satia概述:

  • Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

  • TC (Transaction Coordinator) - 事务协调者
    • 维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器
    • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器
    • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

我们只需要使用一个 @GlobalTransactional 注解在业务方法上:

    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        ......
    }

9.1、Seata 环境准备


1、在每个微服务数据库里创建一个undo_log(回滚日志表)

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2、安装事务协调器:seata-server: https://github.com/seata/seata/releases

  1. 解压启动seata-server

  2. 修改 registry.conf :注册中心配置

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 指定注册中心
  type = "nacos"

  nacos {
    serverAddr = "localhost:8848"
    namespace = "public"
    cluster = "default"
  }
  1. 启动 seata-server
hgw@HGWdeMacBook-Air bin # sh seata-server.sh

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

9.2、整合


1、导入依赖 :spring-cloud-starter-alibaba-seata seata-all-0.7.1

<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

2、给分布式大事务的路口标注@GlobalTransactional; 每一个远程的小事务用 @Transactional

在 gulimall-order服务中com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java 的 SubmitOrderResponseVo方法加上@GlobalTransactional 注解

@GlobalTransactional
@Transactional  // 本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚。
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
  //.....
}

3、配置代理数据源 使用 seata DataSourceProxy代理自己的数据源

因为 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法回滚

添加“com.atguigu.gulimall.order.config.MySeataConfig”类,代码如下:

package com.atguigu.gulimall.order.config;

@Configuration
public class MySeataConfig {

    @Autowired
    DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        //得到数据源
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())){
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }

}

4、每个微服务都必须要导入

  •              registry.conf
    
  •              file.conf
    

分别给gulimall-order和gulimall-ware加上file.conf和registry.conf这两个配置,并修改file.conf

在这里插入图片描述

5、给所有还不使用seata的服务排除掉,修改其pom.xml文件

<exclusion>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</exclusion>


十、RabbitMQ 延时队列

[本项目通过RabbitMQ延时队列 实现 柔性事务+可靠消息+最终一致性发难]

引入:RabbitMq延时队列的目的是为了解决事务最终一致性。

  • 场景:
    比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
  • 常见解决方案:
    spirng的 schedule 定时任务轮训数据库
    • 缺点:
      消耗系统内存、增加了数据库的压力、存在较大的时间误差
    • 解决:
      RabbitMQ的消息TTL和死信Exchange结合

10.1、RabbitMQ相关概念


消息流转过程

10.1.1、消息的TTL(Time To Live)

  • 消息的TTL就是消息的存活时间
  • RabbitMQ 可以对队列消息分别设置TTL.
    • 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
    • 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。

10.1.2、死信队列 DLX 全称(Dead-Letter-Exchange)


  • 死信队列&死信交换器:,称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。

  • 死信消息:

    • 消息被拒绝(Basic.Reject或Basic.Nack)并且设置 requeue 参数的值为 false
    • 消息过期了
    • 队列达到最大的长度
  • 过期消息:
    在 rabbitmq 中存在2种方可设置消息的过期时间,

    • 第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,
      • 队列设置:在队列申明的时候使用 x-message-ttl 参数,单位为 毫秒
    • 第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。
      • 单个消息设置:是设置消息属性的 expiration 参数的值,单位为 毫秒

    如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。

**延时队列:**在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。


10.2、延时队列定时关单模拟


在这里插入图片描述

Order-event-exchange 交换机 绑定了两个队列

  • order.delay.queue队列,绑定的路由键是:order.creare.order
  • order.release.order.queue 队列,绑定的路由键是:order.release.order
  1. 下订单,发送到服务器,消息的路由键是:order.creare.order
  2. 消息发送到 order.delay.queue队列
    • 一分钟后,消息TTL已过期,发送到 Order-event-exchange 交换机,携带路由键是:order.release.order
  3. 消息发送到order.release.order.queue队列,监听该队列

**第一步、**创建相应的交换机、队列、以及交换机和队列的绑定 并 编写一个队列监听

package com.atguigu.gulimall.order.config;

@Configuration
public class MyMQConfig {


    @RabbitListener(queues = "order.release.order.queue")
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息:准备关闭订单!" + entity.getOrderSn());
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    /**
     * Spring中注入Bean之后,容器中的Binding、Queue、Exchange 都会自动创建(前提是RabbitMQ中没有)
     * RabbitMQ 只要有,@Bean属性发生变化也不会覆盖
     * @return
     * Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
     */
    @Bean
    public Queue orderDelayQueue(){
        HashMap<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",30000);

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

    @Bean
    public Queue orderReleaseOrderQueue(){
        return new Queue("order.release.order.queue", true, false, false);
    }

    @Bean
    public Exchange orderEventExchange(){
        // TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        return new TopicExchange("order-event-exchange",true,false);
    }

    @Bean
    public Binding orderCreateOrder(){
        // Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    @Bean
    public Binding orderReleaseOrder(){
        // Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

}

在这里插入图片描述

第二步、编写个Controller用来发送消息

修改“com.atguigu.gulimall.order.web.HelloController”类代码如下:

package com.atguigu.gulimall.order.web;

@Controller
public class HelloController {

    @Autowired
    RabbitTemplate rabbitTemplate;

		// ......
    @ResponseBody
    @GetMapping("/test/createOrder")
    public String createOrderTest(){
        // 此处模拟:省略订单下单成功,并保存到数据库
        OrderEntity entity = new OrderEntity();
        entity.setOrderSn(UUID.randomUUID().toString());
        entity.setModifyTime(new Date());
        // 给MQ发送消息
        rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",entity);
        return "ok";
    }
}


十一、库存自动解锁

在这里插入图片描述

11.1、gulimall-ware 服务添加RabbitMQ

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、添加配置

spring:
  rabbitmq:
    host: 124.222.223.222
    virtual-host: /
    username: guest
    password: guest
    listener:
      simple:
        acknowledge-mode: manual

3、主启动类添加注解

@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {

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

}

11.2、创建业务交换机 & 队列 以及之间的绑定

创建相应的交换机、队列、以及交换机和队列的绑定

gulimall-ware服务中添加“com.atguigu.gulimall.ware.config.MyRabbitConfig”类,代码如下:

package com.atguigu.gulimall.ware.config;

@Configuration
public class MyRabbitConfig {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @RabbitListener(queues = "stock.release.stock.queue")
    public void handle(Message message){

    }

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

    @Bean
    public Exchange exchange(){
        return new TopicExchange("stock-event-exchange", true, false);
    }

    @Bean
    public Queue stockReleaseStockQueue() {
        return new Queue("stock.release.stock.queue", true, false, false);
    }

    @Bean
    public Queue stockDelayQueue() {
        // String name, boolean durable, boolean exclusive, boolean autoDelete,
        //       @Nullable Map<String, Object> arguments
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        arguments.put("x-message-ttl", 120000);
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    @Bean
    public Binding stockReleaseStockBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                new HashMap<>());
    }

    @Bean
    public Binding orderLockedBinding() {
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                new HashMap<>());
    }



}

11.3、监听库存解锁


库存解锁的场景

  1. 下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
  2. 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚;之前锁定的库存就要自动解锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nuTf7P7D-1650102697212)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220414195547133.png)]

11.3.1、环境修改

1、修改gulimall-ware 仓储服务数据库的wms_ware_order_task_detail表结构

在这里插入图片描述

2、修改“com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity”类,代码 如下:

package com.atguigu.gulimall.ware.entity;

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

   /**
    * id
    */
   @TableId
   private Long id;
   /**
    * sku_id
    */
   private Long skuId;
   /**
    * sku_name
    */
   private String skuName;
   /**
    * 购买个数
    */
   private Integer skuNum;
   /**
    * 工作单id
    */
   private Long taskId;
   /**
    * 仓库id
    */
   private Long wareId;
   /**
    * 锁定状态,1-已锁定  2-已解锁  3-扣减
    */
   private Integer lockStatus;

}

3、修改 Mapper文件

修改resources/mapper/ware/WareOrderTaskDetailDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.atguigu.gulimall.ware.dao.WareOrderTaskDetailDao">

   <!-- 可根据自己的需求,是否要使用 -->
    <resultMap type="com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity" id="wareOrderTaskDetailMap">
        <result property="id" column="id"/>
        <result property="skuId" column="sku_id"/>
        <result property="skuName" column="sku_name"/>
        <result property="skuNum" column="sku_num"/>
        <result property="taskId" column="task_id"/>
        <result property="wareId" column="ware_id"/>
        <result property="lockStatus" column="lock_status"/>
    </resultMap>


</mapper>

11.3.2、告诉MQ库存锁定成功

1)、封装给MQ发送的数据 To类

gulimall-conmmon服务 com.atguigu.common.to.mq 路径下编写:StockLockedTo类、StockDetailTo类

package com.atguigu.common.to.mq;

@Data
public class StockLockedTo {
    /**
     * 库存工作单的id
     */
    private Long id;
    /**
     * 工作单详情
     */
    private StockDetailTo detailTo;
}

package com.atguigu.common.to.mq;

/**
 * Data time:2022/4/14 20:21
 * StudentID:2019112118
 * Author:hgw
 * Description: 详情单
 */
@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;
    /**
     * 锁定状态,1-已锁定  2-已解锁  3-扣减
     */
    private Integer lockStatus;
}

2)、编写 告诉MQ库存锁定成功

修改gulimall-ware 服务 com.atguigu.gulimall.ware.service.imp 路径下的 WareSkuServiceImpl 类,代码如下

  1. 保存库存工作单
  2. 保存库存工作单详情
  3. 给MQ发送锁定库存以及详情消息
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
    /**
     * 保存库存工作单的性情
     * 追溯
     */
    // 1、保存库存工作单
    WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
    taskEntity.setOrderSn(vo.getOrderSn());
    orderTaskService.save(taskEntity);

    // 1、每个商品在哪个库存里有库存
    List<OrderItemVo> locks = vo.getLocks();
    List<SkuWareHashStock> collect = locks.stream().map(item -> {
        SkuWareHashStock stock = new SkuWareHashStock();
        Long skuId = item.getSkuId();
        stock.setSkuId(skuId);
        stock.setNum(item.getCount());
        // 查询这个商品在哪里有库存
        List<Long> wareIds = wareSkuDao.listWareIdHashSkuStock(skuId);
        stock.setWareId(wareIds);
        return stock;
    }).collect(Collectors.toList());

    // 2、锁定库存
    for (SkuWareHashStock hashStock : collect) {
        Boolean skuStocked = false;
        Long skuId = hashStock.getSkuId();
        List<Long> wareIds = hashStock.getWareId();
        if (wareIds == null || wareIds.size()==0){
            // 没有任何仓库有这个商品的库存
            throw new NoStockException(skuId);
        }
        // 1、如果每一个商品都锁定成功,将当前商品锁定了几件的的工作单记录发送给MQ
        // 2、如果有一个商品锁定失败,则前面锁定的就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
        //      1、
        for (Long wareId : wareIds) {
            // 成功就返回1,否则就返回0
            Long count = wareSkuDao.lockSkuStock(skuId,wareId,hashStock.getNum());
            if (count == 1){
                skuStocked = true;
                // TODO 告诉MQ库存锁定成功
                // 2、保存库存工作单详情
                WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null,skuId,"",hashStock.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.setDetailTo(stockDetailTo);
                rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
                break;
            } else {
                // 当前仓库锁失败,重试下一个仓库

            }
        }
        if (skuStocked == false){
            // 当前商品所有仓库都没有锁住,其他商品也不需要锁了,直接返回没有库存了
            throw new NoStockException(skuId);
        }
    }

    // 3、运行到这,全部都是锁定成功的
    return true;
}
@Data
class SkuWareHashStock{
    private Long skuId;     // skuid
    private Integer num;    // 锁定件数
    private List<Long> wareId;  // 锁定仓库id
}

11.3.3、监听库存自动解锁


  • 库存自动解锁

    • 1、查询数据库关于这个订单的锁库存消息
      • ,证明库存锁定成功了。
        • 1、没有这个订单。必须解锁
        • 2、有这个订单。不是解锁库存。
          • 订单状态:已取消:解锁库存
          • 订单状态:没取消:不能解锁
      • 没有,库存锁定失败了,库存回滚了。这种情况无需解锁

1)、主体代码封装

gulimall-ware 服务中 com.atguigu.gulimall.ware.listener 路径下 StockReleaseListener

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

    @Autowired
    WareSkuService wareSkuService;

    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {

        System.out.println("收到解锁库存的消息");
        try {
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}
/**
 * 1、库存自动解锁
 *      下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
 * 2、订单失败
 *      锁库存失败,则库存回滚了,这种情况无需解锁
 *      如何判断库存是否锁定失败呢?查询数据库关于这个订单的锁库存消息即可
 *  自动ACK机制:只要解决库存的消息失败,一定要告诉服务器解锁是失败的。启动手动ACK机制
 * @param to
 *
 */
@Override
public void unlockStock(StockLockedTo to) {
    StockDetailTo detail = to.getDetailTo();
    Long detailId = detail.getId();

    /**
     * 1、查询数据库关于这个订单的锁库存消息
     *    有,证明库存锁定成功了。
     *      1、没有这个订单。必须解锁
     *      2、有这个订单。不是解锁库存。
     *          订单状态:已取消:解锁库存
     *          订单状态:没取消:不能解锁
     *    没有,库存锁定失败了,库存回滚了。这种情况无需解锁
     */

    WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
    if (byId != null) {
        Long id = to.getId();   // 库存工作单的Id,拿到订单号
        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 {
        // 无需解锁
    }
}

/**
 * 解库存锁
 *
 * @param skuId        商品id
 * @param wareId       仓库id
 * @param num          解锁数量
 * @param taskDetailId 库存工作单ID
 */
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);
}

2)、编写一个远程方法查询订单的状态

1、编写远程调用 gulimall-order 服务feign接口

gulimall-ware服务中 com.atguigu.gulimall.ware.feign 路径下的 OrderFeignService类,代码如下:

package com.atguigu.gulimall.ware.feign;

@FeignClient("gulimall-order")
public interface OrderFeignService {

    @GetMapping("/order/order/status/{orderSn}")
    R getOrderStatus(@PathVariable("orderSn") String orderSn);

}

2、gulimall-order服务中提供接口

gulimall-order服务中 com.atguigu.gulimall.order.controller 路径下的 OrderController类,代码如下:

@RestController
@RequestMapping("order/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    /**
     * 通过订单号获取订单的详细信息
     * @param orderSn
     * @return
     */
    @GetMapping("/status/{orderSn}")
    public R getOrderStatus(@PathVariable("orderSn") String orderSn){
        OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
        return R.ok().setData(orderEntity);
    }

gulimall-order服务中 com.atguigu.gulimall.order.service.impl 路径下的 OrderServiceImpl类,代码如下:

@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
    OrderEntity order_sn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
    return order_sn;
}

3、本地编写接收信息的VO

gulimall-ware服务中 com.atguigu.gulimall.ware.vo 路径下的 OrderVo类,代码如下:

package com.atguigu.gulimall.ware.vo;

import lombok.Data;

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

/**
 * Data time:2022/4/14 21:05
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */

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

3)、解锁库存方法编写详情

gulimall-ware服务中的 /com/atguigu/gulimall/ware/service/impl/WareSkuServiceImpl.java 路径下 WareSkuServiceImpl.java类的方法

/**
 * 解库存锁
 * @param skuId  商品id
 * @param wareId 仓库id
 * @param num    解锁数量
 * @param taskDetailId  库存工作单ID
 */
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
    wareSkuDao.unlockStock(skuId,wareId,num);
}
void unlockStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);

gulimall-ware服务中的 resources/mapper/ware/WareSkuDao.xml 文件

<update id="unlockStock">
    UPDATE wms_ware_sku SET stock_locked=stock_locked-#{num} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>

4)、由于gulimall-order添加了拦截器,只要使用该服务必须登录才行。因为这边需要远程调用订单,但不需要登录,所以给这个路径放行

修改gulimall-order 服务的 com.atguigu.gulimall.order.interceptoe 路径下 LoginUserInterceptor类

package com.atguigu.gulimall.order.interceptoe;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    /**
     * 用户登录拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     *      用户登录:放行
     *      用户未登录:跳转到登录页面
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // /order/order/status/222222222
        String uri = request.getRequestURI();
        boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
        if (match){
            return true;
        }

        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute!=null){
            loginUser.set(attribute);
            return true;
        } else {
            // 没登录就去登录
            request.getSession().setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.cn/login.html");
            return false;
        }
    }
}

11.4、代码整理


1)、创建一个类监听 stock.release.stock.queue 队列

gulimall-ware服务的 com.atguigu.gulimall.ware.listener 路径 StockReleaseListener 类,接收到消息之后调用 Service层 WareSkuServiceImpl.java 实现类的 unlockStock 方法实现解锁库存:

  1. 没有异常捕捉,则成功解锁消息。手动ACK
  2. 捕捉到异常,则 消息拒绝以后重新放到队列里面,让别人继续消费解锁
package com.atguigu.gulimall.ware.listener;

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

    @Autowired
    WareSkuService wareSkuService;

    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {

        System.out.println("收到解锁库存的消息");
        try {
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

2)、service层业务方法

gulimall-ware服务的 com.atguigu.gulimall.ware.service.impl 路径 WareSkuServiceImpl 类

/**
 * 1、库存自动解锁
 *      下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
 * 2、订单失败
 *      锁库存失败,则库存回滚了,这种情况无需解锁
 *      如何判断库存是否锁定失败呢?查询数据库关于这个订单的锁库存消息即可
 *  自动ACK机制:只要解决库存的消息失败,一定要告诉服务器解锁是失败的。启动手动ACK机制
 * @param to
 *
 */
@Override
public void unlockStock(StockLockedTo to) {
    StockDetailTo detail = to.getDetailTo();
    Long detailId = detail.getId();

    /**
     * 1、查询数据库关于这个订单的锁库存消息
     *    有,证明库存锁定成功了。
     *      1、没有这个订单。必须解锁
     *      2、有这个订单。不是解锁库存。
     *          订单状态:已取消:解锁库存
     *          订单状态:没取消:不能解锁
     *    没有,库存锁定失败了,库存回滚了。这种情况无需解锁
     */

    WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
    if (byId != null) {
        Long id = to.getId();   // 库存工作单的Id,拿到订单号
        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 {
        // 无需解锁
    }
}

/**
 * 解库存锁
 *
 * @param skuId        商品id
 * @param wareId       仓库id
 * @param num          解锁数量
 * @param taskDetailId 库存工作单ID
 */
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);
}


十二、定时关单

  1. 在订单创建成功时向MQ中 延时队列发送消息,携带路由键:order.create.order
    • 30分钟后未支付,则释放订单服务 向MQ中发送消息,携带路由键:order.release.order
      • 监听该order.release.order.queue 队列,进行释放订单服务
  2. 此时存在一种情况,存在订单创建成功之后出现延时卡顿,消息延迟,导致订单解锁在库存解锁之后完成
    • 则每次库存解锁之后 向MQ中发送消息,携带路由键:order.release.other
      • 监听 stock.release.stock.queue ,编写一个重载方法,进行判断
        • 查一下最新库存的状态,防止重复解锁库存
          • 按照工作单找到所有 没有解锁的库存,进行解锁

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

12.1、创建交换机、队列以及之间的绑定

package com.atguigu.gulimall.order.config;

@Configuration
public class MyMQConfig {


    /**
     * Spring中注入Bean之后,容器中的Binding、Queue、Exchange 都会自动创建(前提是RabbitMQ中没有)
     * RabbitMQ 只要有,@Bean属性发生变化也不会覆盖
     * @return
     * Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
     */
    @Bean
    public Queue orderDelayQueue(){
        HashMap<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",30000);

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

    @Bean
    public Queue orderReleaseOrderQueue(){
        return new Queue("order.release.order.queue", true, false, false);
    }

    @Bean
    public Exchange orderEventExchange(){
        // TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
        return new TopicExchange("order-event-exchange",true,false);
    }

    @Bean
    public Binding orderCreateOrder(){
        // Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    @Bean
    public Binding orderReleaseOrder(){
        // Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

    /**
     * 订单释放直接和库存释放进行绑定
     * @return
     */
    @Bean
    public Binding orderReleaseOtherBingding(){
        // Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }

}

12.2、在订单创建成功时向MQ中 延时队列发送消息

在这里插入图片描述

12.3、在订单的关闭之后时向MQ发送消息

为了防止因为其他原因,订单的关闭延期了

/**
 * 订单的关闭
 * @param entity
 */
@Override
public void closeOrder(OrderEntity entity) {
    // 1、查询当前这个订单的最新状态
    OrderEntity orderEntity = this.getById(entity.getId());
    if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
        // 2、关单
        OrderEntity update = new OrderEntity();
        update.setId(entity.getId());
        update.setStatus(OrderStatusEnum.CANCLED.getCode());
        this.updateById(update);
        OrderTo orderTo = new OrderTo();
        BeanUtils.copyProperties(orderEntity, orderTo);
        // 3、发给MQ一个
        rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
    }
}

12.4、监听 order.release.order.queue 队列,释放订单服务

  1. gulimall-order 服务的 com.atguigu.gulimall.order.listener 路径下的 OrderClassListener类。
package com.atguigu.gulimall.order.listener;

@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderClassListener {

    @Autowired
    OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息:准备关闭订单!" + entity.getOrderSn());
        try {
            orderService.closeOrder(entity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
}
  1. Service层中 OrderServiceImpl.java 实现类进行订单的关闭
/**
 * 订单的关闭
 * @param entity
 */
@Override
public void closeOrder(OrderEntity entity) {
    // 1、查询当前这个订单的最新状态
    OrderEntity orderEntity = this.getById(entity.getId());
    if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
        // 2、关单
        OrderEntity update = new OrderEntity();
        update.setId(entity.getId());
        update.setStatus(OrderStatusEnum.CANCLED.getCode());
        this.updateById(update);
        OrderTo orderTo = new OrderTo();
        BeanUtils.copyProperties(orderEntity, orderTo);
        // 3、发给MQ一个
        rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderEntity);
    }
}

12.5、监听 stock.release.stock.queue 队列,进行解锁

在 gulimall-ware 服务中,进行监听处理

1)、编写 StockReleaseListener 进行监听队列

package com.atguigu.gulimall.ware.listener;

import com.atguigu.common.to.mq.OrderTo;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.gulimall.ware.service.WareSkuService;
import com.rabbitmq.client.Channel;
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;

/**
 * Data time:2022/4/14 21:47
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {

    @Autowired
    WareSkuService wareSkuService;

    /**
     * 库存自己过期处理
     * @param to
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {

        System.out.println("收到解锁库存的消息");
        try {
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    /**
     * 订单关闭处理
     * @param orderTo
     * @param message
     * @param channel
     * @throws IOException
     */
    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {

        System.out.println("订单关闭准备解锁库存");
        try {
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

2、Service层 WareSkuServiceImpl 实现类中,进行方法处理

/**
 * 防止订单服务卡顿,导致订单状态一直修改不了,库存消息优先到期。查订单状态肯定是新建状态,什么都不做就走了
 * 导致卡顿的订单,永远不能解锁库存
 * @param orderTo
 */
@Transactional
@Override
public void unlockStock(OrderTo orderTo) {
    String orderSn = orderTo.getOrderSn();
    // 查一下最新库存的状态,防止重复解锁库存
    WareOrderTaskEntity task = orderTaskService.getOrderTeskByOrderSn(orderSn);
    Long taskId = task.getId();
    // 按照工作单找到所有 没有解锁的库存,进行解锁
    List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
            .eq("task_id", taskId)
            .eq("lock_status", 1));
    // 进行解锁
    for (WareOrderTaskDetailEntity entity : entities) {
        unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
    }

3、编写查询最新库存的状态,防止重复解锁库存

package com.atguigu.gulimall.ware.service.impl;

@Service("wareOrderTaskService")
public class WareOrderTaskServiceImpl extends ServiceImpl<WareOrderTaskDao, WareOrderTaskEntity> implements WareOrderTaskService {

//.....
    @Override
    public WareOrderTaskEntity getOrderTeskByOrderSn(String orderSn) {

        WareOrderTaskEntity one = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        return one;
    }

}

4、消息共享封装To

package com.atguigu.common.to.mq;

@Data
public class OrderTo {
    /**
     * id
     */
    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. 做好消息确认机制(pulisher、consumer[手动ACK])
    2. 每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍

13.1、如何保证消息可靠性-消息丢失


  • 消息发送出去,由于网络问题没有抵达服务器
    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式;
    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录;
    • 做好定期重发,如果消息没有发生成,定期去数据库扫描未成功的消息进行重发;
  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机
    • Publisher 也必须加入确认回调机制,确认成功的消息,修改数据库消息状态
  • 自动ACK的状态下。消费者收到消息,但没来得及消费然后宕机
    • 一定开启手动ACK,消费成功才移除,失败或者没来得处理就noAck并重新入队
* 1、做好消息确认机制(pulisher、consumer[手动ACK]* 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍

13.2、如何保证消息可靠性-消息重复


在这里插入图片描述

13.3、如何保证消息可靠性-消息积压


在这里插入图片描述



十四、支付[支付宝]

14.1、支付宝配置相关概念


  1. 公钥 私钥
    • 公钥和私钥时一个相对概念
    • 他们的公私性是相对于生产者来说的
      • 一对密钥生成后,保存在生产者手里的就是私钥
      • 生成者发布出去大家用的就是公钥

在这里插入图片描述

此时:

  • 密钥A 和 密钥C 就是私钥
  • 密钥B 和 密钥D 就是公钥
  1. 加密 和 数字签名

在这里插入图片描述

获取支付宝的沙箱环境中商户的公钥匙、私钥、以及支付宝的公钥
在这里插入图片描述

14.2、环境准备-内网穿透常用软件和安装


在这里插入图片描述

续断:https://www.zhexi.tech/

第一步:登录

第二步:安装客户端下载链接 ,Mac安装教程:视频

第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;

在这里插入图片描述

第四步、新建隧道
后面操作的时候会说



14.2、环境准备-内网穿透常用软件和安装


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v6n7UuOx-1650103370483)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220415140932328.png)]

续断:https://www.zhexi.tech/

第一步:登录

第二步:安装客户端下载链接 ,Mac安装教程:视频

第三步:登录哲西云控制台,到“客户端”菜单,确认客户端已上线;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qziWABOV-1650103370483)(谷粒商城-分布式高级篇[商城业务-订单服务].assets/image-20220415141404548.png)]

第四步、新建隧道


14.3、支付整合服务


第一步、导入依赖

第一步:导入依赖

<!--阿里支付模块-->
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.9.28.ALL</version>
</dependency>

第二步、抽取支付工具类并进行配置

第二步、抽取支付工具类并进行配置

成功调用该接口后,返回的数据就是支付页面的html,因此后续会使用@ResponseBody

1)、编写配置类

gulimall-order 服务的 com.atguigu.gulimall.order.config 路径下的 AlipayTemplate 配置类:

package com.atguigu.gulimall.order.config;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.atguigu.gulimall.order.vo.PayVo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Data time:2022/4/15 14:52
 * StudentID:2019112118
 * Author:hgw
 * Description: 支付宝沙箱测试配置类
 */
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
    //在支付宝创建的应用的id
    private   String app_id = "2021000119667766";

    // 商户私钥,您的PKCS8格式RSA2私钥
    private  String merchant_private_key = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpzlIqpnBAEWCTIr4SaNrpQId9P2KTpJ7Pa2aGoElXYefW9BSlqv+oGz5hLn8VANNJAwjwazgDbIuaaTJ8yYYjWh1wxNf+c4iRDNpBb1qf9GZhrW4l/HGH6XCzFlRpo40CCCDBfgl+U7DLbI4h+4KXokEue6ALWsXBVTYLFTyxpsBC3UOauZUzccvHeczD746psV1oiYounxs4QWJrTBJtxWgRnD+mLPPtk79WjwYJAr0dYLACZVC6Bdi06khEgxnrbu3NJYH2qMts623cygQ0SWk2ZtRWhEwmpQ/Dd2ilsQKGQErkK5mVUmsuo87K4Z6Rq333B5Fleb4TLTVuWd+ZAgMBAAECggEAY/3a5MKd1xxkgkAzLSQRxMj7AAYTRl3qJrpX5W79wTcmDq4semH3qkZgtVlr/DJAOP5QhUKd+WYxzvujf1gsZSTrsTw49N2TzdaDr4SjGQ4SO/Kkqjm9oQsWEl9T1eE5Z7jhkQ9nB7zAnwmNqPUyMZiaSYUC+ay6Rt6mtGANHY7cFmLeBoz93W8V0ClhY2EwKMnyF/QlgR1no3qbjCMRsMHjkJoQBbswhoGpRSIbQidekB9cO71EFW/5dJV87W42UfoNQe4K61yhgWgrCHpVn9rFSdGIX0n8A48X/cPgYVu9Cm3GURoT71ePg1P++SwQGYnkO6xkR8PFMnpqpmDjAQKBgQDwkptaa+vWaFpGH6eSucYEZGMmdwyMiOfZo5kXgphMbd/nEyG0U0o2dtgWG1WbeS714PZgwgNnlidIOeNs0JDocamIAYSVgXdfZTPPJuyvUFVFbQGftPWGKSj90vWxHNS3oph6Yw41Del7UkqxFt1knDwycd4Amfo5nSEoeGSR0wKBgQC0sfZrPjn5hgSAkOazX+cugMfhYpZ69YLEVZkD9yVadV2D19xlwMSpf713g77ux2zSVKDIERbEgVkopjssGM64DgpkH7gwN18v+F6YcbCOB7gIX4hScykgVASXKTXyuUjGLaqs2aEuH/ULRxj+n8cH00x/RxGURROY52ciZRy5YwKBgH51cnh7loMkY5/M7/du9CpG4t/LYKtXJBkBqG31Vj2G3FXJdsQlrDMpEbm9MKkDcK4LTTfbhJKlGY0b8PK4SBQH+4fk1F8KqUdaGXvhCDW30rsl696Z7x5Q8J1MkZ5Ce4b0T5a2DzfQUlVjEqQ4UrSadAJIXNyQFDrI4C836hXFAoGAP0jo3gyQL3UhlImrUv1usVnHJ4fo3i2oW+0Cx2HCwljCpM9wUG7gMeEcUYRh1a0gztV27jsV90K6IEOAC+SwWcQJHaICV1i9TMa3ErsWs9e+O6iBzSaqK7lhVjPHwjfkZgxOb3VVPxtQLl/7QApjobj+XMFeRcifoXjCJUi2c7MCgYEAjpVdOAsZwrHI0YCuUU6xLHTagP9u80EMCr6D7xvfpimS1EL2wHxNwhZGzpTitFj25e8yTA27S2SYeHLvKBIaejclT1TY/Y9PSqmcvl2CCSB2hBw8gbxuBt607eObuR4fxt+/C11K9GAj6Dca+1pmoGfV0a0OPTfcESHQxXM89hg=";
    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private  String alipay_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyay7fjd6Yf2pyGWQK/efrZggwUBE2fEvDoZ1+q6P21d86Rcbzz4lL2qrl4UPgChOE29SNRCKDnWIthXCBMTtRcf6SxCtaldp7D+1uXBmzxoEzKQ2lDjep+pnFMpoA+3CB4nOArL7B2hThh/F2ofbEDK0IJJVXWksZSSjfsKQm0+BXcrYMWe6khcf2S9NnBSnMB1bnHmnMK69oObsg8/dBp6cHruFoMu8OGCMIDO0Z6W7hoywzkf3K08VrqxGOhM5p94oGSBQJcD2CclK8c5wvHFMZm0wtmxWkyY2zdQ84stGJnLhX9ORmfHo/HBeXX8xPGF91SZU1yZ0gmatQZK91QIDAQAB";
    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
    private  String notify_url="http://**.natappfree.cc/payed/notify";

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    private  String return_url="http://member.gulimall.cn/memberOrder.html";

    // 签名方式
    private  String sign_type = "RSA2";

    // 字符编码格式
    private  String charset = "utf-8";

    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    private  String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";

    public  String pay(PayVo vo) throws AlipayApiException {

        //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
        //1、根据支付宝的配置生成一个支付客户端
        AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key, "json",
                charset, alipay_public_key, sign_type);

        //2、创建一个支付请求 //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(return_url);
        alipayRequest.setNotifyUrl(notify_url);

        //商户订单号,商户网站订单系统中唯一订单号,必填
        String out_trade_no = vo.getOut_trade_no();
        //付款金额,必填
        String total_amount = vo.getTotal_amount();
        //订单名称,必填
        String subject = vo.getSubject();
        //商品描述,可空
        String body = vo.getBody();

        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        String result = alipayClient.pageExecute(alipayRequest).getBody();

        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:"+result);

        return result;

    }
}

2)、提交信息封装VO

package com.atguigu.gulimall.order.vo;

import lombok.Data;

@Data
public class PayVo {
    private String out_trade_no; // 商户订单号 必填
    private String subject; // 订单名称 必填
    private String total_amount;  // 付款金额 必填
    private String body; // 商品描述 可空
}

3)、因为加上了@ConfigurationProperties(prefix = "alipay"),是一个配置类

我们可以在application.yaml 配置文件中编写相关的配置

# 支付宝相关的配置
alipay:
  app-id: 2021000119667766

第三步、修改支付页的支付宝按钮

第三步、修改支付页的支付宝按钮

修改 gulimall-order 服务中的 pay.html 页面

<li>
        <img src="/static/order/pay/img/zhifubao.png" style="weight:auto;height:30px;" alt="">
        <a th:href="'http://order.gulimall.cn/payOrder?orderSn='+${submitOrderResp.order.orderSn}">支付宝</a>
      </li>

第四步、订单支付与同步通知

第四步、订单支付与同步通知

1)、编写Controller层接口调用
gulimall-ordert 服务的 com.atguigu.gulimall.order.web 路径下的 PayWebController 类,映射/payOrder

package com.atguigu.gulimall.order.web;

@Controller
public class PayWebController {

    @Autowired
    AlipayTemplate alipayTemplate;

    @Autowired
    OrderService orderService;

    /**
     * 1、将支付页让浏览器展示
     * 2、支付成功后,跳转到用户的订单列表项
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */
    @ResponseBody
    @GetMapping(value = "/payOrder",produces = "text/html")
    public String payOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {
        PayVo payVo = orderService.getOrderPay(orderSn);
        // 返回的是一个页面。将此页面交给浏览器就行
        String pay = alipayTemplate.pay(payVo);
        System.out.println(pay);
        return pay;
    }
}

2)、Service层实现类 OrderServiceImpl.java 编写获取当前订单的支付信息 方法

/**
 * 获取当前订单的支付信息
 * @param orderSn
 * @return
 */
@Override
public PayVo getOrderPay(String orderSn) {
    PayVo payVo = new PayVo();
    OrderEntity order = this.getOrderByOrderSn(orderSn);

    BigDecimal decimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
    payVo.setTotal_amount(decimal.toString());
    payVo.setOut_trade_no(order.getOrderSn());

    List<OrderItemEntity> order_sn = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
    OrderItemEntity itemEntity = order_sn.get(0);
    payVo.setSubject(itemEntity.getSkuName());
    payVo.setBody(itemEntity.getSkuAttrsVals());
    return payVo;
}

测试成功:

在这里插入图片描述


第五步、订单列表页渲染完成


第五步、订单列表页渲染完成

5.1、环境准备
  1. 首先将资料中 订单页 静态资源部署到服务器中,让页面放到gulimall-member服务中。

  2. 配置网关

            - id: gulimall_member_route
              uri: lb://gulimall-member
              predicates:
                - Host=member.gulimall.cn
    
  3. 添加域名映射

    # Gulimall Host Start
    127.0.0.1 gulimall.cn
    127.0.0.1 search.gulimall.cn
    127.0.0.1 item.gulimall.cn
    127.0.0.1 auth.gulimall.cn
    127.0.0.1 cart.gulimall.cn
    127.0.0.1 order.gulimall.cn
    127.0.0.1 member.gulimall.cn
    "/etc/hosts"
    
  4. 整合SpringSession

    1. 导入依赖

      <!-- 整合SpringSession完成Session共享问题-->
      <dependency>
          <groupId>org.springframework.session</groupId>
          <artifactId>spring-session-data-redis</artifactId>
      </dependency>
      <!--引入Redis-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
          <exclusions>
              <exclusion>
                  <groupId>io.lettuce</groupId>
                  <artifactId>lettuce-core</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
      <dependency>
          <groupId>redis.clients</groupId>
          <artifactId>jedis</artifactId>
      </dependency>
      
    2. 编写配置

      spring:
        session:
          store-type: redis
        redis:
          host: 124.222.223.222
      
    3. 启动类加上注解

      @EnableRedisHttpSession
      @EnableFeignClients(basePackages = "com.atguigu.gulimall.member.feign")
      @EnableDiscoveryClient
      @SpringBootApplication
      public class GulimallMemberApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(GulimallMemberApplication.class, args);
          }
      
      }
      
  5. 配置拦截器(这里复制的同时一定要修改 放行:/member/member/**

    1. 用户登录拦截器

      package com.atguigu.gulimall.member.interceptoe;
      
      import com.atguigu.common.constant.AuthServerConstant;
      import com.atguigu.common.vo.MemberRespVo;
      import org.springframework.stereotype.Component;
      import org.springframework.util.AntPathMatcher;
      import org.springframework.web.servlet.HandlerInterceptor;
      
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      
      /**
       * Data time:2022/4/11 22:21
       * StudentID:2019112118
       * Author:hgw
       * Description: 用户登录拦截器
       */
      @Component
      public class LoginUserInterceptor implements HandlerInterceptor {
      
          public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
      
          /**
           * 用户登录拦截器
           * @param request
           * @param response
           * @param handler
           * @return
           *      用户登录:放行
           *      用户未登录:跳转到登录页面
           * @throws Exception
           */
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      
              // /order/order/status/222222222
              String uri = request.getRequestURI();
              boolean match = new AntPathMatcher().match("/member/**", uri);
              if (match){
                  return true;
              }
      
              MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
              if (attribute!=null){
                  loginUser.set(attribute);
                  return true;
              } else {
                  // 没登录就去登录
                  request.getSession().setAttribute("msg", "请先进行登录");
                  response.sendRedirect("http://auth.gulimall.cn/login.html");
                  return false;
              }
          }
      }
      
    2. 编写Web配置类,指定用户登录拦截器

      package com.atguigu.gulimall.member.config;
      
      import com.atguigu.gulimall.member.interceptoe.LoginUserInterceptor;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
      /**
       * Data time:2022/4/15 17:03
       * StudentID:2019112118
       * Author:hgw
       * Description: Web配置
       */
      @Configuration
      public class MemberWebConfig implements WebMvcConfigurer {
      
          @Autowired
          LoginUserInterceptor loginUserInterceptor;
      
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
          }
      }
      
5.2、接口编写

第一步、gulimall-order 服务中编写 分页查询当前登录用户的所有订单 接口方法

1)、在Controller层编写

gulimall-order 服务中/src/main/java/com/atguigu/gulimall/order/controller OrderController.java

package com.atguigu.gulimall.order.controller;

@RestController
@RequestMapping("order/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    /**
     * 分页查询当前登录用户的所有订单
     */
    @PostMapping("/listWithItem")
    public R listWithItem(@RequestBody Map<String, Object> params){
        PageUtils page = orderService.queryPageWithItem(params);

        return R.ok().put("page", page);
    }

2)、Service层 OrderServiceImpl.java实现类方法编写:

gulimall-order 服务中 com/atguigu/gulimall/order/service/impl OrderServiceImpl.java

@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {

    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

    IPage<OrderEntity> page = this.page(
            new Query<OrderEntity>().getPage(params),
            new QueryWrapper<OrderEntity>().eq("member_id", memberRespVo.getId()).orderByDesc("modify_time")
    );

    List<OrderEntity> order_sn = page.getRecords().stream().map(order -> {
        List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));
        order.setItemEntities(itemEntities);
        return order;
    }).collect(Collectors.toList());

    page.setRecords(order_sn);

    return new PageUtils(page);
}

3)、修改 OrderEntity.java 实体类,为其加上一个属性

gulimall-order 服务 com.atguigu.gulimall.order.entity 路径下的 OrderEntity类,添加以下属性:

@TableField(exist = false)
private List<OrderItemEntity> itemEntities;

第二步、在gulimall-member服务中调用 gulimall-order 服务接口

gulimall-member 服务的 com.atguigu.gulimall.member.feign 路径下的 OrderFeignService接口进行远程调用:

package com.atguigu.gulimall.member.feign;

import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Map;

/**
 * Data time:2022/4/15 20:46
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@FeignClient("gulimall-order")
public interface OrderFeignService {

    @PostMapping("/order/order/listWithItem")
    R listWithItem(@RequestBody Map<String, Object> params);
}

第三步、编写过滤器

package com.atguigu.gulimall.member.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.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * Data time:2022/4/12 11:20
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@Configuration
public class GulimallFeignConfig {

    /**
     * feign在远程调用之前会执行所有的RequestInterceptor拦截器
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate requestTemplate) {
                // 1、使用 RequestContextHolder 拿到请求数据,RequestContextHolder底层使用过线程共享数据 ThreadLocal<RequestAttributes>
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes!=null){
                    HttpServletRequest request = attributes.getRequest();
                    // 2、同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给新请求同步了老请求的cookie
                    requestTemplate.header("Cookie",cookie);
                }
            }
        };
    }
}

第四步、Controller 层 MemberWebController 编写

package com.atguigu.gulimall.member.web;

import com.alibaba.fastjson.JSON;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.member.feign.OrderFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;
import java.util.Map;

/**
 * Data time:2022/4/15 17:00
 * StudentID:2019112118
 * Author:hgw
 * Description:
 */
@Controller
public class MemberWebController {

    @Autowired
    OrderFeignService orderFeignService;

    @GetMapping("/memberOrder.html")
    public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
                                  Model model){
        // 查处当前登录的用户的所有订单列表数据
        Map<String,Object> page = new HashMap<>();
        page.put("page",pageNum.toString());
        R r = orderFeignService.listWithItem(page);
        System.out.println(JSON.toJSONString(r));
        model.addAttribute("orders",r);

        return "orderList";
    }
}
5.3、前端页面接收渲染

修改 orderList.html 页面的部分内容,渲染页面

<table class="table" th:each="order:${orders.page.list}">
    <tr>
        <td colspan="7" style="background:#F7F7F7">
            <span style="color:#AAAAAA">2017-12-09 20:50:10</span>
            <span><ruby style="color:#AAAAAA">订单号:</ruby> [[${order.orderSn}]]</span>
            <span>谷粒商城<i class="table_i"></i></span>
            <i class="table_i5 isShow"></i>
        </td>
    </tr>
    <tr class="tr" th:each="item,itemStat:${order.itemEntities}">
        <td colspan="3">
            <img style="height: 60px; width: 60px;" th:src="${item.skuPic}" alt="" class="img">
            <div>
                <p style="width: 242px; height: auto; overflow: auto">
                    [[${item.skuName}]]
                </p>
                <div><i class="table_i4"></i>找搭配</div>
            </div>
            <div style="margin-left:15px;">x[[${item.skuQuantity}]]</div>
            <div style="clear:both"></div>
        </td>
        <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td>
        <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}" style="padding-left:10px;color:#AAAAB1;">
            <p style="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p>
            <hr style="width:90%;">
            <p>在线支付</p>
        </td>
        <td th:if="${itemStat.index==0}" th:rowspan="${itemStat.size}">
            <ul>
                <li style="color:#71B247;" th:if="${order.status==0}">待付款</li>
                <li style="color:#71B247;" th:if="${order.status==1}">已付款</li>
                <li style="color:#71B247;" th:if="${order.status==2}">已发货</li>
                <li style="color:#71B247;" th:if="${order.status==3}">已完成</li>
                <li style="color:#71B247;" th:if="${order.status==4}">已取消</li>
                <li style="color:#71B247;" th:if="${order.status==5}">售后中</li>
                <li style="color:#71B247;" th:if="${order.status==6}">售后完成</li>
                <li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟踪<i class="table_i3"></i>
                    <div class="hi">
                        <div class="p-tit">
                            普通快递 运单号:390085324974
                        </div>
                        <div class="hideList">
                            <ul>
                                <li>
                                    [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                                    的快件已签收,感谢您使用韵达快递)签收
                                </li>
                                <li>
                                    [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                                    的快件已签收,感谢您使用韵达快递)签收
                                </li>
                                <li>
                                    [北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
                                </li>
                                <li>
                                    [北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
                                </li>
                            </ul>
                        </div>
                    </div>
                </li>
                <li class="tdLi">订单详情</li>
            </ul>
        </td>
        <td>
            <button>确认收货</button>
            <p style="margin:4px 0; ">取消订单</p>
            <p>催单</p>
        </td>
    </tr>
</table>

在这里插入图片描述


第六步、异步通知内网穿透环境搭建


  • 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态
  • 由于同步跳转可能由于网络问题失败,所以使用异步通知
  • 支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回success

1)、建立内网穿透

在这里插入图片描述

2)内网穿透设置异步通知地址

  • 将外网映射到本地的order.gulimall.cn:80

  • 由于回调的请求头不是order.gulimall.cn,因此nginx转发到网关后找不到对应的服务,所以需要对nginx进行设置

    /payed/notify异步通知转发至订单服务

设置异步通知的地址:

在这里插入图片描述

3)、内网穿透联调

在这里插入图片描述

通过工具进行内网穿透,第三方并不是从浏览器发送过来请求,即使是从浏览中发送过来,那host也不对,故我们需要修改nginx的配置,来监听 /payed/notify 请求,设置默认的host

服务器的 mydata/nginx/conf/conf.d 目录下

hgw@HGWdeAir conf.d % vim gulimall.conf 
server {
    listen       80;
    server_name  gulimall.cn  *.gulimall.cn mvaophzk6b.51xd.pub;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;
    location /static/ {
        root /usr/share/nginx/html;
    }

    location /payed/  {
        proxy_set_header Host order.gulimall.cn;
        proxy_pass http://gulimall;
    }

    location / {
        proxy_set_header Host $host;
        proxy_pass http://gulimall;
    }

在这里插入图片描述

4)、编写登录拦截器方法,放行/payed/notify

修改gulimall-order服务 com.atguigu.gulimall.order.interceptoe 路径的 LoginUserInterceptor 类,代码如下:

package com.atguigu.gulimall.order.interceptoe;

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // /order/order/status/222222222
        String uri = request.getRequestURI();
        AntPathMatcher matcher = new AntPathMatcher();
        boolean match = matcher.match("/order/order/status/**", uri);
        boolean match1 = matcher.match("/payed/notify", uri);
        if (match || match1 ){
            return true;
        }

        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute!=null){
            loginUser.set(attribute);
            return true;
        } else {
            // 没登录就去登录
            request.getSession().setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.cn/login.html");
            return false;
        }
    }
}

进行测试接口:测试成功

在这里插入图片描述


第七步、验证签名,支付成功


  • 验证签名
    • 验签通过,即是支付宝发过来的数据,处理支付结果
      1. 保存交易流水 oms_payment_info
      2. 修改订单的状态信息 oms_order
      3. 返回"success"
    • 验签通过,即不是支付宝发送过来的数据
      • 返回非 “success”,即可
1、主体代码,Controller层接口编写

主体代码,Controller层接口编写

package com.atguigu.gulimall.order.listener;

import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.atguigu.gulimall.order.config.AlipayTemplate;
import com.atguigu.gulimall.order.service.OrderService;
import com.atguigu.gulimall.order.vo.PayAsyncVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * Data time:2022/4/15 21:54
 * StudentID:2019112118
 * Author:hgw
 * Description: 接收支付宝的异步通知
 */
@RestController
public class OrderPayedListener {

    @Autowired
    OrderService orderService;

    @Autowired
    AlipayTemplate alipayTemplate;

    @PostMapping("/payed/notify")
    public String handleAliPayed(PayAsyncVo vo,HttpServletRequest request) throws AlipayApiException {
        // 只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success,支付宝就再也不通知

        // 验签
        Map<String, String> params = new HashMap<>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (String name : requestParams.keySet()) {
            String[] values = requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }
        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
                alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名

        if (signVerified) {
            System.out.println("签名验证成功....");
            String result = orderService.handlePayRequest(vo);
            return result;
        } else {
            System.out.println("签名验证失败....");
            return "error";
        }

    }
}
2、处理支付结果

1、编写一个实体类Vo 用来映射支付宝异步通知回来的数据

gulimall-order 服务的 com.atguigu.gulimall.order.vo 路径下的 PayAsyncVo 类

package com.atguigu.gulimall.order.vo;

import lombok.Data;
import lombok.ToString;

@ToString
@Data
public class PayAsyncVo {

    private String gmt_create;
    private String charset;
    private String gmt_payment;
    private String notify_time;
    private String subject;
    private String sign;
    private String buyer_id;//支付者的id
    private String body;//订单的信息
    private String invoice_amount;//支付金额
    private String version;
    private String notify_id;//通知id
    private String fund_bill_list;
    private String notify_type;//通知类型; trade_status_sync
    private String out_trade_no;//订单号
    private String total_amount;//支付的总额
    private String trade_status;//交易状态  TRADE_SUCCESS
    private String trade_no;//流水号
    private String auth_app_id;//
    private String receipt_amount;//商家收到的款
    private String point_amount;//
    private String app_id;//应用id
    private String buyer_pay_amount;//最终支付的金额
    private String sign_type;//签名类型
    private String seller_id;//商家的id

}

2)、设置 表oms_payment_info 的索引

因为一个订单对应一个流水号,所以我们给订单号和支付流水号加上两个唯一索引:

在这里插入图片描述

并修改 order_sn 属性的长度为 64位

3)、Service 层实现类 OrderServiceImpl.java 类编写 处理支付宝的支付结果 方法

gulimall-order 服务的 com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java 路径下的 OrderServiceImpl.java

/**
 * 处理支付宝的支付结果
 * @param vo
 * @return
 */
@Override
public String handlePayRequest(PayAsyncVo vo) {
    // 1、保存交易流水 oms_payment_info
    PaymentInfoEntity infoEntity = new PaymentInfoEntity();
    infoEntity.setAlipayTradeNo(vo.getTrade_no());
    infoEntity.setOrderSn(vo.getOut_trade_no());
    infoEntity.setPaymentStatus(vo.getTrade_status());
    infoEntity.setCallbackTime(vo.getNotify_time());
    paymentInfoService.save(infoEntity);

    // 2、修改订单的状态信息 oms_order
    // 判断支付是否成功:支付宝返回 TRADE_SUCCESS、TRADE_FINISHED 都表示成功
    if (vo.getTrade_status().equals("TRADE_SUCCESS") || vo.getTrade_status().equals("TRADE_FINISHED")) {
        // 支付成功状态,则修改订单的状态
        String outTradeNo = vo.getOut_trade_no();
        this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
    }
    return "success";
}

4)、修改订单的状态信息方法编写 oms_order

gulimall-order 服务的 com.atguigu.gulimall.order.dao 路径下的 OrderDao

package com.atguigu.gulimall.order.dao;

@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {

    void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}

gulimall-order 服务的 gulimall-order/src/main/resources/mapper/order/OrderDao.xml

<update id="updateOrderStatus">
    UPDATE oms_order SET `status`=#{code} WHERE order_sn=#{outTradeNo};
</update>

第八步、关单处理


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

在这里插入图片描述

谷粒商城-分布式高级篇[商城业务-秒杀服务]

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

谷粒商城-分布式高级篇[商城业务-订单服务] 的相关文章

  • amqp 与 amqplib - 哪个 Node.js amqp 客户端库更好?

    这些 amqp 客户端库之间有什么区别 哪一款最值得推荐 主要区别是什么 我会推荐amqp node https github com squaremo amqp node and bramqp https github com bakke
  • 使用spring-amqp和rabbitmq实现带退避的非阻塞重试

    我正在寻找一种使用 spring amqp 和 Rabbit MQ 的退避策略来实现重试的好方法 但要求是侦听器不应被阻止 因此可以自由地处理其他消息 我在这里看到了类似的问题 但它不包括 后退 的解决方案 RabbitMQ 和 Sprin
  • 在 RabbitMQ 主题交换中路由与模式不匹配的消息

    两个队列绑定到具有以下路由键的主题交换 队列 A 与路由键模式匹配绑定 foo队列 B 与路由键模式匹配绑定 bar 我想向此交换添加第三个队列 该队列接收的消息都不是foo消息也不bar消息 如果我用一个绑定这个队列 路由密钥 我自然会得
  • 组在 RabbitMQ 中接收消息,最好使用 Spring AMQP?

    我正在从服务 S 接收消息 该服务将每个单独的属性更改作为单独的消息发布到实体 一个人为的例子是这样的实体 Person id 123 name Something address 如果姓名和地址在同一交易中更新 则 S 将发布两条消息 P
  • 如何在 celery 内为每个用户生成队列?

    因此 我尝试将 Web 请求中的阻塞内容移至后台任务并利用队列 我对消息传递和发布 订阅也很陌生 用户将数据推送到那里并进行处理 稍后用户会收到相关通知 我为此做了一个 celery 设置 发现它不能满足我为每个用户分配自己的任务的专用队列
  • 与 RabbitMQ 相比,Amazon SQS 的性能较慢

    我想在我的 Web 应用程序中集成消息队列中间层 我测试了 Rabbitmq 和 Amazon SQS 但发现 Amazon SQS 速度很慢 我在 Amazon SQS 中每秒收到 80 个请求 而在 Rabbitmq 中每秒收到 200
  • 在点网核心应用程序中使用 RabbitMQ 跳过 MassTransit 中的队列

    我有三个项目 一个是Dot net core MVC 两个是API项目 MVC 正在调用一个 API 来获取用户详细信息 当询问用户详细信息时 我通过 MassTransit 向队列发送消息 我看到跳过队列 第三个项目中有消费者 即API项
  • 当我为rabbitmq-management创建用户时,发生了错误

    当我为rabbitmq创建用户时 root localhost rabbitmqctl add user admin admin 发生错误 消息 Creating user admin Error undef crypto hash sha
  • Spring AMQP Java 客户端中的队列大小

    我使用 Spring amqp 1 1 版本作为我的 java 客户端 我有一个大约有 2000 条消息的队列 我想要一个服务来检查这个队列大小 如果它是空的 它会发出一条消息说 所有项目已处理 我不知道如何获取当前队列大小 请帮忙 我用谷
  • 何时使用 RabbitMQ 铲子以及何时使用 Federation 插件?

    对于我工作的公司 我们希望使用 RabbitMQ 作为我们的主要消息总线 我们的想法是 每个应用程序都使用自己的虚拟主机进行内部通信 并且通过 shovel 或联合插件 我们可以在多个虚拟主机 甚至可能是多台机器 非集群 之间共享某些类型的
  • 列出与rabbitmq java客户端API交换的绑定

    我似乎在文档中找不到任何信息 所以我想知道是否可以通过某种方式使用 java RabbitMQ API 获取与交换相关的所有绑定 我在查询 api bindings 时正在寻找类似 http api 结果的内容 api definition
  • 在rabbitmq配置spring boot中在AMQP中配置多个Vhost

    我正在实现一个项目 我必须在rabbitmq中的不同虚拟主机之间发送消息 使用 SimpleRoutingConnectionFactory 但得到 java lang IllegalStateException 无法确定查找键的目标 Co
  • 为什么需要消息队列来与 Web 套接字聊天?

    我在互联网上看到了很多使用 Web 套接字和 RabbitMQ 进行聊天的示例 https github com videlalvaro rabbitmq chat https github com videlalvaro rabbitmq
  • 死信交换 RabbitMQ 丢弃消息

    我正在尝试在 RabbitMQ 中实现 dlx 队列 场景很简单 我有 2 个队列 1 活着 2 死亡 x dead letter exchange 立即 x message ttl 5000 以及 立即 交换 这必然是 1 活着 我尝试运
  • 定义具有多种消息类型的消息传递域

    到目前为止 我见过的大多数 F 消息传递示例都使用 2 4 种消息类型 并且能够利用模式匹配将每条消息定向到其正确的处理函数 对于我的应用程序 由于处理和所需参数的不同性质 我需要数百种独特的消息类型 到目前为止 每个消息类型都是其自己的记
  • 即使设置了 cookie,RabbitMQ 身份验证也会失败

    我最近在运行 lattePanda 的 Windows 10 上安装了带有 ErlanOTP 的rabbitmq 我运行rabbitmqctl status并收到以下错误 C Program Files RabbitMQ Server ra
  • Erl 无法连接到本地 EPMD。为什么?

    Erlang R14B04 erts 5 8 5 source 64 bit rq 1 async threads 0 kernel poll false Eshell V5 8 5 abort with G root ip 10 101
  • RabbitMQ:无法启动rabbitmq_management插件

    Version gt sudo rabbitmqctl status grep rabbit RabbitMQ rabbit RabbitMQ 3 5 6 Error gt sudo rabbitmq plugins enable rabb
  • 从 RabbitMQ 迁移到 Amazon SQS [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我们的初创公司目前正在使用RabbitMQ with Python Django 对于消息队列 现在我们计划转移到Amazon SQS其高可用性
  • RabbitMQ 中的 celeryev 队列变得非常大

    我在rabbitmq上使用celery 我已经向队列发送了数千条消息 它们正在成功处理 一切正常 然而 几个rabbitmq队列中的消息数量增长得相当大 队列中有数十万个项目 队列被命名为celeryev 见下面的截图 这是适当的行为吗 这

随机推荐

  • webug 4.0 第十关 存储型xss

    感谢webug团队一直以来的更新维护 webug是什么 WeBug名称定义为 我们的漏洞 靶场环境基础环境是基于PHP mysql制作搭建而成 中级环境与高级环境分别都是由互联网漏洞事件而收集的漏洞存在的操作环境 部分漏洞是基于Window
  • 将在数组中的对象的 key,做替换

    固定更换个别 key key1 映射 stroke value 替代值不变 比如 value 的 stroke const arrayOfObj key1 value1 key2 value2 key1 value1 key2 value2
  • 什么是CSS权重值?CSS权重值的优先级是什么?

    什么是CSS的权重值 1 权重决定了你css规则怎样被浏览器解析直到生效 css权重关系到你的css规则是怎样显示的 2 当很多的规则被应用到某一个元素上时 权重是一个决定哪种规则生效 或者是优先级的过程 3 每个选择器都有自己的权重 你的
  • 华为OD机试 - 矩阵最大值(Python)

    题目描述 给定一个仅包含0和1的N N二维矩阵 请计算二维矩阵的最大值 计算规则如下 1 每行元素按下标顺序组成一个二进制数 下标越大越排在低位 二进制数的值就是该行的值 矩阵各行值之和为矩阵的值 2 允许通过向左或向右整体循环移动每行元素
  • python selenium从新浪财经网抓取一家公司的高管任职信息写入excel表格

    网址 http vip stock finance sina com cn corp go php vCI CorpManager stockid 600900 phtml 在页面按f12打开开发者工具 在table标签上右键复制xpath
  • SpringMvc学习-4-Spring MVC 拦截器

    SPring MVC JSON数据交互 Spring提供了一个HttpMessageConverter
  • excel多元线性拟合_急,如何用excel拟合多元函数

    数据如下 公式lny lnA aT blnK clnL D 其中AD是固定值 y是生产总值 T是时间 K是固定资产投资 L是就业 求用excel拟合方程求出abc 多谢了 急等各位大侠 生产总值 固定资产投资 时间 就业 522 28 11
  • 胡言乱语

    1 2013 4 10 又梦见你们苍苍的容颜 心里满满的自责 我早该长大了 不应该心存侥幸的在抗拒 让你们承受的太多了 愧疚难当
  • 最新CTR预测服务的GPU优化实践

    CTR模型在互联网的搜索 推荐 广告等场景有着广泛的应用 近年来 随着深度神经网络的引入 CTR模型的推理对硬件算力的要求逐渐增加 本文介绍了美团在CTR模型优化的实践 通过分析模型结构特点 结合GPU硬件架构 我们设计了一系列流程对模型进
  • 目标检测标签文件txt转成xml

    最近在用ppyolo训练好的模型对新采集的数据进行标记 再人工微调 减少从头打标签的时间 但是推理保存的结果都是txt格式的 想要在labelimg中可视化 那就需要将txt转换成xml 以下代码即可完成这一功能 coding UTF 8
  • Web_Components 系列(九)—— Shadow Host 的 CSS 选择器

    前言 在上一节我们了解了如何给自定义组件设置样式 当时是将自定义标签的样式设置在主 DOM 中的
  • docker从安装到入门(centos7连不上网)

    我们安装好centos7之后 只用网络链接的NAT模式 主机有网你就有网 傻瓜式链接 但是发现ping不通百度 通过ifconfig a发现ens33没有ip 虽然和这个没关系 因为用的是NAT模式 我们编辑vi etc sysconfig
  • OpenGL error 0x0502

    项目出现OpenGL error 0x0502 导致有些UI绘制不出来 大致情况是这样 游戏切换到后台之后 其他玩家发来语音 在语音还在播放的时候 切换回前台 这个时候语音服务器 用的融云 会推送到安卓底层 安卓底层有回调到cocos2dx
  • Linux系统之neofetch工具的基本使用

    Linux系统之neofetch工具的基本使用 一 neofetch工具介绍 1 1 neofetch简介 1 2 neofetch特点 二 检查本地环境 2 1 检查操作系统版本 2 2 检查内核版本 三 安装neofetch工具 3 1
  • VMware Workstation Pro 安装教程

    文章目录 笔者的运行环境 VMware Workstation 16 Pro Red Hat Enterprise Linux 8 3 0 需要提前一个操作系统的镜像文件 ISO 这个文件与 VMware 无关 实际上 在安装完 VMwar
  • 生成字典的三种方式

    字典是记录一些特殊或有目的性的密码集合 通常以txt格式进行记录保存 在渗透许多服务器 smb ftp ssh 远程桌面rdp 网页后台等一些用户登录时 没有正确密码 使用密码字典爆破就是最直接的黑客攻击方法 一 使用cupp工具生成 1
  • 对输入数据排序后进行二分查找(C语言)

    输入数据后的排序方法有很多种 这里我用的是暴力排序 各位友友们可以尝试更改排序方法 include
  • Python selenium 滚动页面以及滚动至元素可见之详细讲解

    我们滚动浏览器页面向上 下 左右可以用一下代码 向上和向左需要加 向下滚动xx个像素 driver execute script window scrollBy 0 xx 向上滚动x个像素 driver execute script win
  • JM解码(一):参考帧列表和DPB处理

    以P帧为例 void alloc ref pic list reordering buffer Slice currSlice int size currSlice gt num ref idx active LIST 0 1 if cur
  • 谷粒商城-分布式高级篇[商城业务-订单服务]

    谷粒商城 分布式基础篇 环境准备 谷粒商城 分布式基础 业务编写 谷粒商城 分布式高级篇 业务编写 持续更新 谷粒商城 分布式高级篇 ElasticSearch 谷粒商城 分布式高级篇 分布式锁与缓存 项目托管于gitee 一 页面环境搭建