好友关注:
关注和取关
在探店图文的详情页面中,可以关注发布笔记的作者:
需求:基于该数据结构,实现两个接口
-
关注和取关接口
@Override public Result follow(Long id, Boolean isFollow) { // 1.获取登录用户 Long userId = UserHolder.getUser().getId(); // 2.判断到底是关注还是取关 if (isFollow){ // 2.1.关注,新增数据 Follow follow =new Follow(); //当前用户 follow.setUserId(userId); //被关注的用户 follow.setFollowUserId(id); save(follow); return Result.ok(); } // 2.2.取关,删除 delete from tb_follow where userId = ? and follow_user_id =? LambdaQueryWrapper<Follow> queryWrapper=new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getUserId,userId).eq(Follow::getFollowUserId,id); this.remove(queryWrapper); return Result.ok(); }
-
判断是否关注的接口
@Override public Result isFollow(Long id) { // 1.获取登录用户 Long userId = UserHolder.getUser().getId(); //查询是否关注 select count(*) from tb_follow where userId = ? and follow_user_id =? LambdaQueryWrapper<Follow> queryWrapper=new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getUserId,userId).eq(Follow::getFollowUserId,id); int count = count(queryWrapper); //判断count return Result.ok(count>0); }
关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:
分页查询用户博客信息
//根据用户查询
Page<Blog> page=new Page<>(current,MAX_PAGE_SIZE);
LambdaQueryWrapper<Blog> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(Blog::getUserId,id) ;
page(page,queryWrapper);
//获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
查询用户信息
User user = getById(id);
if (user == null){
return Result.ok();
}
UserDTO userDTO =BeanUtil.copyProperties(user,UserDTO.class);
//返回
return Result.ok(userDTO);
共同关注:
需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
思路:
- 将用户的关注保存到redis中用set集合
- 在前端发送请求时将当前用户和目标用户set集合求交集并返回
改造关注与取关接口:
在每次添加关注时把数据不仅放到数据库中,还应该放到redis中。
public Result follow(Long id, Boolean isFollow) {
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断到底是关注还是取关
if (isFollow){
// 2.1.关注,新增数据
Follow follow =new Follow();
//当前用户
follow.setUserId(userId);
//被关注的用户
follow.setFollowUserId(id);
boolean isSuccess = save(follow);
if (isSuccess){
//把关注用户的id,放入redis的set集合 sadd userId followerUserId
stringRedisTemplate.opsForSet().add(FOLLOWS+userId,id.toString());
}
return Result.ok();
}
// 2.2.取关,删除 delete from tb_follow where userId = ? and follow_user_id =?
LambdaQueryWrapper<Follow> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(Follow::getUserId,userId).eq(Follow::getFollowUserId,id);
boolean isSuccess = this.remove(queryWrapper);
//把关注用户的id从Redis集合中移除
if (isSuccess){
stringRedisTemplate.opsForSet().remove(FOLLOWS+userId,id.toString());
}
return Result.ok();
}
实现共同关注接口:
@Override
public Result followCommons(Long id) {
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
//2.求交集
Set<String> intersect = stringRedisTemplate.opsForSet()
.intersect(FOLLOWS + userId, FOLLOWS + id);
//3.解析id集合
if (intersect==null || intersect.isEmpty()) {
return Result.ok(Collections.emptyList());
}
List<Long> userIdList = intersect.stream().
map(Long::valueOf).collect(Collectors.toList());
List<User> users = userService.listByIds(userIdList);
List<UserDTO> dtoList = users.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(dtoList);
}
关注推送:
关注推送也叫Feed流,直译为投喂。为用户持续的提供”沉浸式”的体验,通过无限下拉刷新获取新的信息。
-
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 优点:信息全面,不会有缺失。并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
-
**智能排序:**利用智能算法屏蔽掉违规的,用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
- 缺点:如果算法不清楚,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
-
拉模式
-
推模式
-
推拉结合
总结:
基于推模式实现关注推送功能:
需求:
- 修改新增探店笔记业务,在保存blog到数据库的同时,推送到分数的收件箱
- 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
- 查询收件箱数据时,可以实现分页查询
在redis中用scoreSet还是list?
Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。
采用滚动分页模式:该方式查询不依赖角标所以list不支持,因为list只能通过角标查询。而scoreSet是按排名查询。
推送到粉丝邮件箱–存到redis中
public Result saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean isSuccess = save(blog);
if (isSuccess){
return Result.fail("新增笔记失败!");
}
// 查询笔记作者的所有粉丝
LambdaQueryWrapper<Follow> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(Follow::getFollowUserId,blog.getUserId());
List<Follow> follows = followService.list(queryWrapper);
// 推送笔记id给所有粉丝
for (Follow follow : follows) {
//获取粉丝id
Long userId = follow.getUserId();
//推送到redis
stringRedisTemplate.opsForZSet()
.add(FEED_KEY+userId,blog.getId().toString(),System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
滚动分页查询收件箱的思路:
需求:在个人主页的“关注”卡片中,查询并展示推送的Blog的信息
ZREVRANGEBYSCORE z1 max min WITHSCORES LIMIT offset count
分页查询参数
- max:上一次查询的最小时间戳 | 当前时间
- min:0
- offset:与上一次查询最小时间戳一致的所有元素个数 | 0
- count:分页大小 3
实现关注推送页面的滚动分页查询
定义数据模型
//滚动分页结果
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
业务代码实现:
@Override
public Result getBlogOfFollow(Long max, Integer offset) {
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
//2.查询收件箱
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(FEED_KEY + userId, 0, max, offset, 2);
//3.非空判断
if (typedTuples ==null || typedTuples.isEmpty()) {
return Result.ok(Collections.emptyList());
}
//4.解析数据: blogId、score(时间戳)、offset
List<Long> ids =new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
//4.1.获取id
ids.add(Long.valueOf(typedTuple.getValue()));
//4.2.获取分数(时间戳)
long time = typedTuple.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) {
//2.查询blog有关的用户
queryBlogUser(blog);
//3.查询blog是否被点赞
isBlogLiked(blog);
}
//6.封装并返回
ScrollResult result =new ScrollResult();
result.setList(blogs);
result.setOffset(os);
result.setMinTime(minTime);
return Result.ok(result);
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
private void isBlogLiked(Blog blog) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登录用户是否已经点赞
// Boolean isMember = stringRedisTemplate.opsForSet().isMember(BLOG_LIKED_KEY + blog.getId(), userId.toString());
Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY + blog.getId(), userId.toString());
blog.setIsLike(score != null);
}