黑马点评学习笔记(好友关注篇)

关注和取关

  • 关注功能:如果 isFollow 为 true,用户会关注目标用户;如果为 false,则会取消关注目标用户。对应的关注关系会被保存或删除。
  • 是否关注功能:通过查询当前用户是否已经关注目标用户来返回是否关注的结果。
  • 主要的数据库操作是通过 Follow 实体类实现的,使用了 MyBatis-Plus 提供的工具(如 saveremovecount 等)来进行 CRUD 操作。

当用户希望关注或取消关注时,后端会通过 FollowService 中的 follow() 方法,依据传入的 isFollow 参数来保存或删除数据库中的关注记录。查询是否已关注功能则通过 isFollow() 方法,结合 MyBatis-Plus 提供的查询功能来判断当前用户与目标用户之间是否存在关注关系。Follow 实体类用于映射数据库中的关注记录,数据库操作通过 MyBatis-Plus 提供的 saveremovecount 等方法实现。

@RestController
@RequestMapping("/follow")
public class FollowController {

    @Resource
    private IFollowService followService;

    /**
     * 关注用户
     * @param followUserId 关注用户的id
     * @param isFollow 是否已关注
     * @return
     */
    @PutMapping("/{id}/{isFollow}")
    public Result follow(@PathVariable("id") Long followUserId, @PathVariable Boolean isFollow){
        return followService.follow(followUserId, isFollow);
    }

    /**
     * 是否关注用户
     * @param followUserId 关注用户的id
     * @return
     */
    @GetMapping("/or/not/{id}")
    public Result isFollow(@PathVariable("id") Long followUserId){
        return followService.isFollow(followUserId);
    }
}

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

    /**
     * 关注用户
     *
     * @param followUserId 关注用户的id
     * @param isFollow     是否已关注
     * @return
     */
    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        Long userId = ThreadLocalUtls.getUser().getId();
        if (isFollow) {
            // 用户为关注,则关注
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            this.save(follow);
        } else {
            // 用户已关注,删除关注信息
            this.remove(new LambdaQueryWrapper<Follow>()
                    .eq(Follow::getUserId, userId)
                    .eq(Follow::getFollowUserId, followUserId));
        }
        return Result.ok();
    }

    /**
     * 是否关注用户
     *
     * @param followUserId 关注用户的id
     * @return
     */
    @Override
    public Result isFollow(Long followUserId) {
        Long userId = ThreadLocalUtls.getUser().getId();
        int count = this.count(new LambdaQueryWrapper<Follow>()
                .eq(Follow::getUserId, userId)
                .eq(Follow::getFollowUserId, followUserId));
        return Result.ok(count > 0);
    }
}

共同关注

共同关注的实现依赖于 Redis 中存储的用户关注列表(通过 Set 集合)

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

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private IUserService userService;

    /**
     * 关注用户
     *
     * @param followUserId 关注用户的id
     * @param isFollow     是否已关注
     * @return
     */
    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = FOLLOW_KEY + userId;
        if (isFollow) {
            // 用户为关注,则关注
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            boolean isSuccess = this.save(follow);
            if (isSuccess) {
                // 用户关注信息保存成功,把关注的用户id放入Redis的Set集合中,
                stringRedisTemplate.opsForSet().add(key, followUserId.toString());
            }
        } else {
            // 用户已关注,删除关注信息
            boolean isSuccess = this.remove(new LambdaQueryWrapper<Follow>()
                    .eq(Follow::getUserId, userId)
                    .eq(Follow::getFollowUserId, followUserId));
            if (isSuccess) {
                stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
            }
        }
        return Result.ok();
    }

    /**
     * 是否关注用户
     *
     * @param followUserId 关注用户的id
     * @return
     */
    @Override
    public Result isFollow(Long followUserId) {
        Long userId = ThreadLocalUtls.getUser().getId();
        int count = this.count(new LambdaQueryWrapper<Follow>()
                .eq(Follow::getUserId, userId)
                .eq(Follow::getFollowUserId, followUserId));
        return Result.ok(count > 0);
    }

    /**
     * 查询共同关注
     *
     * @param id
     * @return
     */
    @Override
    public Result followCommons(Long id) {
        Long userId = ThreadLocalUtls.getUser().getId();
        String key1 = FOLLOW_KEY + userId;
        String key2 = FOLLOW_KEY + id;
        // 查询当前用户与目标用户的共同关注对象
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
        if (Objects.isNull(intersect) || intersect.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
        // 查询共同关注的用户信息
        List<UserDTO> userDTOList = userService.listByIds(ids).stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOList);
    }
}
  1. 获取当前用户与目标用户的关注列表

    • 通过 Redis 键获取当前用户和目标用户的关注列表。
  2. 获取共同关注的用户

    • 使用 Redis 的 intersect 操作获取两个用户关注列表的交集,找出共同关注的用户。
  3. 检查是否存在共同关注

    • 如果交集为空,说明没有共同关注的用户,直接返回空列表。
  4. 将共同关注的用户 ID 转换为 Long 类型

    • 将 Redis 返回的共同关注用户 ID 从字符串类型转换为 Long 类型。
  5. 查询共同关注用户的详细信息

    • 根据共同关注的用户 ID,查询这些用户的详细信息。
  6. 返回共同关注的用户信息

    • 将查询到的共同关注用户信息作为结果返回。

feed流实现方案分析

Feed流产品有两种常见模式

  • 时间排序(Timeline):不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

优点:信息全面,不会有缺失。并且实现也相对简单

缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

  • 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷

缺点:如果算法不精准,可能起到反作用

本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:

1. 推模式(Push Model)

优点:

  • 实时性强:信息可以在事件发生的瞬间推送给接收方,确保用户得到及时的通知或更新。

  • 减少等待时间:接收方无需主动去获取信息,能立即接收消息,特别适合紧急或重要的通知(如紧急警报、软件更新等)。

  • 节省接收方精力:接收方无需主动搜索或请求信息,节省了时间和精力,提升了用户体验。

缺点:

  • 信息过载:过于频繁的推送可能导致信息过载,接收方可能会感到困扰或产生厌烦,导致退订或关闭推送通知。

  • 缺乏个性化:推送信息通常是统一的,可能与接收方的兴趣或需求不匹配,容易产生噪音。

  • 资源消耗:持续推送信息需要不断的网络带宽和服务器资源,尤其在用户数较多时,可能导致系统负担加重。

2. 拉模式(Pull Model)

优点:

  • 按需获取信息:接收方可以在自己需要时主动拉取信息,避免了信息过载的问题。

  • 更个性化:接收方可以根据个人需求进行搜索,获取相关的、有用的信息,避免接收不相关的内容。

  • 减少资源浪费:服务器不会主动发送信息,只有在接收方请求时才会提供数据,减少了系统资源的浪费。

缺点:

  • 响应延迟:拉模式需要接收方主动请求,可能会导致信息获取的延迟,尤其是在接收方没有及时发出请求时。

  • 用户主动性要求高:接收方需要主动去获取信息,可能会错过重要的通知或更新,尤其在信息变化频繁的情况下。

  • 不适合紧急通知:对于一些需要即时通知或处理的紧急事件,拉模式可能会显得不够及时和有效。

3. 推拉结合模式(Push-Pull Hybrid Model)

优点:

  • 灵活性强:结合了推模式和拉模式的优点,能够根据具体情况灵活调整,提供最合适的信息传递方式。

  • 提高用户体验:通过推送确保重要信息不遗漏,通过拉取允许用户根据需求主动获取信息,满足不同场景和用户的需求。

  • 个性化和及时性兼具:重要的通知可以通过推送方式及时传达,而用户可以根据兴趣或需求自主拉取信息,提升整体效率和满意度。

推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息
 

推送到粉丝收件箱

当前项目用户量比较小,所以这里我们选择使用推模式,延迟低、内存占比也没那么大

需要注意的是,Feed 流中的数据会不断更新,所以数据的下标也在变化,因此不能采用传统的分页模式。·这里不能使用list,只能用sortedset。我们在此处采用滚动分页模式

Redis 中的 Sorted Set(有序集合) 是一种特殊的数据结构,它结合了 集合(Set)列表(List) 的特点。与普通的集合不同,Sorted Set 中的每个成员都关联着一个 分数(score),Redis 会根据这些分数对成员进行排序。

Sorted Set 的特点:

  1. 成员唯一:与普通集合一样,Sorted Set 中的每个成员都是唯一的,不能重复。
  2. 按分数排序:Sorted Set 中的成员会根据其关联的分数自动进行排序,从小到大。
  3. 高效的查询和范围操作:可以通过分数来进行高效的范围查询,如获取排名前几的成员,或者按照分数范围获取成员等。
  4. 支持多种排序方式:除了按分数排序外,还可以通过排名(索引)来排序。

 

保存用户发布的笔记;将该笔记推送给所有关注该用户的粉丝,使他们能够看到这篇笔记。

    /**
     * 保存探店笔记
     *
     * @param blog
     * @return
     */
    @Override
    public Result saveBlog(Blog blog) {
        Long userId = ThreadLocalUtls.getUser().getId();
        blog.setUserId(userId);
        // 保存探店笔记
        boolean isSuccess = this.save(blog);
        if (!isSuccess){
            return Result.fail("笔记保存失败");
        }
        // 查询笔记作者的所有粉丝
        List<Follow> follows = followService.list(new LambdaQueryWrapper<Follow>()
                .eq(Follow::getFollowUserId, userId));
        // 将笔记推送给所有的粉丝
        for (Follow follow : follows) {
            // 获取粉丝的id
            Long id = follow.getUserId();
            // 推送笔记
            String key = FEED_KEY + id;
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
        }
        return Result.ok(blog.getId());
    }
  • 查询并返回当前用户的收件箱中,粉丝关注的笔记列表;
  • 使用分页和时间戳排序,确保笔记的显示顺序;
  • 查询并返回每篇笔记的相关信息,如用户信息和点赞情况。
    /**
     * 关注推送页面的笔记分页
     *
     * @param max
     * @param offset
     * @return
     */
    @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 1、查询收件箱
        Long userId = ThreadLocalUtls.getUser().getId();
        String key = FEED_KEY + userId;
        // ZREVRANGEBYSCORE key Max Min LIMIT offset count
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        // 2、判断收件箱中是否有数据
        if (typedTuples == null || typedTuples.isEmpty()) {
            return Result.ok();
        }

        // 3、收件箱中有数据,则解析数据: blogId、minTime(时间戳)、offset
        List<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime = 0; // 记录当前最小值
        int os = 1; // 偏移量offset,用来计数
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2
            // 获取id
            ids.add(Long.valueOf(tuple.getValue()));
            // 获取分数(时间戳)
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                // 当前时间等于最小时间,偏移量+1
                os++;
            } else {
                // 当前时间不等于最小时间,重置
                minTime = time;
                os = 1;
            }
        }

        // 4、根据id查询blog(使用in查询的数据是默认按照id升序排序的,这里需要使用我们自己指定的顺序排序)
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = this.list(new LambdaQueryWrapper<Blog>().in(Blog::getId, ids)
                .last("ORDER BY FIELD(id," + idStr + ")"));
        // 设置blog相关的用户数据,是否被点赞等属性值
        for (Blog blog : blogs) {
            // 查询blog有关的用户
            queryUserByBlog(blog);
            // 查询blog是否被点赞
            isBlogLiked(blog);
        }

        // 5、封装并返回
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setOffset(os);
        scrollResult.setMinTime(minTime);

        return Result.ok(scrollResult);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值