谷粒商城笔记+踩坑(20)——订单确认页。feign异步请求头丢失问题+令牌保证幂等性

Posted vincewm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谷粒商城笔记+踩坑(20)——订单确认页。feign异步请求头丢失问题+令牌保证幂等性相关的知识,希望对你有一定的参考价值。

目录

1、订单确认页

1.1、vo类抽取

1.2、获取订单详情页数据,完整代码

1.2.1、Controller编写跳转订单确认页方法

1.2.2、Service获取订单详情页数据

1.3、【会员模块】获取会员所有收货地址

1.3.1、controller

1.3.2、service 

1.4、订单服务远程调用用户服务

1.5、【购物车模块】 获取用户选择的所有CartItem

1.5.1、业务流程 

1.5.2、编写Controller层接口

1.5.3、Service层实现类

1.5.4、【商品模块】获取指定商品的价格

1.5.5、购物车服务远程调用商品服务 

1.5.6、订单服务远程调用购物车服务

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

1.6.1、问题分析 

1.6.2、【订单模块】解决:配置类添加请求拦截器

1.7、异步线程丢失主线程请求头问题

1.8、前端,订单确认页渲染

1.9、订单确认页里,商品的库存查询

1.10、根据用户地址ID,返回详细地址并计算物流费

1.10.1、需求 

1.10.2、前端,选择收货地址页面效果

1.10.3、 模型类抽取 

1.10.4、controller

1.10.5、仓库模块远程调用用户模块,查地址信息

1.10.6、service,根据地址id获取地址信息和费用

1.11、保证接口幂等性,防重复提交表单

1.11.1、幂等性概述

1.11.2、实现思路

1.11.3、代码实现,redis添加令牌,防重复提交表单


1、订单确认页


1.1、vo类抽取

订单确认页需要用的数据

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

com.atguigu.gulimall.order.vo

/**
 * 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;


1.2、获取订单详情页数据,完整代码


1.2.1、Controller编写跳转订单确认页方法


com.atguigu.gulimall.order.web

@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.2.2、Service获取订单详情页数据

业务流程:

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

 com.atguigu.gulimall.order.service.impl 

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

    @Autowired
    MemberFeignService memberFeignService;

    @Autowired
    CartFeignService cartFeignService;

    /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException 

        //构建响应模型类OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //从拦截器ThreadLocal获取当前用户登录的信息
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //开启第一个异步任务
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> 

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        , threadPoolExecutor);

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> 

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        , threadPoolExecutor).thenRunAsync(() -> 
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() );

            if (skuStockVos != null && skuStockVos.size() > 0) 
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            
        ,threadPoolExecutor);

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

        //4、价格数据自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    



1.3、【会员模块】获取会员所有收货地址

1.3.1、controller

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

1.3.2、service 

com.atguigu.gulimall.member.service.impl

@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) 
  return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));

1.4、订单服务远程调用用户服务

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


1.5、【购物车模块】 获取用户选择的所有CartItem


1.5.1、业务流程 

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

1.5.2、编写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();
    
  
  	//....

1.5.3、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;
  

1.5.4、【商品模块】获取指定商品的价格

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());
    

1.5.5、购物车服务远程调用商品服务 

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

    //.....

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

1.5.6、订单服务远程调用购物车服务

package com.atguigu.gulimall.order.feign;

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

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


 

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

1.6.1、问题分析 


问题 :Feign远程调用的时候会丢失请求头

原因:远程调用是一个新的请求,不携带之前请求的cookie,导致购物车服务得不到请求头cookie里的登录信息。

解决:加上feign远程调用的请求拦截器。(RequestInterceptor)

因为feign在远程调用之前会执行所有的RequestInterceptor拦截器


1.6.2、【订单模块】解决:配置类添加请求拦截器

新请求同步cookie到请求头里

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

1.7、异步线程丢失主线程请求头问题


问题演示,删除红框代码:

上面完整代码里service里,已经解决了异步编排请求头丢失问题,我们可以删除再调试:

发现报错,报错原因是没有登录(因为远程调用线程丢失了请求头,ThreadLocal里也就获取不到登录信息)。

 

问题
由于 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;

1.8、前端,订单确认页渲染


修改 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">   ¥0.00</span>
      </p>
      <p class="qian_y">
         <span>服务费: </span>
         <span class="rmb">   ¥0.00</span>
      </p>
      <p class="qian_y">
         <span>退换无忧: </span>
         <span class="rmb">   ¥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>

1.9、订单确认页里,商品的库存查询


需求:

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

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

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 库存服务中提供 查询库存的接口

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);
    
  //....

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>

1.10、根据用户地址ID,返回详细地址并计算物流费

1.10.1、需求 


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

1.10.2、前端,选择收货地址页面效果

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

1.10.3、 模型类抽取 

gulimall-ware 服务中 com.atguigu.gulimall.ware.vo路径下的 Vo

@Data
public class FareVo 
    private MemberAddressVo addressVo;
    private BigDecimal fare;

 

1.10.4、controller


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

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.10.5、仓库模块远程调用用户模块,查地址信息

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.10.6、service,根据地址id获取地址信息和费用

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.11、保证接口幂等性,防重复提交表单

1.11.1、幂等性概述

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

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

1.11.2、实现思路

redis添加数据,键为"order:token+用户id",值为防重复提交表单的uuid作为token,并设置30min过期时间,下单时候查redis,判断是否重复提交。

1.11.3、代码实现,redis添加令牌,防重复提交表单

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;
    


谷粒商城笔记+踩坑——上架商品spu到ES索引库

目录

1、ES回顾

2、ES整合商品上架 

2.1、分析

2.2、创建sku的es索引库

2.2.1、两种索引库设计方案分析

2.2.2、最终选用的索引库方案

2.3、SkuEsModel模型类

2.4、【库存模块】库存量查询

2.5、【查询模块】保存ES文档

2.5.1、常量类

2.5.2、controller

2.5.3、service

2.6、【商品模块】上架单个spu

2.6.1、controller

2.6.2、远程调用库存模块

2.6.3、【公共模块】商品常量类,添加上传成功或失败的状态码

2.6.4、service

2.6.5、测试

2.6.6、修改结果类R


1、ES回顾

elasticsearch基础1——索引、文档_elasticsearch索引 文档_vincewm的博客-CSDN博客

elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客elasticsearch基础3——聚合、补全、集群_vincewm的博客-CSDN博客elasticsearch基础2——DSL查询文档,黑马旅游项目查询功能_elasticsearch查询文档_vincewm的博客-CSDN博客

2、ES整合商品上架 

2.1、分析

es在整个项目中的应用:

1.对商品的全文检索功能

2.对日志的全文检索功能

为什么不用mysql?

es比mysql检索功能强大,并且对于庞大的检索数据,es性能更好,因为mysql是存在内存中的,而es可以分片存储。 

需求:

  • 在后台选择上架的商品才能在网站展示
  • 上架的商品也可以被ES检索

2.2、创建sku的es索引库

2.2.1、两种索引库设计方案分析

  • 检索的时候输入名字,这样就是需要用sku的名称去全文检索
  • 检索的时候选择商品规格,规格是spu的公共属性,每个spu是一样的

索引库设计方案1(推荐,空间换时间):规格参数放在sku里

缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu下面的sku规格参数都一样。例如spu“华为14Pro”下的sku“红色华为14Pro”,以及spu“华为手机”下的sku“蓝色华为14Pro”,这两个sku的规格参数“CPU:A14”相等。


    skuId:1
    spuId:11
    skyTitile:华为xx
    price:999
    saleCount:99
    attr:[
        尺寸:5,
        CPU:高通945,
        分辨率:全高清
	]

索引库设计方案2(不推荐,传输的数据量大):规格参数和sku分离

sku索引

    spuId:1
    skuId:11

attr索引

    skuId:11
    attr:[
        尺寸:5,
        CPU:高通945,
        分辨率:全高清
	]

结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络故障。

所以我们选方案一,用空间换时间

2.2.2、最终选用的索引库方案

“type”: “keyword” , 保持数据精度问题,可以检索,但不分词
“analyzer”: “ik_smart” 中文分词器
“index”: false, 不可被检索,不生成index
“doc_values”: false 默认为true,不可被聚合,es就不会维护一些聚合的信息

这个数据模型要先在es中建立

PUT product

    "mappings":
        "properties": 
            "skuId": "type": "long" ,    #商品sku
            "spuId": "type": "keyword" ,  #当前sku所属的spu。
            "skuTitle": 
                "type": "text",
                "analyzer": "ik_smart"      #只有sku的标题需要被分词
            ,
            "skuPrice":  "type": "keyword" ,  
            "skuImg"  :  "type": "keyword" ,  
            "saleCount": "type":"long" ,
            "hasStock":  "type": "boolean" ,    #是否有库存。在库存模块添加此商品库存后,此字段更为true
            "hotScore":  "type": "long"  ,
            "brandId":   "type": "long" ,
            "catalogId":  "type": "long"  ,
            "brandName": "type": "keyword", 
            "brandImg":
                "type": "keyword",
                "index": false,  
                "doc_values": false 
            ,
            "catalogName": "type": "keyword" , 
            "attrs": 
                "type": "nested",
                "properties": 
                    "attrId": "type": "long"  ,
                    "attrName": 
                        "type": "keyword",
                        "index": false,
                        "doc_values": false
                    ,
                    "attrValue": "type": "keyword" 
                
            
        
    


2.3、SkuEsModel模型类

商品上架需要在es中保存spu信息并更新spu状态信息,所以我们就建立专门的vo来接收

SkuEsModel

写在common模块

@Data
public class SkuEsModel  //common中
    private Long skuId;
    private Long spuId;
    private String skuTitle;
    private BigDecimal skuPrice;
    private String skuImg;
    private Long saleCount;
    private boolean hasStock;
    private Long hotScore;
    private Long brandId;
    private Long catalogId;
    private String brandName;
    private String brandImg;
    private String catalogName;
    private List<Attr> attrs;

    @Data
    public static class Attr
        private Long attrId;
        private String attrName;
        private String attrValue;
    


2.4、【库存模块】库存量查询

查询sku列表是否有库存:

上架的话需要确定库存,所以调用ware微服务来检测是否有库存

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController 
    @Autowired
    private WareSkuService wareSkuService;
    
    @PostMapping(value = "/hasStock")
    public R getSkuHasStock(@RequestBody List<Long> skuIds) 
        List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
        return R.ok().setData(vos);
    

实现也比较好理解,就是先用自定义的mapper查有没有库存

有的话,给库存赋值,并收集成集合

@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) 
    List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map(item -> 
//根据sku_id查库存,要写mapper,主要为了真实库存减去锁定库存
        Long count = this.baseMapper.getSkuStock(item);    
        SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
        skuHasStockVo.setSkuId(item);
        skuHasStockVo.setHasStock(count == null ? false : count > 0);
        return skuHasStockVo;
    ).collect(Collectors.toList());

    return skuHasStockVos;


自定义mapper

这里的库存并不是简单查一下库存表,需要自定义一个简单的sql。用库存减去锁定的库存即可得出!

<select id="getSkuStock" resultType="java.lang.Long">
    SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id = #skuId
</select>

2.5、【查询模块】保存ES文档

2.5.1、常量类

public class EsConstant 

    //在es中的索引
    public static final String PRODUCT_INDEX = "gulimall_product";

    public static final Integer PRODUCT_PAGESIZE = 16;

2.5.2、controller

ElasticSaveController

package com.xxx.gulimall.search.controller;
@Slf4j
@RequestMapping(value = "/search/save")
@RestController
public class ElasticSaveController 

    @Autowired
    private ProductSaveService productSaveService;


    /**
     * 上架商品
     * @param skuEsModels
     * @return
     */
    @PostMapping(value = "/product")
    public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) 

        boolean status=false;
        try 
            status = productSaveService.productStatusUp(skuEsModels);
         catch (IOException e) 
            //log.error("商品上架错误",e);

            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
        

        if(status)
            return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(),BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
        else 
            return R.ok();
        

    



2.5.3、service

使用BulkRequest ,批量保存sku_es模型类列表到索引库

@Slf4j
@Service("productSaveService")
public class ProductSaveServiceImpl implements ProductSaveService 

    @Autowired
    private RestHighLevelClient esRestClient;

    @Override
    public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException 

        //1.在es中建立索引,建立号映射关系(doc/json/product-mapping.json)[kibana中执行product-mapping.txt,需要ES安装IK分词器]

        //2. 在ES中保存这些数据
        BulkRequest bulkRequest = new BulkRequest();
        for (SkuEsModel skuEsModel : skuEsModels) 
            //构造保存请求
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
            indexRequest.id(skuEsModel.getSkuId().toString());
            String jsonString = JSON.toJSONString(skuEsModel);
            indexRequest.source(jsonString, XContentType.JSON);
            bulkRequest.add(indexRequest);
        


        BulkResponse bulk = esRestClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

        //TODO 如果批量错误
        boolean hasFailures = bulk.hasFailures();

        List<String> collect = Arrays.asList(bulk.getItems()).stream().map(item -> 
            return item.getId();
        ).collect(Collectors.toList());

        log.info("商品上架完成:",collect);

        return hasFailures;
    

2.6、【商品模块】上架单个spu

2.6.1、controller

SpuInfoController上架

/**
 * 商品上架
 */
@PostMapping("/spuId/up")
public R spuUp(@PathVariable("spuId") Long spuId)
    spuInfoService.up(spuId);
    return R.ok();

2.6.2、远程调用库存模块

在商品模块的feign包下:

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

    @PostMapping(value = "/ware/waresku/hasStock")
    R getSkuHasStock(@RequestBody List<Long> skuIds);

商品模块启动类:

@EnableFeignClients(basePackages = "com.xunqi.gulimall.product.feign")

然后service里就能直接@Autowired注入了。 

2.6.3、【公共模块】商品常量类,添加上传成功或失败的状态码

public class ProductConstant 

    public enum AttrEnum 
        ATTR_TYPE_BASE(1,"基本属性"),
        ATTR_TYPE_SALE(0,"销售属性");

        private int code;

        private String msg;

        public int getCode() 
            return code;
        

        public String getMsg() 
            return msg;
        

        AttrEnum(int code, String msg) 
            this.code = code;
            this.msg = msg;
        

    


    public enum ProductStatusEnum 
        NEW_SPU(0,"新建"),
        SPU_UP(1,"商品上架"),
        SPU_DOWN(2,"商品下架"),
        ;

        private int code;

        private String msg;

        public int getCode() 
            return code;
        

        public String getMsg() 
            return msg;
        

        ProductStatusEnum(int code, String msg) 
            this.code = code;
            this.msg = msg;
        

    


2.6.4、service

业务流程:

  1. 查询当前spu下的sku列表;
  2. 将这个sku列表封装成sku_es模型类列表;
    1. 给每个sku加上属性规格列表;
    2. 查询每个sku是否有库存,要远程调用库存模块;
    3. 给每个sku加上热度、所属品牌、所属分类名、所有可被检索的规格等属性;
  3. 将收集的sku_es模型类列表发给es保存,要远程调用查询模块。

SpuInfoServiceImpl

@Override
    public void up(Long spuId) 
        //1.获得spu对应的sku集合
        List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);

        //2.获得spu的基础属性实体集合
        List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListforSpu(spuId);

        //3.获得基本属性中可搜索的属性id
        //3.1获得spu基础属性实体集合中的属性id集合
        List<Long> attrIds = baseAttrs.stream().map(attr -> 
            return attr.getAttrId();
        ).collect(Collectors.toList());

        //3.2获得可搜索属性实体类对象
        List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);

        //3.3将它们转化为set集合
        Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());

        //3.4对所有基础属性实体过滤,第一步是只保留可搜索属性实体类对象,第二步是给这些对象中的Attrs对象赋值,最后收集为attrsList
        List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> 
            return idSet.contains(item.getAttrId());
        ).map(item -> 
            SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
            BeanUtils.copyProperties(item, attrs);
            return attrs;
        ).collect(Collectors.toList());

        //收集所有skuId的集合
        List<Long> skuIdList = skuInfoEntities.stream()
                .map(SkuInfoEntity::getSkuId)
                .collect(Collectors.toList());

        //TODO 1、发送远程调用,库存系统查询是否有库存
        Map<Long, Boolean> stockMap = null;
        try 
            R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() ;
            stockMap = skuHasStock.getData(typeReference).stream()
                    .collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
         catch (Exception e) 
            log.error("库存服务查询异常:原因",e);
        
//2、封装每个sku的信息
        Map<Long, Boolean> finalStockMap = stockMap;
        List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> 
            //组装需要的数据
            SkuEsModel esModel = new SkuEsModel();
            esModel.setSkuPrice(sku.getPrice());
            esModel.setSkuImg(sku.getSkuDefaultImg());

            //设置库存信息
            if (finalStockMap == null) 
                esModel.setHasStock(true);
             else 
                esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
            

            //TODO 2、热度评分。0
            esModel.setHotScore(0L);

            //TODO 3、查询品牌和分类的名字信息
            BrandEntity brandEntity = brandService.getById(sku.getBrandId());
            esModel.setBrandName(brandEntity.getName());
            esModel.setBrandId(brandEntity.getBrandId());
            esModel.setBrandImg(brandEntity.getLogo());

            CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
            esModel.setCatalogId(categoryEntity.getCatId());
            esModel.setCatalogName(categoryEntity.getName());

            //设置检索属性
            esModel.setAttrs(attrsList);

            BeanUtils.copyProperties(sku,esModel);

            return esModel;
        ).collect(Collectors.toList());

        //TODO 5、将数据发给es进行保存:mall-search
        R r = searchFeignService.productStatusUp(collect);

        if (r.getCode() == 0) 
            //远程调用成功
            //TODO 6、修改当前spu的状态,具体代码看代码块后面SpuInfoDao.xml
            this.baseMapper.updaSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
         else 
            //远程调用失败
            //TODO 7、重复调用?接口幂等性:重试机制
        
    

SpuInfoDao更新spu状态

    <update id="updaSpuStatus">
        UPDATE pms_spu_info SET publish_status = #code ,update_time = NOW() WHERE id = #spuId
    </update>

2.6.5、测试

商品上架用到了三个微服务,分别是product、ware、search

那我们分别debug启动它们,然后在这些微服务中使用的方法中打上断点,查看调用流程

获得spu对应的sku集合

获得spu的基础属性实体集合

基础属性如下:

给SkuEsModel.Attrs对象赋值

测试

2.6.6、修改结果类R

	public <T> T getData(String key, TypeReference<T> typeReference) 
		Object data = get(key);// 默认是map类型,springmvc做的
		String jsonStr = JSON.toJSONString(data);
		T t = JSON.parseObject(jsonStr, typeReference);
		return t;
	

	// 利用fastJson进行逆转
	// 这里要声明泛型<T>,这个泛型只跟方法有关,跟类无关。
	// 例如类上有个泛型,这里可以使用类上的泛型,就不用声明
	public <T> T getData(TypeReference<T> typeReference) 
		Object data = get("data");// 默认是map类型,springmvc做的
		String jsonStr = JSON.toJSONString(data);
		T t = JSON.parseObject(jsonStr, typeReference);
		return t;
	

	public R setData(Object data) 
		put("data", data);
		return this;
	

以上是关于谷粒商城笔记+踩坑(20)——订单确认页。feign异步请求头丢失问题+令牌保证幂等性的主要内容,如果未能解决你的问题,请参考以下文章

黑马Java笔记+踩坑汇总JavaSE+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud/SpringCloudAlibaba+黑马旅游+谷粒商城

谷粒商城二十订单服务

Day429.订单服务 -谷粒商城

谷粒商城二十一订单服务分布式事务

Day429&430&431.订单服务 -谷粒商城

Day434.订单&库存服务分布式事务的最终解决 -谷粒商城