关注和取关

- 关注功能:如果
isFollow为true,用户会关注目标用户;如果为false,则会取消关注目标用户。对应的关注关系会被保存或删除。 - 是否关注功能:通过查询当前用户是否已经关注目标用户来返回是否关注的结果。
- 主要的数据库操作是通过
Follow实体类实现的,使用了 MyBatis-Plus 提供的工具(如save、remove、count等)来进行 CRUD 操作。
当用户希望关注或取消关注时,后端会通过 FollowService 中的 follow() 方法,依据传入的 isFollow 参数来保存或删除数据库中的关注记录。查询是否已关注功能则通过 isFollow() 方法,结合 MyBatis-Plus 提供的查询功能来判断当前用户与目标用户之间是否存在关注关系。Follow 实体类用于映射数据库中的关注记录,数据库操作通过 MyBatis-Plus 提供的 save、remove 和 count 等方法实现。
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
private IFollowService followService;
/**
* 关注用户
* @param followUserId 关注用户的id
* @param isFollow 是否已关注
* @return
*/
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable Boolean isFollow){
return followService.follow(followUserId, isFollow);
}
/**
* 是否关注用户
* @param followUserId 关注用户的id
* @return
*/
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId){
return followService.isFollow(followUserId);
}
}
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
/**
* 关注用户
*
* @param followUserId 关注用户的id
* @param isFollow 是否已关注
* @return
*/
@Override
public Result follow(Long followUserId, Boolean isFollow) {
Long userId = ThreadLocalUtls.getUser().getId();
if (isFollow) {
// 用户为关注,则关注
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
this.save(follow);
} else {
// 用户已关注,删除关注信息
this.remove(new LambdaQueryWrapper<Follow>()
.eq(Follow::getUserId, userId)
.eq(Follow::getFollowUserId, followUserId));
}
return Result.ok();
}
/**
* 是否关注用户
*
* @param followUserId 关注用户的id
* @return
*/
@Override
public Result isFollow(Long followUserId) {
Long userId = ThreadLocalUtls.getUser().getId();
int count = this.count(new LambdaQueryWrapper<Follow>()
.eq(Follow::getUserId, userId)
.eq(Follow::getFollowUserId, followUserId));
return Result.ok(count > 0);
}
}
共同关注
共同关注的实现依赖于 Redis 中存储的用户关注列表(通过 Set 集合)
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IUserService userService;
/**
* 关注用户
*
* @param followUserId 关注用户的id
* @param isFollow 是否已关注
* @return
*/
@Override
public Result follow(Long followUserId, Boolean isFollow) {
Long userId = ThreadLocalUtls.getUser().getId();
String key = FOLLOW_KEY + userId;
if (isFollow) {
// 用户为关注,则关注
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = this.save(follow);
if (isSuccess) {
// 用户关注信息保存成功,把关注的用户id放入Redis的Set集合中,
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
} else {
// 用户已关注,删除关注信息
boolean isSuccess = this.remove(new LambdaQueryWrapper<Follow>()
.eq(Follow::getUserId, userId)
.eq(Follow::getFollowUserId, followUserId));
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}
/**
* 是否关注用户
*
* @param followUserId 关注用户的id
* @return
*/
@Override
public Result isFollow(Long followUserId) {
Long userId = ThreadLocalUtls.getUser().getId();
int count = this.count(new LambdaQueryWrapper<Follow>()
.eq(Follow::getUserId, userId)
.eq(Follow::getFollowUserId, followUserId));
return Result.ok(count > 0);
}
/**
* 查询共同关注
*
* @param id
* @return
*/
@Override
public Result followCommons(Long id) {
Long userId = ThreadLocalUtls.getUser().getId();
String key1 = FOLLOW_KEY + userId;
String key2 = FOLLOW_KEY + id;
// 查询当前用户与目标用户的共同关注对象
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
if (Objects.isNull(intersect) || intersect.isEmpty()) {
return Result.ok(Collections.emptyList());
}
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
// 查询共同关注的用户信息
List<UserDTO> userDTOList = userService.listByIds(ids).stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOList);
}
}
-
获取当前用户与目标用户的关注列表:
- 通过 Redis 键获取当前用户和目标用户的关注列表。
-
获取共同关注的用户:
- 使用 Redis 的
intersect操作获取两个用户关注列表的交集,找出共同关注的用户。
- 使用 Redis 的
-
检查是否存在共同关注:
- 如果交集为空,说明没有共同关注的用户,直接返回空列表。
-
将共同关注的用户 ID 转换为 Long 类型:
- 将 Redis 返回的共同关注用户 ID 从字符串类型转换为
Long类型。
- 将 Redis 返回的共同关注用户 ID 从字符串类型转换为
-
查询共同关注用户的详细信息:
- 根据共同关注的用户 ID,查询这些用户的详细信息。
-
返回共同关注的用户信息:
- 将查询到的共同关注用户信息作为结果返回。
feed流实现方案分析

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

1. 推模式(Push Model)
优点:
-
实时性强:信息可以在事件发生的瞬间推送给接收方,确保用户得到及时的通知或更新。
-
减少等待时间:接收方无需主动去获取信息,能立即接收消息,特别适合紧急或重要的通知(如紧急警报、软件更新等)。
-
节省接收方精力:接收方无需主动搜索或请求信息,节省了时间和精力,提升了用户体验。
缺点:
-
信息过载:过于频繁的推送可能导致信息过载,接收方可能会感到困扰或产生厌烦,导致退订或关闭推送通知。
-
缺乏个性化:推送信息通常是统一的,可能与接收方的兴趣或需求不匹配,容易产生噪音。
-
资源消耗:持续推送信息需要不断的网络带宽和服务器资源,尤其在用户数较多时,可能导致系统负担加重。

2. 拉模式(Pull Model)
优点:
-
按需获取信息:接收方可以在自己需要时主动拉取信息,避免了信息过载的问题。
-
更个性化:接收方可以根据个人需求进行搜索,获取相关的、有用的信息,避免接收不相关的内容。
-
减少资源浪费:服务器不会主动发送信息,只有在接收方请求时才会提供数据,减少了系统资源的浪费。
缺点:
-
响应延迟:拉模式需要接收方主动请求,可能会导致信息获取的延迟,尤其是在接收方没有及时发出请求时。
-
用户主动性要求高:接收方需要主动去获取信息,可能会错过重要的通知或更新,尤其在信息变化频繁的情况下。
-
不适合紧急通知:对于一些需要即时通知或处理的紧急事件,拉模式可能会显得不够及时和有效。

3. 推拉结合模式(Push-Pull Hybrid Model)
优点:
-
灵活性强:结合了推模式和拉模式的优点,能够根据具体情况灵活调整,提供最合适的信息传递方式。
-
提高用户体验:通过推送确保重要信息不遗漏,通过拉取允许用户根据需求主动获取信息,满足不同场景和用户的需求。
-
个性化和及时性兼具:重要的通知可以通过推送方式及时传达,而用户可以根据兴趣或需求自主拉取信息,提升整体效率和满意度。
推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息

推送到粉丝收件箱

当前项目用户量比较小,所以这里我们选择使用推模式,延迟低、内存占比也没那么大
需要注意的是,Feed 流中的数据会不断更新,所以数据的下标也在变化,因此不能采用传统的分页模式。·这里不能使用list,只能用sortedset。我们在此处采用滚动分页模式

Redis 中的 Sorted Set(有序集合) 是一种特殊的数据结构,它结合了 集合(Set) 和 列表(List) 的特点。与普通的集合不同,Sorted Set 中的每个成员都关联着一个 分数(score),Redis 会根据这些分数对成员进行排序。
Sorted Set 的特点:
- 成员唯一:与普通集合一样,Sorted Set 中的每个成员都是唯一的,不能重复。
- 按分数排序:Sorted Set 中的成员会根据其关联的分数自动进行排序,从小到大。
- 高效的查询和范围操作:可以通过分数来进行高效的范围查询,如获取排名前几的成员,或者按照分数范围获取成员等。
- 支持多种排序方式:除了按分数排序外,还可以通过排名(索引)来排序。
保存用户发布的笔记;将该笔记推送给所有关注该用户的粉丝,使他们能够看到这篇笔记。
/**
* 保存探店笔记
*
* @param blog
* @return
*/
@Override
public Result saveBlog(Blog blog) {
Long userId = ThreadLocalUtls.getUser().getId();
blog.setUserId(userId);
// 保存探店笔记
boolean isSuccess = this.save(blog);
if (!isSuccess){
return Result.fail("笔记保存失败");
}
// 查询笔记作者的所有粉丝
List<Follow> follows = followService.list(new LambdaQueryWrapper<Follow>()
.eq(Follow::getFollowUserId, userId));
// 将笔记推送给所有的粉丝
for (Follow follow : follows) {
// 获取粉丝的id
Long id = follow.getUserId();
// 推送笔记
String key = FEED_KEY + id;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
return Result.ok(blog.getId());
}
- 查询并返回当前用户的收件箱中,粉丝关注的笔记列表;
- 使用分页和时间戳排序,确保笔记的显示顺序;
- 查询并返回每篇笔记的相关信息,如用户信息和点赞情况。
/**
* 关注推送页面的笔记分页
*
* @param max
* @param offset
* @return
*/
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
// 1、查询收件箱
Long userId = ThreadLocalUtls.getUser().getId();
String key = FEED_KEY + userId;
// ZREVRANGEBYSCORE key Max Min LIMIT offset count
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
// 2、判断收件箱中是否有数据
if (typedTuples == null || typedTuples.isEmpty()) {
return Result.ok();
}
// 3、收件箱中有数据,则解析数据: blogId、minTime(时间戳)、offset
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0; // 记录当前最小值
int os = 1; // 偏移量offset,用来计数
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2
// 获取id
ids.add(Long.valueOf(tuple.getValue()));
// 获取分数(时间戳)
long time = tuple.getScore().longValue();
if (time == minTime) {
// 当前时间等于最小时间,偏移量+1
os++;
} else {
// 当前时间不等于最小时间,重置
minTime = time;
os = 1;
}
}
// 4、根据id查询blog(使用in查询的数据是默认按照id升序排序的,这里需要使用我们自己指定的顺序排序)
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = this.list(new LambdaQueryWrapper<Blog>().in(Blog::getId, ids)
.last("ORDER BY FIELD(id," + idStr + ")"));
// 设置blog相关的用户数据,是否被点赞等属性值
for (Blog blog : blogs) {
// 查询blog有关的用户
queryUserByBlog(blog);
// 查询blog是否被点赞
isBlogLiked(blog);
}
// 5、封装并返回
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setOffset(os);
scrollResult.setMinTime(minTime);
return Result.ok(scrollResult);
}
991

被折叠的 条评论
为什么被折叠?



