✅P282_商城业务-订单服务-锁定库存

gong_yz大约 4 分钟谷粒商城

锁库存流程

锁定库存流程如下图所示


编写VO

锁定库存VO

cfmall-order/src/main/java/com/gyz/cfmall/order/vo/WareSkuLockVo.java

@Data
public class WareSkuLockVo {

    /**
     * 订单编号
     */
    private String orderSn;
    /**
     * 需要锁住的所有库存信息
     */
    private List<OrderItemVo> locks;
}

将订单服务cfmall-order中的WareSkuLockVo与OrderItemVo复制到库存服务cfmall-ware

锁定库存结果VO

cfmall-ware/src/main/java/com/gyz/cfmall/ware/vo/LockStockResultVo.java

@Data
public class LockStockResultVo {

    private Long skuId;

    private Integer num;

    /**
     * 是否锁定成功
     **/
    private Boolean locked;
}

库存异常类与状态码

cfmall-common/src/main/java/com/gyz/common/exception/NoStockException.java

public class NoStockException extends RuntimeException {

    @Getter
    @Setter
    private Long skuId;

    public NoStockException(Long skuId) {
        super("商品id:" + skuId + "库存不足!");
    }

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

cfmall-common/src/main/java/com/gyz/common/exception/BizCodeEnum.java

public enum BizCodeEnum {

    /**
     * 状态码枚举
     */
    NO_STOCK_EXCEPTION(21000,"商品库存不足"),
    
    //省略其它代码
}

锁库存字段设置默认值:0 -> 表:**wms_ware_sku**,字段**stock_locked**


锁定库存

cfmall-ware/src/main/java/com/gyz/cfmall/ware/controller/WareSkuController.java

    @Resource
    private WareSkuService wareSkuService;

	/**
     * 锁定库存
     *
     * @param vo 库存解锁的场景
     *           1)、下订单成功,订单过期没有支付被系统自动取消或者被用户手动取消,都要解锁库存
     *           2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
     *           3)、
     */
    @PostMapping(value = "/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo) {
        try {
            boolean lockStock = wareSkuService.orderLockStock(vo);
            return R.ok().setData(lockStock);
        } catch (NoStockException e) {
            return R.error(NO_STOCK_EXCEPTION.getCode(), NO_STOCK_EXCEPTION.getMessage());
        }
    }

cfmall-ware/src/main/java/com/gyz/cfmall/ware/service/impl/WareSkuServiceImpl.java

    @Transactional(rollbackFor = NoStockException.class)
    @Override
    public boolean orderLockStock(WareSkuLockVo vo) {
        //1、按照下单的收货地址,找到一个就近仓库,锁定库存
        //2、找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();

        List<SkuWareHasStock> collect = locks.stream().map((item) -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪个仓库有库存
            List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIdList);

            return stock;
        }).collect(Collectors.toList());

        //2、锁定库存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();

            if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }

            //1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
            //2、锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
            for (Long wareId : wareIds) {
                //锁定成功就返回1,失败就返回0
                Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
                if (count == 1) {
                    skuStocked = true;
                    break;
                }
            }
            if (!skuStocked) {
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }
        //3、肯定全部都是锁定成功的
        return true;
    }

SkuWareHasStock内部类

    @Data
    static class SkuWareHasStock {
        private Long skuId;
        private Integer num;
        private List<Long> wareId;
    }

cfmall-ware/src/main/resources/mapper.ware/WareSkuDao.xml

    <select id="listWareIdHasSkuStock" resultType="java.lang.Long">
        SELECT ware_id
        FROM wms_ware_sku
        WHERE sku_id = #{skuId}
          AND stock - stock_locked > 0
    </select>
    <update id="lockSkuStock">
        UPDATE wms_ware_sku
        SET stock_locked = stock_locked + #{num}
        WHERE sku_id = #{skuId}
          AND ware_id = #{wareId}
          AND stock - stock_locked > #{num}
    </update>

提交订单业务

提交订单调用锁库存操作

Feign

cfmall-order/src/main/java/com/gyz/cfmall/order/feign/WmsFeignService.java

@FeignClient("cfmall-ware")
public interface WmsFeignService {
    /**
     * 锁定库存
     *
     * @param vo 锁定库存VO
     * @return true/false
     */
    @PostMapping(value = "/ware/waresku/lock/order")
    R orderLockStock(@RequestBody WareSkuLockVo vo);
}

实现逻辑

提交订单完整方法:

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) {
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        responseVo.setCode(0);
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
        String token = orderSubmitVo.getOrderToken();
        //脚本执行的返回结果: 0-代表令牌校验失败, 1-代表令牌成功删除即成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        //Arg1:用DefaultRedisScript的构造器封装脚本和返回值类型; Arg2:用于存放Redis中token的key;Arg3:用于比较的token,即浏览器存储的token
        //令牌的验证和删除必须保证原子性
        Long response = redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX.toString() + memberResponseVo.getId()),
                token);
        if (response != null && response == 0) {
            responseVo.setCode(1);
            return responseVo;
        } else {
            //令牌校验成功,去创建订单,校验价格,所库存
            submitVoThreadLocal.set(orderSubmitVo);
            //1、创建订单、订单项等信息
            OrderCreateTo order = this.createOrder();
            //2、验证价格
            BigDecimal payAmount = order.getOrder().getPayAmount();
            double substractValue = 0.01;
            if (Math.abs(orderSubmitVo.getPayPrice().subtract(payAmount).doubleValue()) < substractValue) {
                //3、保存订单
                this.saveOrder(order);
                //4、库存锁定,只要有异常,回滚订单数据
                //订单号、所有订单项信息(skuId,skuNum,skuName)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());

                //获取出要锁定的商品数据信息
                List<OrderItemVo> 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);

                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    //锁定成功
                    responseVo.setOrder(order.getOrder());
                } else {
                    //库存锁定失败
                    responseVo.setCode(3);
                }
                return responseVo;
            } else {
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }

向前端页面返回订单提交的数据响应

cfmall-order/src/main/java/com/gyz/cfmall/order/web/OrderWebController.java

    @Resource
    private OrderService orderService;

	@PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo orderSubmitVo, Model model) {
        SubmitOrderResponseVo responseVo = orderService.submitOrder(orderSubmitVo);
        if (responseVo.getCode() == 0) {
            model.addAttribute("submitOrderResp", responseVo);
            //下单成功,支付
            return "pay";
        }
        //下单失败,重新下单
        return "redirect:http://order.cfmall.com/toTrade";
    }