2023.12.12
本章实现好友关注模块,包含以下功能的实现:关注与取关、共同关注、关注推送。
关注与取关
当点击某个用户的主页时,会调用如下接口:
该接口是用来判断是否已经关注该用户,最后一个参数是该用户的id。
在我们点击关注之后,又会调用一个接口:
该接口就是用来实现关注或者取关的操作的,倒数第二个参数是该用户的id,最后一个参数是当前是否已经关注该用户,根据这个bool值来判断是执行关注操作还是取关操作。
关注与取关操作 和 判断是否关注 的实现类方法定义为:follow和isFollow,代码如下:
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登陆用户
Long userId = UserHolder.getUser().getId();
//2.判断是关注还是取关
if(isFollow){
//2.1 关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
save(follow);
}else {
//2.2取关 直接删除表中的数据即可
remove(new QueryWrapper<Follow>()
.eq("user_id",userId).eq("follow_user_id",followUserId));
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
//1.获取登陆用户
Long userId = UserHolder.getUser().getId();
//2.查询数据库看是否关注
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count > 0);
}
}
共同关注
接下来实现共同关注功能。当点进一个用户的主页时,会显示该用户的基本信息(头像、id),还会显示该用户发布的博客。
右边还有一个共同关注的列表,显示的内容是我与这个用户共同关注的用户,点击共同关注,接口信息如下:
1022为当前操作用户的id,这里需要实现当前操作用户与点击的这个用户的共同关注列表,思路就是将两个用户的关注列表取一个交集即可,那么适合此操作的数据结构为redis中的set集合,set集合有取交集的操作。
这里得先修改我们之前关注和取关操作的逻辑,在关注博主的同时,需要将数据放到set集合中,方便后期我们实现共同关注,当取消关注时,也需要将数据从set集合中删除。
原代码改为:
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登陆用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
//2.判断是关注还是取关
if(isFollow){
//2.1 关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if(isSuccess){
//把关注用户的id,存入redis的set集合中
stringRedisTemplate.opsForSet().add(key,followUserId.toString());
}
}else {
//2.2取关 直接删除表中的数据即可
boolean isSuccess = remove(new QueryWrapper<Follow>()
.eq("user_id", userId).eq("follow_user_id", followUserId));
if(isSuccess){
// 把关注用户的id 从redis集合中移除
stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
return Result.ok();
}
即在修改数据库之后,对redis中的set集合数据进行保存或者移除。
接下来,实现共同关注的功能,大致思路为:从redis中取出两个用户关注列表,再使用redis中的intersect操作取出两个关注列表的集合,再返回给前端页面。代码如下:
@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());
}
//3.解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//4.查询用户
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
测试结果如下:
关注推送
当我们关注了用户之后,这个用户发布了动态,那我们应该把这些动态推送给粉丝,即关注推送,关注推送也叫作Feed流,直译为投喂,为用户提供沉浸式体验,通过无限下拉刷新获取新的信息。
- Feed流的实现有两种模式
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注(B站关注的up,朋友圈等)
- 优点:信息全面,不会有缺失,并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
- 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户
- 优点:投喂用户感兴趣的信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能会起到反作用(给你推的你都不爱看)
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注(B站关注的up,朋友圈等)
这里我们采用的是Timeline方式,有三种方案:
- 拉模式:也叫读扩散,博主将发布的博客保存至发件箱,粉丝需要将关注的内容拉到自己的收件箱中。
- 推模式:也叫写扩散,博主会将发布的博客发布至所有粉丝的收件箱中。
- 推拉结合:上述两种方式的结合版。
这里考虑到粉丝量不会有很多,采取推模式。需要修改新增探店博客的业务,在保存blog到数据库的同时,推送到粉丝的收件箱,收件箱需要满足可以根据时间戳排序,必须用Redis的数据结构实现,这里使用ZSet集合来充当收件箱。
这里为什么使用redis中的Zset集合呢?Zset支持滚动分页,传统的分页模式在这里会有问题,因为feed流中的数据是不断变化的,数据的角标也在变化,这里需要使用滚动分页。
改造新增探店博客的代码:
@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());
}
点击关注栏,会发送请求:
实现该接口的实现类:
@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);
}
最终效果如图: