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

简介: feign、异步请求头丢失问题+令牌保证幂等性

 导航:

谷粒商城笔记+踩坑汇总篇

Java笔记汇总:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析-CSDN博客

目录

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、业务流程

1.11.4、代码实现,防重复提交表单,唯一序列号方式保证幂等性

11.1.5 测试


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

image.gif

收货地址,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;
}

image.gif

商品项信息

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

package com.atguigu.gulimall.order.feign;
@FeignClient("gulimall-cart")
public interface CartFeignService {
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}

image.gif

 

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

1.6.1、问题分析


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

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

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

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

image.gif

image.gif

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

image.gif

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


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

image.gif

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

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

问题

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

解决

向异步 RequestContextHolder 线程域中放主线程的域。

image.gif

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

image.gif

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

image.gif

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


image.gif

修改 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>

image.gif

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

image.gif

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


需求:

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

image.gif

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

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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

image.gif

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>

image.gif

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

1.10.1、需求


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

image.gif image.gif

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

image.gif image.gif image.gif

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

image.gif

1.10.3、 模型类抽取

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

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

image.gif

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

image.gif

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

image.gif

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

image.gif

 

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

1.11.1、幂等性概述

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

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

image.gif

1.11.2、任务幂等性的三种保证方法

  • 数据库约束:比如唯一约束,主键。同一个主键不可能两次都插入成功。不推荐因为适用范围太窄,只适用于保存数据库前就已经设置好主键并且每次主键一样的情况下。
  • 乐观锁:数据库表中增加一个版本字段,更新时判断是否等于某个版本。例如重复提交时判断数据库发现版本已被改变就不提交了。不推荐,因为要查数据库,给数据库压力,临时的操作我们尽量在缓存库里操作,降低数据库压力。
  • Redis唯一序列号(推荐):Redis键为任务id,值为随机序列化uuid。请求前生成唯一的序列号,携带序列号去请求,请求时在redis记录该序列号表示以该序列号的请求执行过了,如果相同的序列号再次来执行说明是重复执行。也可以通过让用户每次提交时输入验证码,提交后校验前后端验证码实现幂等性。

1.11.3、业务流程

需求:用户进入订单确认页,在不刷新、不重进的情况下,重复点击“提交订单”,只有一次能提交成功。

确认订单: 生成令牌:redis添加数据,key为"order:token"+用户id,value为防重复提交表单的uuid作为token,并设置30min过期时间。

提交订单(下一篇文章详细讲):

验令牌:先获取前端传来的token,再根据用户id查询Redis里的token,比较两个token是否相等,相等则代表是同一个的订单。因为uuid能保证唯一性,它是根据时间戳和mac地址生成的。

原子性验删令牌:验令牌和删除令牌写成一个lua脚本,Redis传参键值对并执行lua脚本,执行成功代表验证成功,执行失败代表验证失败。

1.11.4、代码实现,防重复提交表单,唯一序列号方式保证幂等性

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

简洁版:

/**
     * 确认订单、订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        //构建OrderConfirmVo,查登录、查询库存、购物车、商品id
        //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);
        return confirmVo;
    }
/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @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;
    }
}

image.gif

前端会显示令牌:

<form action="http://order.gulimall.com/submitOrder" method="post">
    <input id="addrInput" type="hidden" name="addrId" />
    <input id="payPriceInput" type="hidden" name="payPrice">
        <input name="orderToken" th:value="${confirmOrderData.orderToken}" type="hidden"/>
        <button class="tijiao" type="submit">提交订单</button>
    </form>

image.gif

11.1.5 测试

重启服务,进入订单确认页,暂时删去前端<input>里的type="hidden",可以看见令牌:

image.gif

测试发现,刷新页面,令牌会更改。回退后重新进入确认页,令牌会更改。

我(id是66)在不刷新的情况下,连续点击2次提交,传到后端的token都是一样的,记为token1,Redis存"order:token66"--->token1。

  • 线程A:“提交订单”的controller接收到token1,原子性对比redis里的token1和删除,校验通过;
  • 线程B:“提交订单”的controller接收到token1,原子性对比redis里的token1和删除,因为线程A已经删除成功,所以现在校验失败或者删除失败,所以校验失败。

我刷新一下,再次点击2次提交,传到后端的token都是一样的,记为token2,Redis存"order:token66"--->token2。

  • 线程C:“提交订单”的controller接收到token2,原子性对比redis里的token2和删除,校验通过;
  • 线程D:“提交订单”的controller接收到token2,原子性对比redis里的token2和删除,因为线程C已经删除成功,所以现在校验失败或者删除失败,所以校验失败。
相关文章
|
存储 缓存 NoSQL
防止订单重复提交或支付分布式锁方案设计
防止订单重复提交或支付分布式锁方案设计
786 0
|
NoSQL Java Redis
服务端如何防止订单重复支付!
如图是一个简化的下单流程,首先是提交订单,然后是支付。 支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互。 支付成功以后,异步通知支付中心,支付中心更新自身支付订单状态,再通知业务应用,各业务再更新各自订单状态。
服务端如何防止订单重复支付!
|
3月前
|
消息中间件 设计模式 SQL
谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存
完成提交订单功能,并使用分布式事务方案,保证了订单提交的幂等性
谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存
|
4月前
|
SQL NoSQL 前端开发
大厂如何解决订单幂等问题
本文探讨了分布式系统中接口幂等性的重要性和实现方法,特别是在防止重复下单的场景中。首先介绍了通过数据库事务处理创建订单时的原子性需求。接着分析了服务间调用时可能遇到的重复请求问题,提出每个请求需具备唯一标识,并记录处理状态以识别并阻止重复操作。具体实践包括生成全局唯一的订单ID,利用数据库主键唯一性约束来防止重复插入,以及使用Redis存储订单支付状态。此外,文章还讨论了解决ABA问题(即数据在两次检查之间被修改的问题)的方法,引入版本号机制来确保数据更新的原子性和一致性。这些技术方案不仅限于订单服务,也可广泛应用于需要实现幂等性的其他业务场景中。
|
6月前
|
NoSQL Redis
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
138 0
|
7月前
|
消息中间件 Java Unix
MQ产品使用合集之消费订单状态,订单消费待支付消息失败,是否会导致其他订单也没法消费
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
Java Spring 容器
通过注解开发来实现下单接口的防重提交
通过注解开发来实现下单接口的防重提交
|
SQL 负载均衡 NoSQL
【防止重复下单】分布式系统接口幂等性实现方案
【防止重复下单】分布式系统接口幂等性实现方案
1777 0
【防止重复下单】分布式系统接口幂等性实现方案
|
存储 NoSQL Redis
下单接口防重提交问题
下单接口防重提交问题
|
NoSQL Java 数据库
java接口防重提交如何处理
举一个最简单的例子:日常开发中crud在业务系统中普遍存在,在服务端没有做任何处理,客户端没有做节流、防抖等限流操作时,同一秒一个用户点了两次新增按钮,导致数据库中存在同样两条数据,其结果可想而知,同理修改、删除同样的道理;查询本身具有幂等性,但是在同一秒钟同样的操作,查询多次和一次,有区别吗?区别大了去了,不谈用户体验如何,光是网络开销、流量占用、带给服务器的压力等等,生产中一点小的问题,如何不及时处理,可能会引发灾难性bug。