秒杀代码解析

尚硅谷秒杀代码解析

//detail.html 代码快1
<li>
	<a href="javascript:" v-if="isBuy" @click="generateSeckillCode()" class="sui-btn  btn-danger addshopcar">立即抢购</a>
	<a href="javascript:" v-if="!isBuy" class="sui-btn  btn-danger addshopcar" disabled="disabled">立即抢购</a>
</li>

看代码会发现点击“立即抢购”的时候先调用generateSeckillCode()这个方法,然后我们看generateSeckillCode()方法

//detail.html  代码快2
generateSeckillCode() {
   //debugger
   seckill.generateSeckillCode(this.skuId).then(response => {
   var seckillCode = response.data.data
   window.location.href = '/seckill-queue.html?skuId='+this.skuId+'&seckillCode='+seckillCode
   })
   },

从代码中看出要进入这个方法,需要拿取抢购码,然后重定向到的页面(这里可以看出前端有控制必须有抢购码才能进入秒杀方法)

//seckill.js 代码快3
// 获取秒杀参数
    generateSeckillCode(skuId) {
        return request({
            url: this.api_name + '/generateSeckillCode/' + skuId,
            method: 'get'
        })
    },

从js文件中我们可以看到获取抢购码的接口 请求方式是get,所以我们想写一个生成抢购码的接口,参数是skuId

 /**
     * 3.生成抢购码,防止用户直接跳转到商品详情页面进行秒杀
     * http://api.gmall.com/seckill/generateSeckillCode/34
     * @param skuId
     * @param request
     * @return
     */
    @GetMapping("generateSeckillCode/{skuId}")
    public RetVal generateSeckillCode(@PathVariable Long skuId, HttpServletRequest request){
        //1.判断用户是否登录
        String userId = AuthContextHolder.getUserId(request);
        if(StringUtils.isNotEmpty(userId)) {
            //2.从缓存中拿到秒杀商品的信息
            SeckillProduct seckillProduct = seckillProductService.getSeckillProductBySkuId(skuId);
            //Date currentTime = new Date();
            Date currentTime = new Date();
            //TODO 二、判断秒杀商品是否在秒杀时间之内
            if (DateUtil.dateCompare(seckillProduct.getStartTime(), currentTime) &&
                    DateUtil.dateCompare(currentTime, seckillProduct.getEndTime())) {
                //3.生成抢购码
                //TODO 三、生成抢购码
                String seckillCode = MD5.encrypt(userId);
                return RetVal.ok(seckillCode);
            }
        }
        return RetVal.fail().message("获取抢购码失败,请先登录");
    }

代码解析:
1.请求方式与前端一致,get请求,参数是skuId,所以请求头是@GetMapping(“generateSeckillCode/{skuId}”)
2.方法名要和前端的请求路径一致所以使用写成generateSeckillCode,参数的skuid是必要的,因为生成抢购码需要userId,需要从请求中拿到,所以要用request,从request中获得skuId。
3.需要usreId才能生成抢购码,所以必须在用户登录的情况下才能获取到skuId,所以需要先判断用户是否登录
4.根据用户id生成抢购码,并进行md5加密,不加密的话不安全
5.返回抢购码给前端,然后就进入generateSeckillCode方法了。
假设已经登录,skuid=34,抢购码为c84258e9c39059a89ab77d846ddab909
进入generateSeckillCode方法之后会发现拿到,会跳转
window.location.href = ‘/seckill-queue.html?skuId=’+this.skuId+‘&seckillCode=’+seckillCode这个地址,然后调用下边这个方法,把skuid和抢购码给后端传过去,跳转到排队页面

    //3.获取抢购码成功之后要去的页面
    @GetMapping("/seckill-queue.html")
    public String seckillQueue(Long skuId, String seckillCode, Model model) {
        model.addAttribute("skuId", skuId);
        model.addAttribute("seckillCode", seckillCode);
        return "seckill/queue";
    }

成功跳转到排队页面,继续看下面的前端代码

//2.html加载完成后执行
            mounted() {
                const timer = setInterval(() => {
                    if (this.code != 211) {
                        clearInterval(timer);
                    }
                    this.hasQualified()
                }, 3000);
                // 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
                this.$once('hook:beforeDestroy', () => {
                    clearInterval(timer);
                })
            },
            //1.html加载完成之前执行
            created() {
                this.prepareSeckill();
            },

从前端代码可以看出页面加载之前会调用prepareSeckill()方法,加载之后调用hasQualified()方法

methods: {
     prepareSeckill() {
     seckill.prepareSeckill(this.skuId, this.seckillCode).then(response => {
     console.log(JSON.stringify(response))
     if (response.data.code == 200) {
     this.isCheckOrder = true
     } else {
          this.show = 2
          this.message = response.data.message
          }
          })
          },
          hasQualified() {
          if (!this.isCheckOrder) return
          seckill.hasQualified(this.skuId).then(response => {
          //debugger
          this.data = response.data.data
          this.code = response.data.code
          console.log(JSON.stringify(this.data))
          //排队中
          if (response.data.code == 211) {
              this.show = 1
              } else {
              //秒杀成功
              if (response.data.code == 215) {
                  this.show = 3
                  this.message = response.data.message
                     } else {
                       if (response.data.code == 218) {
                        this.show = 4
                        this.message = response.data.message
                          } else {
                          this.show = 2
                          this.message = response.data.message
                         }
                    }
               }
           })
       }
 }

然后我们看js文件中对接口的定义要求

//预下单
    prepareSeckill(skuId, seckillCode) {
        return request({
            url: this.api_name + '/prepareSeckill/' + skuId + '?seckillCode=' + seckillCode,
            method: 'post'
        })
    },

    // 查询订单
    hasQualified(skuId) {
        return request({
            url: this.api_name + '/hasQualified/' + skuId,
            method: 'get'
        })
    },

然后根据这个我们在写出后端的接口
controller层
客户点击立即抢购,进入到排队页面,先进行prepareSeckill()预下单处理,这里需要对用户进行确认,看他的抢购码是否匹配,看秒杀商品的状态位是否正常,如果都正常的话,我们就可以进行抢购,然后把用户的id,抢购商品的id消息,发送给rabbitmq进行排队处理,

/**
     * 4.秒杀预下单
     * @param skuId
     * @param seckillCode 抢购码
     * @param request
     * @return
     */
    @PostMapping("prepareSeckill/{skuId}")
    public RetVal prepareSeckill(@PathVariable Long skuId ,String seckillCode,HttpServletRequest request){
        //1.抢购码是否匹配
        String userId = AuthContextHolder.getUserId(request);
        //TODO 四、判断用户抢购码是否匹配
        if(!MD5.encrypt(userId).equals(seckillCode)){
            //当用户预下单的时候用户不匹配,不能进行秒杀
            return RetVal.build(null, RetValCodeEnum.SECKILL_ILLEGAL);
        }
        //2.该商品是否可以秒杀 状态为是否正常
        //TODO 五、判断秒杀商品状态位是否为1
        String state = (String) redisTemplate.opsForValue().get(RedisConst.SECKILL_STATE_PREFIX + skuId);
        if(StringUtils.isEmpty(state)){
            return RetVal.build(null, RetValCodeEnum.SECKILL_ILLEGAL);
        }
        //可以进行抢购
        if (RedisConst.CAN_SECKILL.equals(state)){
            //3.如果可以秒杀,吧秒杀用户id和商品skuid发送给rabbitmq 等会处理
            UserSeckillSkuInfo userSeckillSkuInfo = new UserSeckillSkuInfo();
            userSeckillSkuInfo.setUserId(userId);
            userSeckillSkuInfo.setSkuId(skuId);
            //TODO 六、把消息放到rabbitmq中进行排队处理 这里下一步之后记得看rabbitmq页面是否有消息在队列中
            rabbitTemplate.convertAndSend(MqConst.PREPARE_SECKILL_EXCHANGE,MqConst.PREPARE_SECKILL_ROUTE_KEY,userSeckillSkuInfo);
        }else{
            return RetVal.build(null,RetValCodeEnum.SECKILL_FINISH);
        }
        return RetVal.ok();
    }

消费者消费rabbitmq队列中的消息,当消费者拿到rabbitmq中的消息,调用秒杀商品服务,对下单信息进行处理

/**
     * 预下单监听器
     * @param userSeckillSkuInfo 用户预下单详情
     * @param message rabbitmq生产者发布的消息 seckillController.prepareSeckill()方法中发布的消息
     * @param channel 发布多条消息的信道
     * @throws IOException
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = MqConst.PREPARE_SECKILL_QUEUE,durable = "false"),
            exchange = @Exchange(value = MqConst.PREPARE_SECKILL_EXCHANGE,durable = "false"),
            key = {MqConst.PREPARE_SECKILL_ROUTE_KEY}))
    public void prepareSeckill(UserSeckillSkuInfo userSeckillSkuInfo, Message message, Channel channel) throws IOException {
        //TODO 七、这里对rabbitmq的消息进行消费
        if (userSeckillSkuInfo!=null){
            //开始处理预下单
            seckillProductService.prepareSeckill(userSeckillSkuInfo);
        }
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }

调用秒杀商品服务中的预下单处理方法,
先是判断商品状态位是否正常,
然后判断用户是否已经下过单,
还有就是校验库存是否正常,
如果要是都符合的话,就把客户预下单的信息存到redis中,(userId,商品数量,商品信息,订单码),然后把商品数量更新到redis和mysql中

/**
     * 预下单消息处理
     * @param userSeckillSkuInfo
     */
    @Override
    public void prepareSeckill(UserSeckillSkuInfo userSeckillSkuInfo) {
        //TODO 八、rabbotmq对消息处理操作
        String userId = userSeckillSkuInfo.getUserId();
        Long skuId = userSeckillSkuInfo.getSkuId();
        String state = (String)redisTemplate.opsForValue().get(RedisConst.SECKILL_STATE_PREFIX+skuId);
        //判断商品是否已经卖完
        if(RedisConst.CAN_NOT_SECKILL.equals(state)){
            return;
        }
        //a.把用户抢购的商品放入缓存中,用于判断用户是否抢购过
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(RedisConst.PREPARE_SECKILL_USERID_SKUID + userId, skuId, RedisConst.PREPARE_SECKILL_LOCK_TIME, TimeUnit.SECONDS);
        //b.从库存中减库存,也就是从list集合中弹出一个
        //true下单成功,false至少是第二次下单,已经预下单过
        //TODO  这走
        if(!flag){
            return;
        }
        String stockSkuId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();
        //c.如果该库存没有了,通知其他结点商品不能秒杀
        if(StringUtils.isEmpty(stockSkuId)){
            redisTemplate.convertAndSend(RedisConst.PREPARE_PUB_SUB_SECKILL,skuId+":"+RedisConst.CAN_NOT_SECKILL);
            return;
        }
        //d.把用户抢购的预下单商品信息存储到redis中
        PrepareSeckillOrder prepareSeckillOrder = new PrepareSeckillOrder();
        prepareSeckillOrder.setUserId(userId);
        prepareSeckillOrder.setBuyNum(1);
        SeckillProduct seckillProduct = getSeckillProductBySkuId(skuId);
        prepareSeckillOrder.setSeckillProduct(seckillProduct);
        //设置订单码
        prepareSeckillOrder.setPrepareOrderCode(MD5.encrypt(userId+skuId));
        redisTemplate.boundHashOps(RedisConst.PREPARE_SECKILL_USERID_ORDER).put(userId,prepareSeckillOrder);
        //e.更新秒杀商品的库存信息到mysql+redis中
        updateSeckillStock(skuId);

    }

对redis和mysql秒杀商品的数量进行更新(如果商品数量为2的倍数时,对mysql的库存进行修改)

    private void updateSeckillStock(Long skuId) {
        //拿取商品剩余库存量库存
        Long stockCount = redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).size();

        //更新数据库设置更新频率
        if(stockCount%2==0){
            SeckillProduct seckillProduct = getSeckillProductBySkuId(skuId);
            seckillProduct.setStockCount(stockCount.intValue());
            baseMapper.updateById(seckillProduct);
            //更新redis缓存,为了给用户看有没有机会秒到
            redisTemplate.boundHashOps(RedisConst.SECKILL_PRODUCT).put(skuId,seckillProduct);
        }
    }

前端页面在执行完prepareSeckill()方法之后就继续执行了hasQualified()方法,去查看用户是否有抢购资格,

    // 查询订单
    hasQualified(skuId) {
        return request({
            url: this.api_name + '/hasQualified/' + skuId,
            method: 'get'
        })
    },
/**
     * 5.判断用户是否有抢购资格
     * @param skuId 商品信息
     * @param request 用来获取客户信息
     * @return
     */
    @GetMapping("hasQualified/{skuId}")
    public RetVal hasQualified(@PathVariable Long skuId ,HttpServletRequest request){
        //TODO 这里走完之后页面会显示 抢购成功
        String userId = AuthContextHolder.getUserId(request);
        return seckillProductService.hasQualified(skuId,userId);
    }
   

service层实现类代码
根据用户id来查询redis缓存中是否有预下单信息,如果有的话就返回抢单成功的状态码

    SECKILL_RUN(211, "正在排队中"),
    SECKILL_FINISH(213, "已售罄"),
    SECKILL_END(214, "秒杀已结束"),
    PREPARE_SECKILL_SUCCESS(215, "抢单成功"),
/**
     * 判断用户是否有抢购资格
     * @param skuId
     * @param userId
     * @return
     */
    @Override
    public RetVal hasQualified(Long skuId, String userId) {
        //判断用户是否有抢购资格
        // 1.查询redis中是否
        // 有用户id和skuid
        Boolean isExist = redisTemplate.hasKey(RedisConst.PREPARE_SECKILL_USERID_SKUID +userId);
        if(isExist){
            /**
             * 如果预下单有用户的预下单订单信息,说明用户预抢单成功
             * prepareSeckillOrder为空,name就说明用户已经下单成功,此时走下边第二步
             * prepareSeckillOrder不为空,那么用户就有抢购资格
             */
            PrepareSeckillOrder prepareSeckillOrder=(PrepareSeckillOrder)redisTemplate.boundHashOps(RedisConst.PREPARE_SECKILL_USERID_ORDER).get(userId);
            if(prepareSeckillOrder!=null){
                return RetVal.build(prepareSeckillOrder, RetValCodeEnum.PREPARE_SECKILL_SUCCESS);
            }
        }
        //第二步:如果用户已经下单(同一个用户购买同一件商品,才能到这里)
        Integer orderId = (Integer)redisTemplate.boundHashOps(RedisConst.BOUGHT_SECKILL_USER_ORDER).get(userId);
        if(orderId!=null){
            return RetVal.build(null, RetValCodeEnum.SECKILL_ORDER_SUCCESS);
        }
        // 2.查询redis中是否有预下单信息
        return RetVal.build(null,RetValCodeEnum.SECKILL_RUN);
        // 3.当用户预下单成功就具有抢购资格
    }
   

在前端页面我们可以看到,当用户抢购成功的时候,会有“去下单”可以点击

<div class="seckill_dev" v-if="show == 3">
            恭喜取得抢购资格&nbsp;&nbsp;
            <a href="/seckill-confirm.html" target="_blank">去下单</a>
        </div>

从代码我们可以看到如果我们获得抢购资格之后,点击“去下单”,然后跳转到"/seckill-confirm.html"页面

//秒杀确认订单信息
    @GetMapping("seckillConfirm")
    public RetVal seckillConfirm(HttpServletRequest request) {
        //TODO 这是支付页面详情
        String userId = AuthContextHolder.getUserId(request);
        //a.秒杀到的预售商品订单信息
        PrepareSeckillOrder prepareSeckillOrder = (PrepareSeckillOrder)redisTemplate.boundHashOps(RedisConst.PREPARE_SECKILL_USERID_ORDER).get(userId);
        if (prepareSeckillOrder==null){
            return RetVal.fail().message("请求非法");
        }
        //b.获取用户的收货地址
        List<UserAddress> userAddressList = userFeignClient.getAddressByUserId(userId);
        //c.用户秒到的商品
        SeckillProduct seckillProduct = prepareSeckillOrder.getSeckillProduct();
        //d.秒杀商品的订单明细
        OrderDetail orderDetail = new OrderDetail();
        orderDetail.setSkuId(seckillProduct.getSkuId());
        orderDetail.setSkuName(seckillProduct.getSkuName());
        orderDetail.setImgUrl(seckillProduct.getSkuDefaultImg());
        orderDetail.setSkuNum(prepareSeckillOrder.getBuyNum()+"");
        orderDetail.setOrderPrice(seckillProduct.getCostPrice());
        List<OrderDetail> orderDetailList = new ArrayList<>();
        orderDetailList.add(orderDetail);
        //e.返回数据给页面
        Map<String, Object> retMap = new HashMap<>();
        retMap.put("userAddressList",userAddressList);
        retMap.put("orderDetailList",orderDetailList);
        retMap.put("totalMoney",seckillProduct.getCostPrice());
        return RetVal.ok(retMap);
    }
 //4.订单信息确认页面
    @GetMapping("/seckill-confirm.html")
    public String seckillConfirm(Model model) {
        RetVal retVal = seckillFeignClient.seckillConfirm();
        if(retVal.isOk()){
            Map<String,Object> data = (Map<String,Object>)retVal.getData();
            model.addAllAttributes(data);
            return "seckill/confirm";
        }else{
            model.addAttribute("message",retVal.getMessage());
            return "seckill/confirm";
    }

未完待续…

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值