关注用户
当点击相关博客时,会发送多个请求:
http://127.0.0.1:8080/api/blog/5
http://127.0.0.1:8080/api/shop/1
http://127.0.0.1:8080/api/blog/likes/5
http://127.0.0.1:8080/api/user/me
http://127.0.0.1:8080/api/follow/or/not/2
这些请求会获取博客信息,店铺信息,点赞信息,用户信息,用户是否关注博主来进行页面展示。
判断是否关注
对于请求 http://127.0.0.1:8080/api/follow/or/not/2
,在 FollowerController 添加 isFollow
判断是否关注功能函数
@GetMapping("/or/not/{id}")public Result isFollow(@PathVariable("id") Long followUserId) {return followService.isFollow(followUserId);}
在 FollowerService 的具体实现 FollowerServiceImpl 中完成 isFollow
具体函数:
@Overridepublic Result isFollow(Long followUserId) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();// 3.判断return Result.ok(count > 0);}
如果没有关注,就会返回:{“success”:true,“data”:false}
关注用户和共同关注
当未关注用户时,点击关注按钮,发送请求:http://127.0.0.1:8080/api/follow/2/true
当已经关注用户时,点击取消关注按钮,发送请求:http://127.0.0.1:8080/api/follow/2/false
在 FollowerController 添加 follow
关注或取消关注功能函数
@PutMapping("/{id}/{isFollow}")public Result follow(@PathVariable("id") Long id,@PathVariable("isFollow") Boolean isFollow){return followService.follow(id,isFollow);}
在 FollowerService 的具体实现 FollowerServiceImpl 中完成 follow
具体函数,为了完成共同关注的功能,需要在 redis 中使用 set 来存储当前登录用户关注的用户id
public Result follow(Long followUserId, Boolean isFollow) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();String key = "follows:" + userId;// 1.判断到底是关注还是取关if (isFollow) {// 2.关注,新增数据Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);boolean isSuccess = save(follow);if (isSuccess) {// 把关注用户的id,放入redis的set集合 sadd userId followerUserIdstringRedisTemplate.opsForSet().add(key, followUserId.toString());}} else {// 3.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?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();}
共同关注
点击博主头像,会跳转到博主个人主页,点击共同关注,发送请求:http://127.0.0.1:8080/api/follow/common/2
在 FollowerController 添加 followCommons
查询共同关注功能函数
@GetMapping("/common/{id}")public Result followCommons(@PathVariable("id") Long id){return followService.followCommons(id);}}
在 FollowerService 的具体实现 FollowerServiceImpl 中完成 followCommons
具体函数
@Overridepublic 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);}
}
信息推荐
信息推荐就是系统分析用户到底想要什么,然后直接把内容推送给用户
信息推荐两种常见模式:
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
Timeline的模式实现方案有三种:
- 拉模式
- 推模式
- 推拉结合
拉模式
当用户要读取信息系统才会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序
优点:比较节约空间,平时不拉取
缺点:比较延迟,同一时间拉取服务器压力大
推模式
推模式是没有写邮箱的,写完直接推过去
优点:时效快,不用临时拉取
缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多份数据到粉丝那边去
推拉结合
对发收用户都进行分类,普通人直接发,大V需被拉,普通粉去拉,活跃粉等发
推模式实现
Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。
解决方案:记录上次访问到的最后一个位置,可以借助 redis 中 sortedset 结构的 score 属性
推具体实现
- 修改 BlogController 中的
saveBlog
保存博客功能函数,核心的意思:就是我们在保存完探店笔记后,获得到当前笔记的粉丝,然后把数据推送到粉丝的 redis 中去。
@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {// // 获取登录用户// UserDTO user = UserHolder.getUser();// blog.setUserId(user.getId());// // 保存探店博文// blogService.save(blog);// // 返回id// return Result.ok(blog.getId());return blogService.saveBlog(blog);}
- 在 BolgService 的具体实现 BlogServiceImpl 中完成
saveBlog
功能函数:
@Overridepublic Result saveBlog(Blog blog) {// 1. 获取当前用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 2. 保存探店笔记boolean isSuccess = save(blog);if(!isSuccess){return Result.fail("保存笔记失败");}// 3. 查询用户所有粉丝 select * from tb_follower where follower_user_id = ?List<Follow> followerUserId = followService.query().eq("follower_user_id", user.getId()).list();// 4. 推送笔记id给粉丝for (Follow follow : followerUserId){// 4.1 获取粉丝idLong userId = follow.getUserId();// 4.2 推送String key = "feed:";stringRedisTemplate.opsForZSet().add(key+userId,blog.getId().toString(),System.currentTimeMillis());}// 5. 返回idreturn Result.ok(blog.getId());}
- 在个人主页的“关注”卡片中,点击关注
发送请求:http://127.0.0.1:8080/api/blog/of/follow?&lastId=1740743656837
1、每次查询完成后,我们要分析出查询出数据的最小时间戳,这个值会作为下一次查询的条件
2、我们需要找到与上一次查询相同的查询个数作为偏移量,下次查询时,跳过这些查询过的数据,拿到我们需要的数据
综上:我们的请求参数中就需要携带 lastId:上一次查询的最小时间戳 和偏移量这两个参数。
这两个参数第一次会由前端来指定,以后的查询就根据后台结果作为条件,再次传递到后台。
第一,定义出来具体的返回值实体类:
@Data
public class ScrollResult {private List<?> list;private Long minTime;private Integer offset;
}
第二,在 BlogController 中定义 queryBlogOfFollow
查询关注用户的博客
@GetMapping("/of/follow")public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){return blogService.queryBlogOfFollow(max,offset);}
}
第三,在 BlogService 的具体实现 BlogServiceImpl 中完成 queryBlogOfFollow
具体功能
@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {// 1. 获取当前用户Long userId = UserHolder.getUser().getId();// 2. 查询收件箱String key = "feed:" + userId;// 取分数在 0 到 max 之间的跳过前面 offset 个满足条件的 2 个元素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(时间戳)、offsetList<Long> ids = new ArrayList<>(typedTuples.size());long minTime = 0;int os = 1;for (ZSetOperations.TypedTuple<String> tuple : typedTuples){ids.add(Long.valueOf(tuple.getValue()));long time = tuple.getScore().longValue();if (time==minTime){os++;}else {minTime = time;os = 1;}}os = minTime == max ? os:os+offset;// 5.根据id查询blogString 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);Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY + blog.getId(), userId.toString());if (score != null){blog.setIsLike(Boolean.TRUE);}}// 6.封装并返回ScrollResult r = new ScrollResult();r.setList(blogs);r.setOffset(os);r.setMinTime(minTime);return Result.ok(r);}
效果展示
