谷粒商城笔记+踩坑(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

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

}

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取出登录的用户
     */
}

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

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

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

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

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

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


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

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

1)、封装提交订单数据

package com.atguigu.gulimall.order.vo;

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

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

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

4.1、抽取订单创建传输类

@Data
public class OrderCreateTo {

    private OrderEntity order;

    private List<OrderItemEntity> orderItems;

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

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

}

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<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
    // 4、计算价格、积分等相关信息
    computePrice(orderEntity,itemEntities);

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

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

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

4.3.2、封装运费模型类

package com.atguigu.gulimall.order.vo;

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

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

4.4、构造订单项数据

4.4.1、构建订单项数据service

OrderServiceImpl 类

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

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

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

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

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

4.5、计算价格

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

5、锁定库存

在这里插入图片描述

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

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

}

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

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

5.2.3、锁定库存的vo类

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

@Data
public class WareSkuLockVo {

    private String orderSn;

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



}

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

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

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {

    @Autowired
    WareSkuDao wareSkuDao;

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

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

}

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

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

}

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

<update id="addStock">
    UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
</update>

<select id="listWareIdHashSkuStock" resultType="java.lang.Long">
    SELECT ware_id FROM wms_ware_sku WHERE sku_id=#{skuId} and stock-stock_locked>0;
</select>

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

6、前端页面的修改


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

<div class="Jdbox_BuySuc">
  <dl>
    <dt><img src="/static/order/pay/img/saoyisao.png" alt=""></dt>
    <dd>
      <span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
      <span>应付金额<font>[[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font>元</span>
    </dd>
    <dd>
      <span>推荐使用</span>
      <span>扫码支付请您在<font>24小时</font>内完成支付,否则订单会被自动取消(库存紧订单请参见详情页时限)</span>
      <span>订单详细</span>
    </dd>
  </dl>
</div>

在这里插入图片描述

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


<p class="p1">填写并核对订单信息 <span style="color: red" th:value="${msg!=null}" th:text="${msg}"></span></p>

在这里插入图片描述

在这里插入图片描述

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

}

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/5722.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ : C++基础 :从内存的角度看 char[]和char*

char*和char[]区别1&#xff1a;数据在内存中的存储2&#xff1a;char*和 char[]分析3&#xff1a;char* p2 和 char p1[]3.1 修改指针所指向的地址4: string转char*5: char * 转string5.1 to_string()用法1&#xff1a;数据在内存中的存储 栈&#xff1a;就是在那些由编译器在…

PYQT 自带的 Pyrcc 系统的使用,PyInstaller对PYQT程序进行打包,不能打包背景图片,图标等解决办法

问题 使用 PyInstaller 对程序进行打包&#xff0c;不能打包背景图片。打包后的软件可以正常运行&#xff0c;但涉及到图片相关的资源全部不显示。 问题分析 当使用Python PyInstaller对程序进行打包时&#xff0c;如果程序中涉及到背景图片&#xff0c;会出现无法打包背景图…

第十一章 指针

第十一章 指针 目录一&#xff0e; 指针变量二&#xff0e; 取地址运算符和间接寻址运算符三&#xff0e; 指针赋值一&#xff0e; 指针变量 概述   指针就是地址&#xff0c;而指针变量就是存储地址的变量。指针的大小都是相同的。32位机器一个地址是4个byte。64位机器一个…

【ChatGPT】这是一篇ChatGPT写的关于Python的文章

文章目录Python基础语法教学1、变量2、数据类型3、运算符4、条件语句5、循环语句更高级的概念1、函数2、模块3、面向对象编程ChatGPT的记录Python基础语法教学 Python是一种高级编程语言&#xff0c;它被广泛应用于计算机科学领域、数据分析和人工智能等各种领域。在学习Pytho…

聊聊MyBatis缓存机制(一)

前言 Mybatis是常见的Java数据库访问层框架&#xff0c;虽然我们在日常的开发中一般都是使用Mybatis Plus&#xff0c;但是从官网信息可以知道&#xff0c;其实Mybatis Plus只是让开发者在使用上更简单&#xff0c;并没有改动核心原理。在日常工作中&#xff0c;大多数开发者都…

HTML5 <!DOCTYPE> 标签

实例 <!DOCTYPE> 声明非常重要&#xff0c;它是一种标准通用标记语言的文档类型声明&#xff0c;通过该标签&#xff0c;浏览器能够了解HTML5文档正在使用的HTML规范&#xff0c;<!DOCTYPE> 声明是HTML5文档的起始点&#xff0c;也就是说它必须位于HTML5文档的第一…

《SpringBoot》第03章 自动配置机制(二) 根注解@SpringBootApplication

前言 之前介绍到了把启动类封装成BeanDefinition注入进IOC容器&#xff0c;那么这个启动类就会跟普通的bean一样在refresh()中被实例化&#xff0c;那么显而易见作为启动类这个实例化并不简单&#xff0c;肯定会存在一些特殊处理&#xff0c;那么就需要研究一下其注解SpringBo…

AI只会淘汰不进步的程序员

最近AI界的大新闻有点多&#xff0c;属于多到每天很努力都追不上&#xff0c;每天都忙着体验各种新产品或申请试用新产品。各种自媒体肯定也不会放过这个机会&#xff0c;AI取代程序员的文章是年年有&#xff0c;今天特别多。那么AI到底会不会取代程序员的工作呢&#xff1f;先…

[chapter4][5G-NR][传输方案]

前言&#xff1a; 多天线传输的基本过程传输方案 前面见过数据加扰&#xff0c;调制&#xff0c;层映射的一些基本原理&#xff0c;算法。 这里重点讲一下传输方案 目录&#xff1a; 1&#xff1a; 下行传输方案 2&#xff1a; 上行传输方案 3&#xff1a; 资源块映射 备注&…

.net开发安卓从入门到放弃 最后的挣扎(排查程序闪退问题记录-到目前为止仍在继续)

安卓apk闪退问题排查记录logcat程序包名先看日志&#xff08;以下日志是多次闪退记录的系统日志&#xff0c;挑拣几次有代表性的发上来&#xff09;最近一次闪退adb shell tophelp一个demo说明adb shell dumpsys meminfo <package_name>ps&#xff1a;写在前面&#xff0…

训练中文版chatgpt

文章目录1. 斯坦福的模型——小而低廉&#xff1a;Alpaca: A Strong Open-Source Instruction-Following Model2. Meta 模型&#xff1a;LLaMA&#xff1a;open and efficient foundation language models3.ChatGLM4.斯坦福开源机器人小羊驼Vicuna&#xff0c;130亿参数匹敌90%…

SSM+LayUi实现的学籍管理系统(分为管理员、教师、学生三个角色,实现了专业管理,班级管理,学生管理,老师管理,课程管理,开课管理以及用户管理等)

博客目录jspservletmysql实现的停车场管理系统实现功能截图系统功能使用技术完整源码jspservletmysql实现的停车场管理系统 本系统是一个servlet原生框架实现的停车场管理系统&#xff0c;总共分为两个角色&#xff0c;普通用户和管理员&#xff0c;实现了用户管理、停车信息管…

Linux基础IO

本篇博客来讲述Linux中的新一模块--文件IO&#xff0c;我们来做简单的介绍和陈述。 在笔者之前的文章之中&#xff0c;已经对C语言中的文件操作做了简要介绍&#xff0c;我们旧事重提&#xff0c;再次进行一个简要的回顾。 目录 1.文件的操作 1.1打开文件 1.2向文件写入数…

Java多态

目录 1.多态是什么&#xff1f; 2.多态的条件 3.重写 3.1重写的概念 3.2重写的作用 3.3重写的规则 4.向上转型与向下转型 4.1向上转型 4.2向下转型 5.多态的优缺点 5.1 优点 5.2 缺点 面向对象程序三大特性&#xff1a;封装、继承、多态。 1.多态是什么&#xff1…

七结(4.2)遍历集合与javaFX界面

今天由学长学界们进行了一次授课&#xff0c;算是温习了一遍面向对象的知识&#xff0c;同时配置了关于javaFX的环境&#xff0c;以及一些关于项目的知识。 java学习总结&#xff1a; Collection的遍历&#xff1a; 迭代器遍历&#xff08;Iterator&#xff09;&#xff1a;…

leetcode 87. Scramble String(扰乱字符串)

scramble(字符串s) 算法&#xff1a; s长度为1时结束。 s可以拆分成2部分x和y&#xff0c;sxy, 这两部分可以交换&#xff0c;也可以不交换&#xff0c;即 s xy 或 s yx. 上面scramble还会递归作用于x 和 y. 给出相同长度的字符串s1, s2, 问s2是否可通过scramble(s1)获得。 …

WTW-16P 应用电路

1、WTW-16P 按键控制 PWM 输出应用电路 软件设置&#xff1a; 按键控制模式。 I/O 口定义&#xff1a; 选取 I/O 口 P00、P01、P02、P03 作为触发口&#xff0c;在编辑 WT588D 语音工程时&#xff0c;把触发口的按键定义为可触发播放的触发方式&#xff0c;就可进行工作。 BUS…

如何提高网站安全防护?

网站的安全问题一直是很多运维人员的心头大患&#xff0c;一个网站的安全性如果出现问题&#xff0c;那么后续的一系列潜在危害都会起到连锁反应。就好像网站被挂马&#xff0c;容易遭受恶意请求呀&#xff0c;数据泄露等等都会成为杀死网站的凶手。 1、让服务器有一个安全稳定…

百度双塔召回引擎MOBIUS

1. 概述 对于一个搜索或者推荐系统来说&#xff0c;分阶段的设计都是当下的一个标配&#xff0c;主要是为了平衡效率和效果&#xff0c;在百度的广告系统中&#xff0c;也是分成了如下的三层结构&#xff1a; 最上层的Matching阶段负责从全库中找到与query相关的候选集&#x…

KD2511N系列微电阻测试仪

一、产品概述 KD2511N系列微电阻测试仪是一款对变压器、电机、开关、继电器、接插件等各类直流电阻进行测试的仪器。其基本测试精度最高可达 0.05%&#xff0c;并具有较高的测量速度。 KD2511N微电阻测试仪使用了高精度恒流流经被测件以及四端测量&#xff0c;有效的消除了 引线…