IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> 黑马点评项目-好友关注 -> 正文阅读

[开发测试]黑马点评项目-好友关注

一、关注和取关

在探店图文的详情页中,可以关注发布笔记的作者:
在这里插入图片描述
需求:基于该表数据结构,实现两个接口:
① 关注和取关接口
② 判断是否关注的接口

关注是 User 之间的关系,是博主与粉丝的关系,数据库中有一张 tb_follow 表来标识:
在这里插入图片描述
注意:这里需要把主键修改为自增长
在这里插入图片描述
FollowController

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

    @Autowired
    private IFollowService followService;

    @PutMapping("/{id}/{isFollow}")
    public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
        return followService.follow(followUserId, isFollow);
    }


    @PutMapping("/or/not/{id}")
    public Result isFollow(@PathVariable("id") Long followUserId){
        return followService.isFollow(followUserId);
    }

}

IFollowService

public interface IFollowService extends IService<Follow> {

    Result follow(Long followUserId, Boolean isFollow);

    Result isFollow(Long followUserId);
}

FollowServiceImpl

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

    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        // 获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        // 判断是否关注
        if(isFollow){
            // 关注,新增
            Follow follow = new Follow();
            follow.setFollowUserId(followUserId);
            follow.setUserId(userId);
            save(follow);
        } else {
            // 取消关注,删除
            remove(new QueryWrapper<Follow>().eq("follow_user_id", followUserId).eq("user_id", userId));
        }
        return Result.ok();
    }

    @Override
    public Result isFollow(Long followUserId) {
        // 获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        Integer count = query().eq("follow_user_id", followUserId).eq("user_id", userId).count();
        return Result.ok(count > 0);
    }
}

二、共同关注

点击博主头像,可以进入博主首页:
在这里插入图片描述
博主个人首页依赖两个接口:
① UserController 根据 id 查询 User 信息

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

② BlogController 根据 id 查询博主的探店笔记

@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));
    // 获取当前页数据
    List<Blog> records = page.getRecords();
    return Result.ok(records);
}

需求:利用 Redis 中的 set 类型的数据结构,实现共同关注功能(set 数据结构可以用来计算指定 key 之间元素的交集)。在博主个人页面展示出当前用户与博主的共同好友。
在这里插入图片描述
FollowController

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

    @Autowired
    private IFollowService followService;

    @PutMapping("/{id}/{isFollow}")
    public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
        return followService.follow(followUserId, isFollow);
    }


    @PutMapping("/or/not/{id}")
    public Result isFollow(@PathVariable("id") Long followUserId){
        return followService.isFollow(followUserId);
    }

    @GetMapping("/common/{id}")
    public Result followCommons(@PathVariable("id") Long id){
        return followService.followCommons(id);
    }

}

IFollowService

public interface IFollowService extends IService<Follow> {

    Result follow(Long followUserId, Boolean isFollow);

    Result isFollow(Long followUserId);

    Result followCommons(Long id);
}

FollowServiceImpl:对 follow 方法进行改进,在向数据库写入关注信息时,同时保存至 Redis 中,在取消关注时,除了删除数据库中的数据,同时移除 Redis 中的相关数据。followCommons 为查询共同关注的方法,使用 set 数据结构的特性进行查询。

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

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IUserService userService;

    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        // 获取当前登录用户
        Long userId = UserHolder.getUser().getId();

        String key = "follows:" + userId;
        // 判断是否关注
        if(isFollow){
            // 关注,新增
            Follow follow = new Follow();
            follow.setFollowUserId(followUserId);
            follow.setUserId(userId);
            boolean isSuccess = save(follow);
            if (isSuccess) {
                stringRedisTemplate.opsForSet().add(key, followUserId.toString());
            }
        } else {
            // 取消关注,删除
            boolean isSuccess = remove(new QueryWrapper<Follow>().eq("follow_user_id", followUserId).eq("user_id", userId));
            if (isSuccess) {
                stringRedisTemplate.opsForSet().remove(key, followUserId);
            }
        }
        return Result.ok();
    }

    @Override
    public Result isFollow(Long followUserId) {
        // 获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        Integer count = query().eq("follow_user_id", followUserId).eq("user_id", userId).count();
        return Result.ok(count > 0);
    }

    @Override
    public Result followCommons(Long id) {
        // 获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        String key = "follows:" + userId;
        String key2 = "follows:" + id;

        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
        if(intersect == null){
            return Result.ok(Collections.emptyList());
        }

        List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
        List<UserDTO> collect = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());

        return Result.ok(collect);
    }
}

三、关注推送

3.1 Feed 流实现方案分析

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

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

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

拉模式: 也叫做读扩散
假设有三个人,分别是张三、李四、王五,这三个人分别会在自己的账号发布自己的笔记或者视频,在这里我们统一称之为消息,这三个人发布的所有的消息都会被发送到发件箱中,发送到发件箱中的消息除了消息本身之外,还需要带有时间戳。粉丝赵六会有一个收件箱,平时收件箱是空的,只有他在读取消息时,才会把赵六关注的所有人的发件箱中的消息拉取到他的收件箱中,拉取到收件箱后,消息会按照携带的时间戳进行排序,然后赵六就可以读取消息了。
在这里插入图片描述
优点:节省内存空间。收件箱中的消息在读取完后就会被清空,下次需要读取的时候会重新从所有关注人的发件箱中拉取。消息只保存了一份。
缺点:每次读取消息都要重新拉取发件箱中的消息,然后再做排序,比较耗时。

推模式: 也叫做写扩散
假设现在有两个 up 主:张三、李四,有三个粉丝:粉丝1、粉丝2、粉丝3,粉丝1关注了张三,粉丝2和3都关注了张三和李四,如果张三此时想要发送消息,那么这条消息会直接推送到张三的所有粉丝的收件箱中,而李四发送的消息也会被推送到粉丝2和3的收件箱中,收件箱收到消息后会对所有的消息进行排序,粉丝在读取消息时,就已经是排序好的消息了。这样的一个好处就是延时低,不必每次读取时都需要重新拉取消息。但这种方式内存占用会比较高,up 主每次发消息都要同步所有的粉丝,如果粉丝数过多,超过几百万,就需要复制几百万份。
在这里插入图片描述

推拉结合模式: 也叫做读写混合,兼具推和拉两种模式的优点。
普通粉丝人数众多,但是活跃度较低,读取消息的频率也就低,可采用拉模式;
而活跃粉丝人数少,但是活跃度高,读取消息的频率高,可采用推模式。
大 V 发送消息时,会直接将消息推送到活跃粉丝的发件箱中,而针对于普通粉丝,消息会先发送到发件箱中,当普通粉丝读取消息时,会直接从发件箱中拉取。
在这里插入图片描述
三种模式对比:
在这里插入图片描述

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

3.2.1 需求分析

需求:
① 修改新增探店笔记的业务,在保存 Blog 到数据库的同时,推送到粉丝的收件箱
② 收件箱满足可以根据时间戳排序,必须用 Redis 的数据结构实现
③ 查询收件箱数据时,可以实现分页查询

需求分析:
Redis 中 List、SortedSet 两种数据结构均可以实现排序功能。List 要想实现按时间排序,可以按照插入顺序排。SortedSet 排序则是按照 score 进行排序,score 值中可以存储时间戳来实现按照时间排序。
Redis中的 List 可以按照角标进行查询,完全可以实现分页查询。而 SortedSet 虽然没有角标,但是排序完成后,会有一个排名机制,可以使用排名机制进行查询,也能实现分页查询。
那究竟应该选择哪种数据结构来分页查询功能呢?
由于Feed 流中的数据会不断更新,这就导致数据的角标也在不断变化,因此不能采用传统的分页模式。
来看下传统分页模式:
在这里插入图片描述
假设 t1 时刻,Feed 流中有 10 条消息,此时从 Feed 流中读取前 5 条消息,在 t2时刻向 Feed 流中插入了一条新消息,当在 t3 时刻再去读取后 5 条数据时,就会出现数据重复读取的问题。为了避免这种情况,可以采用滚动分页。所谓滚动分页,其实就是记录每一次查询的最后一条消息的下标,下一次查询时从该条消息开始查询。
在这里插入图片描述
假设 t1 时刻,Feed 流中有 10 条消息,此时从 Feed 流中读取前 5 条消息,在 t2时刻向 Feed 流中插入了一条新消息,当在 t3 时刻再去读取后 5 条数据时,查询会从记录的 lastId 开始,向后查询 5 条,这样也就不会出现查询重复的问题。
SortedSet 除了可以按照时间戳排序,还支持按照 score 值的范围进行查询,即按照时间戳范围进行查询。每次查询时,记录最小的时间戳,下次查询时,从比该时间戳还小的下一个时间戳开始查询。
为了保证发布的消息不会重复进入需要推送的粉丝的收件箱中,以及保证查询速度,使用 SortedSet 更优。

3.2.2 推送到粉丝收件箱

修改 BlogController 中的 saveBlog 方法

@RestController
@RequestMapping("/blog")
public class BlogController {

    @Resource
    private IBlogService blogService;

    @PostMapping
    public Result saveBlog(@RequestBody Blog blog) {
        return blogService.saveBlog(blog);
    }
 }

IBlogService

public interface IBlogService extends IService<Blog> {
    Result saveBlog(Blog blog);
}

BlogServiceImpl

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
	@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IFollowService followService;

    @Override
    public Result saveBlog(Blog blog) {
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        // 保存探店博文
        boolean isSuccess = save(blog);
        if(!isSuccess){
            return Result.ok();
        }
        // 查询笔记作者的所有粉丝  select * from tb_follow where follow_user_id=?
        List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();

        if(follows == null || follows.isEmpty()){
            return Result.ok(blog.getId());
        }

        // 推送笔记给所有粉丝
        for (Follow follow: follows) {
            Long userId = follow.getUserId();
            stringRedisTemplate.opsForZSet().add(RedisConstants.FEED_KEY + userId, blog.getId().toString(), System.currentTimeMillis());
        }

        // 返回id
        return Result.ok(blog.getId());
    }
}

3.3 实现关注推送页面的分页查询

3.3.1 需求分析

需求:在个人主页的”关注“卡片中,查询并展示推送的 Blog 信息:
在这里插入图片描述
实现滚动分页需要使用下面的命令:

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

返回有序集 key 中, score 值介于 max 和 min 之间(默认包括等于 max 或 min )的所有的成员。有序集成员按 score 值递减(从大到小)的次序排列。WITHSCORES 表示是否返回成员分数。LIMIT 表示是否分页,如果带有 LIMIT,则后面必须带有offset、count,offset表示偏移量(相对于max值而言),count 表示结果数量。关于 ZREVRANGEBYSCORE 更多的介绍可以参看这位大佬的文章Redis有序集合命令ZREVRANGEBYSCORE详解与应用
这里要注意是:一旦使用分页查询后,max 以及 offset 的值就是一个动态变化的值了。其中,max 值是上一次查询结果中的最小分数(即时间戳)。而 offset 的取值也与上一次查询结果中的最小分数有关,如果上一次查询结果中的最小分数值重复多次出现,offset的值就应该为最小分数重复出现的次数。
如下图所示:
其中 m7 和 m6 的 score 值相同
在这里插入图片描述
在这里插入图片描述
根据上图可以看出 offset 是需要动态获取。

定义滚动查询结果类 ScrollResult

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

BlogController

@RestController
@RequestMapping("/blog")
public class BlogController {

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

IBlogService

public interface IBlogService extends IService<Blog> {
    Result queryBlogOfFollow(Long max, Integer offset);
}
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
 @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 获取当前用户
        UserDTO user = UserHolder.getUser();
        // 查询收件箱  参数1:key 参数2:最小分数 参数3:最大分数 参数4:偏移量 参数5:每次取几条
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().
                reverseRangeByScoreWithScores(RedisConstants.FEED_KEY + user.getId(), 0, max, offset, 2);

        if(typedTuples == null) {
            return Result.ok(Collections.emptyList());
        }

        int count = 1;
        long minTime = 0;
        List<Long> ids = new ArrayList<>(typedTuples.size());
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            ids.add(Long.valueOf(typedTuple.getValue()));
            // 获取分数(时间戳)
            long time = typedTuple.getScore().longValue();
            if(time == minTime){
                count++;
            } else {
                minTime = time;
                count = 1;
            }
        }

        String join = StrUtil.join(",", ids);
        // 根据 id 查询 blog
        List<Blog> blogs = query().in("id", ids).last("order by field(id," + join + ")").list();
		// 封装结果
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setMinTime(minTime);
        scrollResult.setOffset(count);
        return Result.ok(scrollResult);
    }

}
  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2022-05-12 16:40:57  更:2022-05-12 16:41:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 14:51:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码