黑马点评(达人探店)

news2024/11/25 9:42:41

达人探店

在这里插入图片描述

一、发布探店笔记

发布探店笔记功能是项目本身就完成了的功能,他会把图片存在本地,有兴趣可以去看源码,在UploadCOntroller类下
在这里插入图片描述

二、查看探店笔记

这个功能项目本身是没有完成这个接口的,所以需要我们自己去完成。
在这里插入图片描述

三、点赞功能

项目本身已经实现了点赞功能
在这里插入图片描述
项目原本的写法是通过点击点赞标识,直接向数据库发起请求,更新liked字段,并没有加任何判断。
在这里插入图片描述
这样看似没问题,但是却不符合逻辑,因为一个用户可以对同一篇笔记多次点赞,而现实应该是点一次赞,再点一次取消赞。
所以我们需要根据下面的需求更新代码:
在这里插入图片描述
使用redis的ZSet集合存储(key:被点赞的博客id,value:点赞的用户id)
在这里插入图片描述
使用Zset是为了后面方便按照用户的点赞时间排序(展示最先点赞的5个用户)

    /**
     * 点赞功能
     *
     * @param id
     * @return
     */
    @Override
    public Result likeBlog(Long id) {
        ZSetOperations<String, String> opsForZSet = stringRedisTemplate.opsForZSet();
        //1.获取当前登录用户
        UserDTO user = UserHolder.getUser();
        //2.判断当前用户是否在点赞列表中
        String key = "blog:liked:" + id;
        Double score = opsForZSet.score(key, user.getId().toString());

        if (score == null) {
            //3.如果不在,则点赞
            //3.1 数据库对应blog的点赞数+1
            boolean isSuccess = this.update().setSql("liked = liked + 1").eq("id", id).update();
            //3.1 redis中blog对应的set集合添加当前用户
            if (isSuccess) {
                opsForZSet.add(key, user.getId().toString(), System.currentTimeMillis());
            }
        } else {
            //4.如果在,则取消点赞
            //4.1 数据库对应blog的点赞数-1
            boolean isSuccess = this.update().setSql("liked = liked - 1").eq("id", id).update();
            //4.2 redis中blog的set集合移除当前用户
            if (isSuccess) {
                opsForZSet.remove(key, user.getId().toString());
            }
        }
        return Result.ok();

    }

/**
     * 判断当前用户有没有给传来的博客点过赞
     * @param blog
     */
    private void isBlogLiked(Blog blog) {
        ZSetOperations<String, String> opsForZSet = stringRedisTemplate.opsForZSet();
        //1.获取当前登录用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            //用户未登录
            return;
        }
        //2.判断当前用户是否在点赞列表中
        String key = "blog:liked:" + blog.getId();
        Double score = opsForZSet.score(key, user.getId().toString());
        blog.setIsLike(score != null);
    }

点赞排行榜

在redis中使用ZSet存储用户点赞情况,key为博客的id,value为给这个博客点赞的用户id,并且每个entry都有一个score,这个score为当前点赞时间的距离1970.1.1的毫秒数,使用range方法获取前五个点赞的用户id。拿着用户id去数据库查出它们的头像等信息并展示。

四、关注和取关

这里没有什么特别要说的,只是拿着前端传来的字段修改数据库

@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    /**
     * 先判断是否关注,已关注则取关,未关注则关注
     *
     * @param id
     * @param isFollow 传来的参数为true,则是要关注,false则是要取关
     * @return
     */
    @Override
    public Result follow(Long id, Boolean isFollow) {
        Long userId = UserHolder.getUser().getId();
        String key = "follows:" + userId;
        SetOperations<String, String> opsForSet = stringRedisTemplate.opsForSet();
        if (BooleanUtil.isTrue(isFollow)) {
            Follow follow = new Follow();
            follow.setFollowUserId(id);
            follow.setUserId(userId);
            follow.setCreateTime(LocalDateTime.now());
            this.save(follow);
            //关注后还要将被关注的用户id存在redis中
            opsForSet.add(key, String.valueOf(id));
        } else {
            LambdaQueryWrapper<Follow> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(Follow::getFollowUserId, id).eq(Follow::getUserId, userId);
            this.remove(lambdaQueryWrapper);
            //取消关注后还要将被取消关注的用户从redis的set集合中移除
            opsForSet.remove(key, id);
        }
        return Result.ok();
    }

    /**
     * 判断当前传来的用户是否被当前用户关注了
     * @param id
     * @return
     */
    @Override
    public Result isFollow(Long id) {
        Long userId = UserHolder.getUser().getId();
//        LambdaQueryWrapper<Follow> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//        lambdaQueryWrapper.eq(Follow::getFollowUserId, id).eq(Follow::getUserId, userId);
//        Follow one = this.getOne(lambdaQueryWrapper);
//        if (one == null) {
//            return Result.ok(false);
//        } else {
//            return Result.ok(true);
//        }
        /**
         * 在添加了“关注后将关注用户id添加到redis存储”功能后,
         * 就不需要查询数据库来判断当前用户有咩有关注传来的用户了,直接在redis中判断
         */
        String key = "follows:" + userId;
        Boolean member = stringRedisTemplate.opsForSet().isMember(key, String.valueOf(id));
        if (BooleanUtil.isTrue(member)) {
            return Result.ok(true);
        } else {
            return Result.ok(false);
        }
    }
}

共同关注

共同关注是指userA和userB两个人关注的列表中相同的用户。
这里选择使用redis的Set数据结构求交集。
那么在向数据库中保存前端传来的关注信息的同时,还需要将当前登录用户关注的用户id在redis中保存一份,例如:这里的意思是,用户1010关注了用户2。

    /**
     * 先判断是否关注,已关注则取关,未关注则关注
     *
     * @param id
     * @param isFollow 传来的参数为true,则是要关注,false则是要取关
     * @return
     */
    @Override
    public Result follow(Long id, Boolean isFollow) {
        Long userId = UserHolder.getUser().getId();
        String key = "follows:" + userId;
        SetOperations<String, String> opsForSet = stringRedisTemplate.opsForSet();
        if (BooleanUtil.isTrue(isFollow)) {
            Follow follow = new Follow();
            follow.setFollowUserId(id);
            follow.setUserId(userId);
            follow.setCreateTime(LocalDateTime.now());
            this.save(follow);
            //关注后还要将被关注的用户id存在redis中
            opsForSet.add(key, String.valueOf(id));
        } else {
            LambdaQueryWrapper<Follow> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(Follow::getFollowUserId, id).eq(Follow::getUserId, userId);
            this.remove(lambdaQueryWrapper);
            //取消关注后还要将被取消关注的用户从redis的set集合中移除
            opsForSet.remove(key, id);
        }
        return Result.ok();
    }

在完成向数据库保存关注信息的同时向redis中存一份的功能后,
我们就可以获取用户1010,和用户1011的关注列表;
比如登录的用户是1010,他要查询他和1011的共同关注,只需要求他们两个关注列表的交集就可以了,使用redis的Set数据结构中的intersect方法就可以获得共同关注的userId。拿着id去数据库查用户头像,名称等信息。

    @Override
    public Result getCommonFollows(Long id) {
        SetOperations<String, String> opsForSet = stringRedisTemplate.opsForSet();
        Long userId = UserHolder.getUser().getId();
        String key = "follows:" + userId;
        String key2 = "follows:" + id;
        Set<String> intersect = opsForSet.intersect(key, key2);
        if(intersect == null || intersect.isEmpty()){
            return Result.ok(Collections.emptyList());
        }
        List<Long> userIdCollect = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
        List<User> users = baseMapper.selectBatchIds(userIdCollect);
        List<UserDTO> collect = users.stream().map(item -> BeanUtil.copyProperties(item, UserDTO.class)).collect(Collectors.toList());
        return Result.ok(collect);
    }

五、关注列表笔记推送

feed流

在这里插入图片描述

拉模式

拉模式是指张三,李四发布了笔记后,赵六查看收件箱时,会从发件箱拉取笔记到收件箱,再根据时间戳排序。这种模式又叫做读扩散,因为是等到要读的时候才拉取。
优点:
不占内存
缺点:
拉取耗时,因为要等到读的时候才拉取。
在这里插入图片描述

推模式

张三,李四在发送笔记时直接将笔记推到自己的所有粉丝的收件箱中。
优点:延时低,因为在发布笔记时就直接推给粉丝了,粉丝不必等打开收件箱时再拉取。
缺点:如果粉丝多,比如张三有一个亿的粉丝,那么他就需要推送一个亿份笔记,对内存要求较高
在这里插入图片描述

推拉结合

详情看视频
推拉结合
在这里插入图片描述
在这里插入图片描述

推模式的代码实现

实现思路

  • 用户A发布笔记,从数据库查出博主的粉丝id
  • 将笔记的id存到每个粉丝的收件箱中(收件箱是redis的一个ZSet集合,key为“feed:userId,value为博客id)
  • 使用ZSet是为了排序,score值设置为当前时间戳
    代码
    /**
     * 保存笔记,并且将笔记的id发送给笔记发布者的粉丝
     *
     * @param blog
     * @return
     */
    @Override
    public Result saveBlog(Blog blog) {
        // 1.获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        // 2.保存探店博文
        boolean save = blogService.save(blog);
        if (save) {
            ZSetOperations<String, String> ops = stringRedisTemplate.opsForZSet();
            //3.查询笔记发布者的所有粉丝 select * from tb_follow where follow_user_id = ?
            LambdaQueryWrapper<Follow> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(Follow::getFollowUserId, user.getId());
            List<Follow> list = followService.list(lambdaQueryWrapper);
            //4.推送笔记id给粉丝
            for (Follow follow : list) {
                //4.1 获取粉丝id
                Long userId = follow.getUserId();
                //4.2 推送
                ops.add(RedisConstants.FEED_KEY + userId, blog.getId().toString(), System.currentTimeMillis());
            }
        }
        // 5.返回笔记id
        return Result.ok(blog.getId());
    }

用户A发布了一篇笔记
在这里插入图片描述
则用户B的收件箱会收到博客id
在这里插入图片描述

实现粉丝关注页面的分页查询功能(滚动分页)

滚动分页原理分析
上面使用推模式将笔记id推送到了粉丝的收件箱,这一节我们将会根据笔记id查出笔记,并且会根据时间排序。
在这里插入图片描述
在这一节中会使用到ZSet的很多命令,我们先模拟一些数据,来测试一下滚动分页和普通分页的区别
数据:
在这里插入图片描述
操作:
先测试按照角标查询
倒序查询,并且结果带上分数
在这里插入图片描述
添加一条数据,再查询3条数据
此时我们使用zrevrange查询倒数3至5条数据(从0开始的),按照我们的想法,预期结果应该是3,2,1。但是结果却是4,3,2。这样导致数据4又被查询了一遍。
可能有人会说,再查一边就再查一边呗,没影响。
影响可大了,因为在实际使用中,推文或者博客的发布速度很快,可能短时间内就会发布很多条,那么作为粉丝/用户,就可能存在永远都刷不到3,2,1这三条数据。

在这里插入图片描述
按照分数查询
按照角标查询会出现重复查询的问题,所以我们需要使用按照分数查询,Zset提供了这样的命令
在这里插入图片描述
这条命令的含义是从分数最大值1000开始查询,一直到0,limit后面的0是指偏移量,3为个数限制。
因为前面插入了一条数据,所以结果是7,6,5
在这里插入图片描述
这里再插入一条数据,查询时,max参数使用上次查询结果的最后一条数据的分数,min参数还是0,limit的offset参数切换为1,因为5是上次查询的结果,不需要再次查询了,所以偏移量设置为1,限制个数还是3。
查询结果为4,3,2
这里可以看到,使用分数分页查询,就不会出现重复查询的情况了
在这里插入图片描述
但是这样还是会出现一个情况,就是当数据的分数相同时,limit的参数offset会发生改变,会变成相同分数的个数,具体情况看视频的第10分钟后的内容redis Zset实现滚动查询

根据上面的情况总结出来命令的各个参数应该填什么:
在这里插入图片描述
下面我们将会实现这个接口
在这里插入图片描述
本次查询推送的最小时间戳会作为下次查询的lastId。
我们需要封装一个类来存放返回值。

package com.hmdp.dto;

import lombok.Data;

import java.util.List;

@Data
public class ScrollResult {
    private List<?> list;
    private Long minTime;
    private Integer offset;
}

BlogController接口

    @GetMapping("/of/follow")
    public Result queryBlogOfFollow(@RequestParam("lastId") Long max,@RequestParam(value = "offset",defaultValue = "0") Integer offset){
        return blogService.queryBlogOfFollow(max,offset);
    }

写在BlogService中的滚动分页方法

/**
     * 查询用户的关注者发布的笔记
     *
     * @param max    上次查询结果的分数的最小值
     * @param offset 偏移量
     * @return
     */
    @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        //1.获取当前用户
        UserDTO user = UserHolder.getUser();
        //2.查询收件箱
        ZSetOperations<String, String> szo = stringRedisTemplate.opsForZSet();
        Set<ZSetOperations.TypedTuple<String>> typedTuples = szo.reverseRangeByScoreWithScores(RedisConstants.FEED_KEY + user.getId(), 0, max, offset, 2);
        //3.非空判断
        if (typedTuples == null || typedTuples.isEmpty()) {
            return Result.ok();
        }
        //4.解析数据:blogId,score(时间戳),offset(需要我们从查出来的数据进行计算,得到偏移量)
        ArrayList<Long> blogIds = new ArrayList<>();
        long min = 0L;
        int returnOffset = 1;
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            //获取blogId
            Long id = Long.valueOf(Objects.requireNonNull(typedTuple.getValue()));
            blogIds.add(id);
            //获取时间戳,计算偏移量
            Long score = Objects.requireNonNull(typedTuple.getScore()).longValue();
            if (score == min) {
                returnOffset++;
            } else {
                min = score;
                returnOffset = 1;
            }
        }
        //5.查询博客
        String idStr = StrUtil.join(",", blogIds);
        List<Blog> blogs = query().in("id", blogIds).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Blog blog : blogs) {
            //查询出博客的用户信息
            queryBlogUser(blog);
            //查询出当前用户对博客的点赞情况
            isBlogLiked(blog);
        }
        //6.封装结果
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setOffset(returnOffset);
        scrollResult.setMinTime(min);
        return Result.ok(scrollResult);
    }

六、附近商户

GEO数据结构

练习:

在这里插入图片描述
1.添加上面的北京火车站地理信息
在这里插入图片描述
在redis中数据以Zset的形式存储
在这里插入图片描述

2.计算北京西站到北京站的距离
默认距离单位是
在这里插入图片描述
3.搜索天安门(116.397904 39.909005)附近10km内的所有火车站,并按照距离升序
我安装的redis是6.2版本之前的,所以只能使用georadius命令
在这里插入图片描述

附近商户搜索

在这里插入图片描述
数据库中的shop表存储了店铺的信息,将店铺的id和坐标存入geo数据结构,然后从redis中获取店铺的id(按照离当前用户的坐标的距离远近排序),然后根据店铺id从数据库中查询店铺的信息。
在这里插入图片描述
但是在使用时,我们查询店铺的信息都是按照店铺类型进行查询的,比如查询美食类,查询ktv类,所以在将店铺信息向redis中存储时还需要根据店铺类型进行分组。
在这里插入图片描述
编写一个单元测试,将数据库中的店铺id和地理信息存入redis

    /**
     * 将店铺的地理信息存入redis
     */
    @Test
    void loadShopData(){
        GeoOperations<String, String> geoOps = stringRedisTemplate.opsForGeo();
        //1.查询店铺信息
        List<Shop> list = shopServiceImpl.list();
        //2.把店铺分组,按照typeId分组,typeId一致的分到一个geo集合中
        //TODO 这是Stream流的写法,非常优雅,可以花时间学习一下 通过店铺类型id分类 生成一个map集合
        Map<Long, List<Shop>> collect = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        //3.分批完成,写入redis
        Set<Map.Entry<Long, List<Shop>>> entrySet = collect.entrySet();
        for (Map.Entry<Long, List<Shop>> longListEntry : entrySet) {
            Long key = longListEntry.getKey();
            List<Shop> value = longListEntry.getValue();
            //这样的话,每个店铺都要向redis发起一次请求
//            for (Shop shop : value) {
//                Long id = shop.getId();
//                Double x = shop.getX();
//                Double y = shop.getY();
//                geoOps.add(RedisConstants.SHOP_GEO_KEY+key,new Point(x,y),id.toString());
//            }
            //批量插入
            List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();
            for (Shop shop : value) {
                Long id = shop.getId();
                Double x = shop.getX();
                Double y = shop.getY();
                locations.add(new RedisGeoCommands.GeoLocation<String>(id.toString(),new Point(x,y)));
            }
            geoOps.add(RedisConstants.SHOP_GEO_KEY+key,locations);
        }
    }

结果显示存入了两个店铺分类1和2
在这里插入图片描述
编写接口和业务代码
在这里插入图片描述

    /**
     * 根据商铺类型分页查询商铺信息
     * @param typeId 商铺类型
     * @param current 页码
     * @return 商铺列表
     */
    @GetMapping("/of/type")
    public Result queryShopByType(
            @RequestParam("typeId") Integer typeId,
            @RequestParam(value = "current", defaultValue = "1") Integer current,
            @RequestParam(value = "x",required = false) Double x,
            @RequestParam(value = "y",required = false) Double y
    ) {
        return shopService.queryShopByTypeId(typeId,current,x,y);
//        // 根据类型分页查询
//        Page<Shop> page = shopService.query()
//                .eq("type_id", typeId)
//                .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
//        // 返回数据
//        return Result.ok(page.getRecords());
    }

   /**
     * 根据店铺的TypeId查询店铺信息
     *
     * @param typeId  店铺类型id
     * @param current 当前要获取数据的页码
     * @param x       当前用户的经度
     * @param y       当前用户的纬度
     * @return
     */
    @Override
    public Result queryShopByTypeId(Integer typeId, Integer current, Double x, Double y) {
        //1.判断是否需要根据坐标查询
        if (x == null || y == null) {
            // 根据类型分页查询
            Page<Shop> page = shopService.query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
        //2.计算分页参数
        //这个是分页参数的计算公式
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
        //3.查询redis,按照距离排序,分页 结果:shopId,distance
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                .newGeoRadiusArgs()//创建一个新的GeoRadiusCommandArgs对象
                .includeDistance()//查询结果包含与中心点的距离
                .limit(end);//查询从0开始到end结束的数据,所以分页还需要我们手动做逻辑分页
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().radius(
                RedisConstants.SHOP_GEO_KEY + typeId,
                new Circle(new Point(x, y), 5000),
                args
        );
        //非空判断
        if(results == null){
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        //如果查询出来的数据不足from个,则返回空,因为后续操作会使用strem流的skip对数据进行跳过,如果跳过了超出数据个数的次数,在stream流中的操作将会引发空指针
        if(list.size() <= from){
            return Result.ok(Collections.emptyList());
        }
        //4.解析出商户id
        //这里使用stream跳过from条数据是在做逻辑分页
        List<Long> ids = new ArrayList<>();//存储店铺id
        Map<Long,Distance> map = new HashMap<>();//存储店铺和店铺距离中心点的距离
        list.stream().skip(from).forEach(result ->{
            String id = result.getContent().getName();//获取店铺id
            Distance distance = result.getDistance();//获取店铺距离中心点的距离
            ids.add(Long.valueOf(id));
            map.put(Long.valueOf(id),distance);
        });
        //5.根据商户id查询数据库 这里要根据ids中的id顺序查询数据库
        String idStr = StrUtil.join(",",ids);
        List<Shop> shops=query().in("id",ids).last("ORDER BY FIELD(id,"+idStr+")").list();

        for (Shop shop : shops) {
            Long id = shop.getId();
            shop.setDistance(map.get(id).getValue());
        }
        //6.返回结果
        return Result.ok(shops);
    }

七、用户签到

bitmap签到用法

在这里插入图片描述

bitmap数据结构

在这里插入图片描述

练习:

使用bitset向ket为b的bitmap中添加数据,第0、1、2、4位设置为了1,第3位默认为0.
在这里插入图片描述
使用bitget查看,第0、1、2、4位都是1,第3位是0
在这里插入图片描述
使用bitfield查看多位
在这里插入图片描述
在这里插入图片描述
bitfield命令后面的参数很多,我们只想要他的查看功能
b是数据的key,get是查看,u是结果以无符号数字展示(对应的有i,有符号数字),u后面跟着的3是指查看几位,0是offset是指从第几位查起。
结果是7,我们前面插入的数据是11101,从0位开始查看3个,即111,转为十进制数就是7

签到功能实现

在这里插入图片描述
在这里插入图片描述

    @PostMapping("/sign")
    public Result sign(){
      return userService.sign();
    }

/**
     * 用户签到
     * @return
     */
    @Override
    public Result sign() {
        //1.获取当前登录用户
        UserDTO user = UserHolder.getUser();
        //2.获取当前日期
        LocalDateTime now = LocalDateTime.now();
        //获取年月
        String format = now.format(DateTimeFormatter.ofPattern("yyyy/MM"));
        //3.获取当前是该月第几天
        int dayOfMonth = now.getDayOfMonth();
        //4.拼接key前缀
        String key = USER_SIGN_KEY+user.getId()+format;
        //5.写入redis
        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);//月份中的天数是从1开始,而bitmap的第一位是0
        //6.返回结果
        return Result.ok();
    }

统计连续签到天数

在这里插入图片描述
问题2中的0是偏移量,dayOfMonth是今天是当前月的第几天,是指从0开始取多少个bit位。
重点:问题3,使用位运算
在这里插入图片描述

    /**
     * 统计当前用户本月截至今天连续签到天数
     * @return
     */
    @GetMapping("/sign/count")
    public Result signCount(){
        return userService.signCount();
    }

/**
     * 统计本月截至今天,连续签到天数
     *
     * @return
     */
    @Override
    public Result signCount() {
        //1.获取当前用户
        UserDTO user = UserHolder.getUser();
        //2.获取当前年月
        LocalDateTime now = LocalDateTime.now();
        String format = now.format(DateTimeFormatter.ofPattern("yyyy/MM"));
        //3.获取当前是本月第几天
        int dayOfMonth = now.getDayOfMonth();
        //4.获取bitmap中从0开始当今天代表的bit序列(得到一个十进制数
        String key = USER_SIGN_KEY + user.getId() + format;
        List<Long> list = stringRedisTemplate.opsForValue().bitField(
                key,
                BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
        );//为什么会得到一个list呢,因为bitField命令里面还有很多子命令,可能会用到多个子命令,会有多个结果,所以结果是一个list,
        if (list == null || list.size() == 0) {//返回结果为空,直接返回一个0,代表连续签到天数为0
            return Result.ok(0);
        }
        Long result = list.get(0);
        if(result==null){
            return Result.ok(0);
        }
        //5.遍历循环,统计本月截至今天,连续签到天数
        int count = 0;
        while(true){
            Long x = result & 1;
            if(x == 0){//如果当前bit序列最后一位是0,说明断签,退出循环
                break;
            }else {
                count++;//如果不为0,则count++ 连续签到天数加1
                result >>= 1;//bit序列右移一位,抛弃最后一个bit位,继续判读下一个bit位
            }
        }
        //6.返回结果
        return Result.ok(count);
    }

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

点击echart图即可获取对应代码的网址

PPChart - 让图表更简单 点击第一个数据中心即可获取对应的代码

在XAMPP环境中搭建wordpress网站教程

1、在xampp的安装目录中找到htdocs文件夹&#xff0c;在此文件夹中建立新文件夹&#xff0c;作本地网站要目录&#xff0c;如&#xff0c;wp-jianzhanpress。 2、将解压后的wordpress程序&#xff0c;放到该目录下。 3、启动XAMPP程序&#xff0c;点击“admin”。进入数据库管理…

Centos 7 下安装Redis

官网地址&#xff08;英文&#xff09;&#xff1a;Redis 官网地址&#xff08;中文&#xff09;&#xff1a;CRUG网站 or redis中文文档 Redis源码地址&#xff1a;GitHub - redis/redis: Redis is an in-memory database that persists on disk. The data model is key-v…

媲美postman?这款国产测试工具你知道吗

没有测试数据的用例就像一盘散沙&#xff0c;跑两步就跑不动了 没有测试数据&#xff0c;所谓的功能测试和性能测试全都是无米之炊。但我发现一个蛮诡异的事情&#xff0c;就是行业内很少会有人去强调测试数据的重要性&#xff0c;甚至市面上都没有人在做测试数据这门生意。 …

JMeter笔记(二)

个人学习笔记&#xff08;整理不易&#xff0c;有帮助点个赞&#xff09; 笔记目录&#xff1a;学习笔记目录_pytest和unittest、airtest_weixin_42717928的博客-CSDN博客 目录 一&#xff1a;了解常用组件 二&#xff1a;创建测试计划 1&#xff09;先新建一个测试计划 2&…

了解三维展厅模型从这里开始

引言&#xff1a; 随着科技的不断进步&#xff0c;展览方式也在不断演变。在这个数字化时代&#xff0c;三维展厅模型正成为展览领域的新宠。三维展厅模型通过结合计算机图形技术和虚拟现实技术&#xff0c;为观众带来身临其境的展览体验。 一&#xff0e;三维展厅模型的定义与…

ARM day7 (串口协议)

实验一 键盘输入一个字符a,串口工具显示b uart4.h #ifndef __UART4_H__ #define __UART4_H__#include "stm32mp1xx_gpio.h" #include "stm32mp1xx_rcc.h" #include "stm32mp1xx_uart.h"//初始化UART4 void hal_uart4_init();//发送一个字符 v…

MBD开发 STM32 CAN

Matlab/Simulink之STM32开发-CAN接收 - 知乎 Matlab/Simulink之STM32开发-CAN发送 - 知乎 目录 can发送 can接收 can发送 一直报这个错误&#xff0c;不用管 手动指定信号 can接收 CAN通讯中断选择&#xff1a;USB low priority or CAN RX0 interrupts CAN报文的接收模型主…

Mysql-事务及索引

事务 概述 用来统一sql语句的操作 防止删了这种情况的发生 删了部门&#xff0c;但是删员工的出错了没删成 事务中的语句要么全部都运行成功&#xff0c;要么全部都不运行成功 且可以撤销事务的操作&#xff1a;叫事务回滚 介绍 正常不开启事务 就每一条语句都是一个事务 …

安装jupyter notebook及插件

pip命令 pip install jupyter notebook 安装插件的pip pip install jupyter_nbextensions_configurator pip install jupyter_contrib_nbextensions jupyter nbextensions_configurator enable --user jupyter contrib nbextension install --user 输入jupyter notebook &…

vue 递归

目录 1. 树结构递归效果图 2. 代码&#xff1a; 1. 树结构递归效果图 ps &#xff1a;递归说白了就是自己掉自己。 2. 代码&#xff1a; //1. 调查询表格的接口_this.$API.departmentGetTreeList().then((res) > {if (res.data.code 200) {this.loading false;let temp…

long类型值与bytes数组互转

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

对于float或者double的集合求解交集

对于一般的集合求解交集&#xff0c;我们直接使用std::set_intersection即可&#xff0c;但是float和double都有精度问题&#xff0c;如果直接求交集&#xff0c;会认为比如0.9999和1.0001是两个数&#xff0c;造成并没有真正取得交集&#xff0c;其实这个函数实现也很容易&…

【UnityDOTS 十一】SharedComponent介绍

SharedComponent介绍 SharedComponent内存图 共享组件的值数组在单独的SharedComponentDataArrary中。每个Chunk中有一个单独的Handle指向这个值。 所以这个Chunk中放的不只是ArcheType相同的Entity&#xff0c;他们所指向的ShareComponent值也是相同的。 同时修改一个Entity…

网络知识点之-STP协议

STP&#xff08;Spanning Tree Protocol&#xff09;是生成树协议的英文缩写&#xff0c;可应用于计算机网络中树形拓扑结构建立&#xff0c;主要作用是防止网桥网络中的冗余链路形成环路工作。但某些特定因素会导致STP失败&#xff0c;要排除故障可能非常困难&#xff0c;这取…

❤ VUE3 项目具体配置(二)

❤ VUE3 项目具体配置&#xff08;二&#xff09; 一、create-vue快速生成项目原理 介绍&#xff1a; 前段时间我们有去探索了一下vue-cli、cra的原理&#xff0c;生成项目的过程&#xff0c;他是基于webpack的&#xff0c;但是今天我们的主角是create-vue&#xff0c;他是基…

用final修饰java方法的参数

当Java方法的参数用final修饰&#xff1a; 如果输入参数是简单类型&#xff0c;那么在被调用函数内部不能修改参数的值。如果输入参数是对象的引用&#xff0c;那么在被调用函数内部不能改变对象的引用&#xff0c;即必须引用同一个对象&#xff0c;但可以修改对象的属性。 代…

数据预处理之数据规约

目录 一、前言 二、PCA的主要参数&#xff1a; 三、数据归约任务1 四、数据规约任务2 一、前言 PCA(Principal Component Analysis)&#xff0c;即主成分分析方法&#xff0c;是一种使用最广泛的数据降维算法。PCA的主要思想是将n维特征映射到k维上&#xff0c;这k维是全新…

第三十九章Java成员方法的声明和调用

声明成员方法可以定义类的行为&#xff0c;行为表示一个对象能够做的事情或者能够从一个对象取得的信息。类的各种功能操作都是用方法来实现的&#xff0c;属性只不过提供了相应的数据。一个完整的方法通常包括方法名称、方法主体、方法参数和方法返回值类型&#xff0c;其结构…

9、动手学深度学习——使用块的网络(VGG)

1、VGG块 虽然AlexNet证明深层神经网络卓有成效&#xff0c;但它没有提供一个通用的模板来指导后续的研究人员设计新的网络。 在下面的几个章节中&#xff0c;我们将介绍一些常用于设计深层神经网络的启发式概念。 与芯片设计中工程师从放置晶体管到逻辑元件再到逻辑块的过程…