黑马点评之达人探店(点赞、点赞排行榜)

点赞功能

业务逻辑分析

该点赞功能业务旨在实现用户对博客的点赞与取消点赞操作,同时确保每个用户仅能点赞一次,且前端能根据点赞状态高亮显示按钮。

  • 数据结构设计:
    • 在Blog类中添加isLike字段,用于标识当前登录用户是否对该博客点赞。
  • 点赞逻辑(利用Redis的Set集合):
    • "blog:liked:" + 博客id 为键,value为用户ID集合。检查用户ID是否存在于对应博客的Set中:
      • 若不存在,将用户ID加入Set,并将博客点赞数 +1 (表示点赞成功)
      • 若存在,从Set中移除用户ID,并将博客点赞数 -1 (表示取消点赞)
SADD blog:liked:1001 123  // 用户ID=123点赞博客ID=1001
SREM blog:liked:1001 123  // 用户取消点赞
SISMEMBER blog:liked:1001 123  // 检查用户是否已点赞
  • 查询逻辑:
    • 单个博客查询:根据博客ID查询时,检查当前登录用户ID是否在 blog:{blogId}:likes 的Set中,若存在则 isLike = true,否则 isLike = false。
    • 分页查询:对分页结果中的每个博客,执行上述检查,为每个博客的isLike字段赋值。
  • 前端展示:
    • 前端根据isLike字段的值,决定点赞按钮是否高亮显示(已实现)。

避免重复点赞:使用Redis的Set集合存储每个博客的点赞用户ID,通过SADD、SREM、SISMEMBER命令实现用户唯一性校验,确保单用户仅能点赞一次。

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private IUserService userService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 查询热门博客(分页查询)
     * @param current
     * @return
     */
    @Override
    public Result queryHotBlog(Integer current) {
        // 1.根据用户查询
        Page<Blog> page = query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 2.获取当前页数据
        List<Blog> records = page.getRecords();
        // 3.查询用户 以及 blog是否被点赞了(给isLike属性赋值)
        records.forEach(blog -> {
            this.queryBlogUser(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    /**
     * 根据id查询博客(单个查询)
     *
     * @param id
     * @return
     */
    @Override
    public Result queryBlogById(Long id) {
        // 1.查询博客blog
        Blog blog = getById(id);
        if (blog == null) {
            return Result.fail("笔记不存在!");
        }
        // 2.查询blog有关的用户
        queryBlogUser(blog);
        // 3.查询blog是否被点赞了(给isLike属性赋值)
        isBlogLiked(blog);
        return Result.ok(blog);
    }

    /**
     * 判断博客是否被点赞了
     * @param blog
     */
    private void isBlogLiked(Blog blog) {
        // 1. 获取登录用户(判空)
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            // 未登录用户默认未点赞
            blog.setIsLike(false);
            return;
        }
        Long userId = user.getId();

        // 2. 判断是否点赞
        String key = "blog:liked:" + blog.getId();
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        blog.setIsLike(BooleanUtil.isTrue(isMember));
    }


    /**
     * 点赞博客
     *
     * @param id
     * @return
     */
    @Override
    public Result likeBlog(Long id) {
        //1.获取登录用户
        Long userId = UserHolder.getUser().getId();
        //2.判断当前登录用户是否已经点赞
        String key = "blog:liked:" + id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        if (BooleanUtil.isFalse(isMember)) {
            //3.如果未点赞,可以点赞
            //3.1 数据库点赞数量+1
            boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
            //3.2 保存用户到redis的set集合中
            if(isSuccess){
                stringRedisTemplate.opsForSet().add(key, userId.toString());
            }
        } else {
            //4.如果已经点赞,取消点赞
            //4.1 数据库点赞数量-1
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            //4.2 将用户从redis的set集合中移除
            if(isSuccess){
                stringRedisTemplate.opsForSet().remove(key, userId.toString());
            }
        }
        return Result.ok();
    }


    /**
     * 查询用户
     *
     * @param blog
     */
    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }

}

    点赞排行榜

    1. 核心需求

    • 展示给用户“最早点赞该博客的TOP N用户列表”

    2. 技术实现

    数据结构选择:

    • Redis Sorted Set(ZSet):存储每个博客的点赞用户ID及时间戳(作为排序依据)。
      • Key格式:BLOG_LIKED_KEY + 博客id
      • Value格式:用户id + 时间戳(作为排序依据)

    3. 业务逻辑

    • 写入排行榜:用户点赞时,记录时间戳到ZSet
    stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
    • 查询排行榜:
      • 按时间升序(最早点赞在前)获取TOP N用户
      • Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);  //前5名
      • 根据用户ID列表查询用户信息(头像、昵称)
      • 顺序保持:SQL中使用IN语句和 ORDER BY FIELD () 保证返回顺序与Redis中一致
    // 3.根据用户id查询用户  where id in(5,1) ORDER BY FIELD(id ,5, 1)
            String idStr = StrUtil.join(",", ids);
            List<User> users = userService.query()
                    .in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
            //将User对象转换为UserDTO对象
            List<UserDTO> userDTOS = users
                    .stream()
                    .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                    .collect(Collectors.toList());

    实现代码

    代码功能解析

    1. 点赞功能(likeBlog方法)
    /**
         * 点赞博客
         *
         * @param id
         * @return
         */
        @Override
        public Result likeBlog(Long id) {
            //1.获取登录用户
            Long userId = UserHolder.getUser().getId();
            //2.判断当前登录用户是否已经点赞
            String key = BLOG_LIKED_KEY + id;
            Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
            if (score == null) {
                //3.如果未点赞,可以点赞
                //3.1 数据库点赞数量+1
                boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
                //3.2 保存用户到redis的set集合中  zadd key value score
                if (isSuccess) {
                    stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
                }
            } else {
                //4.如果已经点赞,取消点赞
                //4.1 数据库点赞数量-1
                boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
                //4.2 将用户从redis的set集合中移除
                if (isSuccess) {
                    stringRedisTemplate.opsForZSet().remove(key, userId.toString());
                }
            }
            return Result.ok();
        }
    • 实现逻辑

      • 使用ZSet检查用户是否已点赞(score是否存在)。

      • 未点赞时:数据库liked字段+1,并写入Redis(用户ID + 时间戳)。

      • 已点赞时:数据库liked字段-1,并从Redis删除用户ID。

    • 优点

      • 使用时间戳作为排序依据,天然支持最早点赞排行。

      • 通过ZSetscore判断用户是否点赞,效率较高。

    2. 查询点赞排行榜(queryBlogLikes方法)
    /**
         * 查询点赞列表(排行榜)
         *
         * @param id
         * @return
         */
        public Result queryBlogLikes(Long id) {
            String key = BLOG_LIKED_KEY + id;
            // 1.查询top5的点赞用户(最早点赞的前五位用户) zrange key 0 4   查到的是redis中的值即是userId
            Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
            if (top5 == null || top5.isEmpty()) {
                return Result.ok(Collections.emptyList());
            }
    
            // 2.解析出这些用户id(将Redis查询到的字符串类型用户ID集合(Set<String>)转换为Long类型列表)
            //创建流管道  使用Long.valueOf转换每个元素   收集元素到List集合。
            List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
    
            /*List<Object> ids = new ArrayList<>();
            //top5.forEach(s -> ids.add(Long.valueOf(id)));
            for (String s : top5) {
                ids.add(Long.valueOf(s));
            }*/
    
            // 3.根据用户id查询用户  where id in(5,1) ORDER BY FIELD(id ,5, 1)
            String idStr = StrUtil.join(",", ids);
            List<User> users = userService.query()
                    .in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
            //将User对象转换为UserDTO对象
            List<UserDTO> userDTOS = users
                    .stream()
                    .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                    .collect(Collectors.toList());
    
            // 4.将用户信息返回
            return Result.ok(userDTOS);
        }
    • 实现逻辑

      • 从Redis的ZSet中获取前5个用户ID(按时间升序)。

      • 将用户ID转换为Long列表,使用ORDER BY FIELD保持顺序。

      • 转换为UserDTO返回前端(脱敏)。

    • 优点

      • 通过ORDER BY FIELD保证数据库查询结果与Redis顺序一致。

      • 返回脱敏后的UserDTO,避免敏感信息泄露。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值