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

目录

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

}

猜你喜欢

转载自blog.csdn.net/qq_40991313/article/details/129914622
今日推荐