关注和取关

在探店图文的详情页面中,可以关注发布笔记的作者:
image.png
需求:基于该表数据结构,实现两个接口:
① 关注和取关接口
② 判断是否关注的接口
关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:
image.png

  1. @Service
  2. public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
  3. @Override
  4. public Result follow(Long followUserId, Boolean isFollow) {
  5. //0.获取登录用户
  6. Long userId = UserHolder.getUser().getId();
  7. //1.判断是关注还是取关
  8. if (isFollow) {
  9. //2.关注,新增数据
  10. Follow follow = new Follow();
  11. follow.setUserId(userId);
  12. follow.setFollowUserId(followUserId);
  13. save(follow);
  14. }else{
  15. //3.取关,删除数据
  16. remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id",followUserId));
  17. }
  18. return Result.ok();
  19. }
  20. @Override
  21. public Result isFollow(Long followUserId) {
  22. //0.获取登录用户
  23. Long userId = UserHolder.getUser().getId();
  24. //1.查询是否关注
  25. Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
  26. return Result.ok(count > 0);
  27. }
  28. }

共同关注

image.png
博主个人首页依赖两个接口:

@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){
    User user = userService.getById(userId); 
    if (user == null) {
        return Result.ok();
    }
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    return Result.ok(userDTO);
}
@GetMapping("/of/user")
public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current,
                                @RequestParam("id") Long id) { 
    Page<Blog> page = blogService.query()
        .eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
    return Result.ok(page.getRecords());
}

实现共同关注功能

需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
image.png

@Override
public Result followCommons(Long id) {
    //1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    //2.求交集
    String key2 = "follows:" + id;
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
    if(intersect == null || intersect.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    List<UserDTO> users = userService.listByIds(ids)
        .stream()
        .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
        .collect(Collectors.toList());
    return Result.ok(users);
}

关注推送

关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
image.png

Feed流的模式

Feed流产品有两种常见模式:

  • Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
    • 优点:信息全面,不会有缺失。并且实现也相对简单
    • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
  • 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
    • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
    • 缺点:如果算法不精准,可能起到反作用

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

  1. 拉模式
  2. 推模式
  3. 推拉结合

Feed流的实现方案1

拉模式:也叫做读扩散。用户主动拉取关注者的发送的信息
image.png

Feed流的实现方案2

推模式:也叫做写扩散。用户被动接收关注者发送的消息
image.png

Feed流的实现方案3

推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。
image.png
image.png

基于推模式实现关注推送功能

需求
①修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱

@Override
public Result saveBlog(Blog blog) {
    //1.获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    //2.保存探店博文
    boolean isSuccess = save(blog);
    if (!isSuccess) {
        return Result.fail("新增笔记失败!");
    }
    //3.查询笔记作者的所有粉丝
    List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
    //4.推送笔记id给所有粉丝
    for (Follow follow : follows) {
        //4.1获取粉丝id
        Long userId = follow.getUserId();
        //4.2推送箱
        String key = "feed:" + userId;
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
    }
    //5.返回id
    return Result.ok(blog.getId());
}

②收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
③查询收件箱数据时,可以实现分页查询

Feed流分页问题

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。
image.png

Feed流的滚动分页

可以采用滚动分页,记录上一次最大的ID
image.png

滚动分页查询收件箱思路

需求:在个人主页的“关注”卡片中,查询并展示推送的Blog信息:
image.png

分页结果实体类:

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

BlogServiceImpl:

@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    // 1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count
    String key = FEED_KEY + userId;
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
        .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
    // 3.非空判断
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }
    // 4.解析数据:blogId、minTime(时间戳)、offset
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0; // 2
    int os = 1; // 2
    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2
        // 4.1.获取id
        ids.add(Long.valueOf(tuple.getValue()));
        // 4.2.获取分数(时间戳)
        long time = tuple.getScore().longValue();
        if(time == minTime){
            os++;
        }else{
            minTime = time;
            os = 1;
        }
    }

    // 5.根据id查询blog
    String idStr = StrUtil.join(",", ids);
    List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();

    for (Blog blog : blogs) {
        // 5.1.查询blog有关的用户
        queryBlogUser(blog);
        // 5.2.查询blog是否被点赞
        isBlogLiked(blog);
    }

    // 6.封装并返回
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);

    return Result.ok(r);
}

实现成功测试:image.png