谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存

目录

1、环境准备

1.1、业务流程

1.2、Controller 层编写下单功能接口

1.3、订单提交的模型类

1.4、前端页面 confirm.html 提供数据

2、提交订单业务完整代码

3、原子性验令牌:令牌的对比和删除保证原子性

4、初始化新订单,包含订单、订单项等信息

4.1、抽取订单创建传输类

4.2、service

4.3、创建订单

4.3.1、远程调用仓库服务,计算运费和详细地址的接口

4.3.2、封装运费模型类

4.3.3、创建订单service

4.4、构造订单项数据

4.4.1、构建订单项数据service

4.4.2、【商品模块】通过skuId查询spu信息

4.4.3、订单服务远程调用商品服务

4.4.4、抽取商品信息vo

4.5、计算价格

5、锁定库存

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

5.1.1、service保存订单数据并锁定库存

5.1.2、【公共模块】无库存异常类

5.2、【仓库模块】锁定库存

5.2.1、订单服务远程调用仓库服务

5.2.2、锁定库存controller

5.2.3、锁定库存的vo类

5.2.4、锁定指定订单的库存service

5.2.5、dao,根据sku_id查询在有库存的仓库

5.2.6、【公共模块】错误码枚举类添加库存相关错误码

6、前端页面的修改

7、提交订单的完整代码

7.1、Controller层接口编写

7.2、Service层代码

7.2.1、提交订单业务

7.2.2、创建订单、构建订单、计算价格等调用的方法


1、环境准备

1.1、业务流程

img谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第1张图片

1.2、Controller 层编写下单功能接口

订单服务 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";
    }

}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第2张图片

1.3、订单提交的模型类


页面提交数据 添加“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取出登录的用户
     */
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第3张图片

1.4、前端页面 confirm.html 提供数据

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第4张图片

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第5张图片

2、提交订单业务完整代码

   /**
     * 提交订单
     * @param vo
     * @return
     */
    // @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
    // @Transactional(propagation = Propagation.REQUIRED)   设置事务的传播级别
    @Transactional(rollbackFor = Exception.class)
    // @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        confirmVoThreadLocal.set(vo);

        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        //去创建、下订单、验令牌、验价格、锁定库存...

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

        //2、验证令牌是否合法【令牌的对比和删除必须保证原子性】
        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();

        //通过lure脚本原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript(script, Long.class),
                Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
                orderToken);

        if (result == 0L) {
            //令牌验证失败
            responseVo.setCode(1);
            return responseVo;
        } else {
            //令牌验证成功
            //1、创建订单、订单项等信息
            OrderCreateTo order = createOrder();

            //2、验证价格
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();

            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                //金额对比
                //TODO 3、保存订单
                saveOrder(order);

                //4、库存锁定,只要有异常,回滚订单数据
                //订单号、所有订单项信息(skuId,skuNum,skuName)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());

                //获取出要锁定的商品数据信息
                List orderItemVos = order.getOrderItems().stream().map((item) -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(orderItemVos);

                //TODO 调用远程锁定库存的方法
                //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
                //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    //锁定成功
                    responseVo.setOrder(order.getOrder());
                    // int i = 10/0;

                    //TODO 订单创建成功,发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

                    //删除购物车里的数据
                    redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
                    return responseVo;
                } else {
                    //锁定失败
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                    // responseVo.setCode(3);
                    // return responseVo;
                }

            } else {
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第6张图片

3、原子性验令牌:令牌的对比和删除保证原子性


问题:存在网路延时,同时提交从Redis拿到的令牌一直,导致重复提交

解决:令牌的对比和删除必须保证原子性

1)、封装提交订单数据

package com.atguigu.gulimall.order.vo;

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第7张图片

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(script, Long.class), Arrays.asList(),
            OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), orderToken);
    if (result == 0L) {
        // 令牌验证失败
        response.setCode(1);
        return response;
    } else {
        // 令牌验证成功
        return response;
    }
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第8张图片

4、初始化新订单,包含订单、订单项等信息

4.1、抽取订单创建传输类

@Data
public class OrderCreateTo {

    private OrderEntity order;

    private List orderItems;

    /** 订单计算的应付价格 **/
    private BigDecimal payPrice;

    /** 运费 **/
    private BigDecimal fare;

}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第9张图片

4.2、service

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

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

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第10张图片

4.3、创建订单

4.3.1、远程调用仓库服务,计算运费和详细地址的接口

package com.atguigu.gulimall.order.feign;

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

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

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第11张图片

4.3.2、封装运费模型类

package com.atguigu.gulimall.order.vo;

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第12张图片

4.3.3、创建订单service

/**
 * 构建订单
 * @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() {
    });
    // 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;
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第13张图片

4.4、构造订单项数据

4.4.1、构建订单项数据service

OrderServiceImpl 类

/**
 * 构建所有订单项数据
 * @return
 */
private  List buildOrderItems(String orderSn) {
    // 最后确定每个购物项的价格
    List currentUserCartItems = cartFeignService.getCurrentUserCartItems();
    if (currentUserCartItems != null && currentUserCartItems.size()>0){
        List 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() {
    });
    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;
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第14张图片

4.4.2、【商品模块】通过skuId查询spu信息

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第15张图片

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第16张图片

4.4.3、订单服务远程调用商品服务

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

    @GetMapping("/product/spuinfo/skuId/{id}")
    R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第17张图片

4.4.4、抽取商品信息vo

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第18张图片

4.5、计算价格

/**
 * 计算价格
 * @param orderEntity
 * @param itemEntities
 */
private void computePrice(OrderEntity orderEntity, List 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 未删除
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第19张图片

5、锁定库存

在这里插入图片描述谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第20张图片

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

5.1.1、service保存订单数据并锁定库存

/**
 * 下单操作:验令牌、创建订单、验价格、验库存
 * @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 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;
        }
    }
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第21张图片

5.1.2、【公共模块】无库存异常类

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

}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第22张图片

5.2、【仓库模块】锁定库存

5.2.1、订单服务远程调用仓库服务

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第23张图片

5.2.2、锁定库存controller

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

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第24张图片

5.2.3、锁定库存的vo类

/**
 * @Description: 锁定库存的vo
 **/

@Data
public class WareSkuLockVo {

    private String orderSn;

    /** 需要锁住的所有库存信息 **/
    private List locks;



}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第25张图片

5.2.4、锁定指定订单的库存service

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

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl implements WareSkuService {

    @Autowired
    WareSkuDao wareSkuDao;

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

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

}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第26张图片

5.2.5、dao,根据sku_id查询在有库存的仓库

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

package com.atguigu.gulimall.ware.dao;

@Mapper
public interface WareSkuDao extends BaseMapper {

    /**
     * 通过skuId查询在哪个仓库有库存
     * @param skuId
     * @return  仓库的编号
     */
    List 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);

}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第27张图片

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


    UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}



谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第28张图片

5.2.6、【公共模块】错误码枚举类添加库存相关错误码

在 错误码和错误信息定义类 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;
    }
}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第29张图片

6、前端页面的修改


订单提交成功,跳转到支付页面 pay.html

订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]] 应付金额[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]
推荐使用 扫码支付请您在24小时内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限) 订单详细

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第30张图片

在这里插入图片描述谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第31张图片

订单提交失败,重定项到confirm.html 并回显 失败原因


填写并核对订单信息

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第32张图片

在这里插入图片描述谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第33张图片

在这里插入图片描述谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第34张图片

7、提交订单的完整代码

7.1、Controller层接口编写

/**
 * 下单功能
 * @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";
    }

}

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第35张图片

7.2、Service层代码

7.2.1、提交订单业务

   /**
     * 提交订单
     * @param vo
     * @return
     */
    // @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别
    // @Transactional(propagation = Propagation.REQUIRED)   设置事务的传播级别
    @Transactional(rollbackFor = Exception.class)
    // @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        confirmVoThreadLocal.set(vo);

        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        //去创建、下订单、验令牌、验价格、锁定库存...

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

        //2、验证令牌是否合法【令牌的对比和删除必须保证原子性】
        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();

        //通过lure脚本原子验证令牌和删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript(script, Long.class),
                Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
                orderToken);

        if (result == 0L) {
            //令牌验证失败
            responseVo.setCode(1);
            return responseVo;
        } else {
            //令牌验证成功
            //1、创建订单、订单项等信息
            OrderCreateTo order = createOrder();

            //2、验证价格
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();

            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                //金额对比
                //TODO 3、保存订单
                saveOrder(order);

                //4、库存锁定,只要有异常,回滚订单数据
                //订单号、所有订单项信息(skuId,skuNum,skuName)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());

                //获取出要锁定的商品数据信息
                List orderItemVos = order.getOrderItems().stream().map((item) -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(orderItemVos);

                //TODO 调用远程锁定库存的方法
                //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
                //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    //锁定成功
                    responseVo.setOrder(order.getOrder());
                    // int i = 10/0;

                    //TODO 订单创建成功,发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

                    //删除购物车里的数据
                    redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());
                    return responseVo;
                } else {
                    //锁定失败
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                    // responseVo.setCode(3);
                    // return responseVo;
                }

            } else {
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }

谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存_第36张图片

7.2.2、创建订单、构建订单、计算价格等调用的方法

    /**
     * 创建订单、订单项等信息
     * @return
     */
    private OrderCreateTo createOrder(){
        OrderCreateTo createTo = new OrderCreateTo();
        // 1、生成一个订单号
        String orderSn = IdWorker.getTimeId();
        // 2、构建一个订单
        OrderEntity orderEntity = buildOrder(orderSn);
        // 3、获取到所有的订单项
        List 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() {
        });
        // 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 buildOrderItems(String orderSn) {
        // 最后确定每个购物项的价格
        List currentUserCartItems = cartFeignService.getCurrentUserCartItems();
        if (currentUserCartItems != null && currentUserCartItems.size()>0){
            List 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() {
        });
        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 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);
        }

你可能感兴趣的:(谷粒商城项目,java学习路线,java,前端,servlet)