达人探店
1、发布探店笔记
BlogServiceImpl.java:
public Result saveBlog(Blog blog) {
// 1、获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2、保存探店博文
boolean isSave = this.save(blog);
if (!isSave) {
return Result.fail("保存失败");
}
// 3、查询粉丝信息
LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Follow::getFollowUserId, user.getId());
List<Follow> follows = followService.list(queryWrapper);
// 4、推送博客id给粉丝
for (Follow follow : follows) {
// 4.1、获取粉丝id
Long userId = follow.getUserId();
// 4.2、推送博客id给粉丝
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
2、查看探店笔记
getBlog():
public Result getBlog(Integer id) {
Blog blog = this.getById(id);
if (blog == null) {
return Result.fail("该博客不存在");
}
User user = userService.getById(blog.getUserId());
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
UserDTO userlo = UserHolder.getUser();
if (userlo != null) {
// 获取登录用户Id
Long userloginId = userlo.getId();
// 判断用户是否点赞过使用redis的set集合
String key = BLOG_LIKED_KEY + id;//
Boolean ismember = stringRedisTemplate.opsForSet().isMember(key, userloginId.toString());
Double ismember = stringRedisTemplate.opsForZSet().score(key, userloginId.toString());
if (ismember == null) {
// if (BooleanUtil.isFalse(ismember)) {
blog.setIsLike(Boolean.valueOf(false));
} else {
blog.setIsLike(Boolean.valueOf(true));
}
}
return Result.ok(blog);
}
3、点赞功能
需求:
* 同一个用户只能点赞一次,再次点击则取消点赞
* 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
实现步骤:
* 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
* 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
* 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
* 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
采用Redis的Set集合
在Blog实体类新增字段:
@TableField(exist = false)
private Boolean isLike;
likeBlog():使用Redis的set集合进行当前用户是否点赞判断判断
public Result likeBlog(Long id) {
UserDTO userDTO = UserHolder.getUser();
if (userDTO == null) {
return Result.fail("请先登录"); }
// 获取登录用户Id
Long userId = UserHolder.getUser().getId();
// 判断用户是否点赞过使用redis的set集合
String key = BLOG_LIKED_KEY + id;
Boolean ismember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if (BooleanUtil.isFalse(ismember)) {
// 如果用户未点赞
// 修改点赞数量
boolean isSuccess = this.update()
.setSql("liked = liked + 1").eq("id", id).update();
// 保存点赞记录到redis
if (isSuccess) {
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
} else {
// 如果用户已经点过赞
// 数据库点赞数量减一
boolean isSuccess = this.update().setSql("liked = liked - 1").eq("id", id).update();
// 把用户从redis中移除
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
return Result.ok();
}
4、点赞排行榜
在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:
之前的点赞是放到set集合,但是set集合是不能排序的,所以这个时候,咱们可以采用一个可以排序的set集合,就是咱们的sortedSet
所有点赞的人,需要是唯一的,所以我们应当使用set或者是sortedSet
其次我们需要排序,就可以直接锁定使用sortedSet
修改上述likeBlog()方法:添加zset时score等于当前时间戳
// 获取登录用户Id
Long userId = UserHolder.getUser().getId();
// 判断用户是否点赞过使用redis的set集合
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
// 保存点赞记录到redis的sortedSet集合 zadd key value score
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
添加点赞列表查询getBlogLike方法:使用Redis的sortedSet集合,实现基于点赞时间戳顺序的头像先后展示效果
public Result getBlogLike(Long id) {
String key = BLOG_LIKED_KEY + id;
// 1.查询top5的点赞用户 zrange key 0 4
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList()); }
// 2.解析出其中的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idStr = StrUtil.join(",", ids);
// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
List<UserDTO> userDTOS = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 4.返回
return Result.ok(userDTOS);}
关注业务
1、关注和取消关注
因为查看博客时会判断当前用户是否关注了作者,添加iisfollow()方法:
/**
* 判断是否关注
*
* @param id
* @return
*/
@GetMapping("/or/not/{id}")
public Result isfollow(@PathVariable Long id) {
// 获取登录用户id
Long userId = UserHolder.getUser().getId();
// 查询是否关注
LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Follow::getUserId, userId);
wrapper.eq(Follow::getFollowUserId, id);
if (this.followService.getOne(wrapper) != null) {
return Result.ok(true); }
return Result.ok(false); }
关注或取消关注方法follow():
/**
* 关注
* @param id
* @return
*/
@PutMapping("/{id}/{type}")
public Result follow(@PathVariable Long id, @PathVariable("type") Boolean isFollow) {
// 获取登录用户id
Long userId = UserHolder.getUser().getId();
// 添加到redis中,方便后续查询共同关注的人
String key = "follows:" + userId;
// 判断是关注还是取关
if (isFollow) {
// 关注,添加关注记录
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(id);
boolean isSave = this.followService.save(follow);
if (isSave) {
// 添加成功,添加到redis中 sadd userId followerUserId
this.stringRedisTemplate.opsForSet().add(key, id.toString());
}
} else {
// 取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Follow::getUserId, userId);
wrapper.eq(Follow::getFollowUserId, id);
boolean isremove = this.followService.remove(wrapper);
if (isremove) {
// 删除成功,删除redis中的数据
this.stringRedisTemplate.opsForSet().remove(key, id.toString()); }
}
return Result.ok(); }
2、共同关注
想要去看共同关注的好友,需要首先进入到这个页面,这个页面会发起两个请求
1、去查询用户的详情
2、去查询用户的笔记
// UserController 根据id查询用户
@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集合中,有交集并集补集的api,我们可以把两人的关注的人分别放入到一个set集合中,然后再通过api去查看这两个set集合中的交集数据。
改进关注功能,在向数据库写入之后,同时需要存入Redis的set集合中,key为: follows:userId,表示当前用户关注的作者
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
// 把关注用户的id,放入redis的set集合 sadd userId followerUserId
boolean isSuccess = save(follow);
if (isSuccess){
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
获取共同关注方法followCommons():
/**
* 查询共同关注的人
* @param id
* @return
*/
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable Long id) {
// 获取登录用户id
Long loginUserId = UserHolder.getUser().getId();
// 登录用户的redis key
String loginKey = "follows:" + loginUserId;
// 作者的redis key
String authorKey = "follows:" + id;
// 求交集
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(loginKey, authorKey);
// 查询交集的用户信息
if (intersect==null||intersect.isEmpty()){
// 无交集
return Result.ok(Collections.emptyList()); }
// 有交集,解析交集的用户id,需要获取用户的姓名
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
// 据集合中的每项用户id,查询用户信息
List<User> userList = this.userService.listByIds(ids);
return Result.ok(userList);
/*List<UserDTO> users = userService.listByIds(ids)
.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
return Result.ok(users);*/
}
3、好友关注
3.1、好友关注-Feed流实现方案(关注推送)
1.拉模式(读扩散)
优点:节省内存空间
缺点:每次读取都需要从用户那里重新拉取到本人收件箱,耗时较长,延时较高
2、推模式(写扩散)
优点:延时低
缺点:内存占用高
3、推拉结合模式
大V的活跃用户推送模式,普通用户采用拉模式
普通V采用推送模式
对比:
3.2、好友关注-推送到粉丝收件箱
修改saveBlog方法:
在保存博客之后,需要向Redis的set粉丝集合中存入BlogId
@Override
public Result saveBlog(Blog blog) {
// 1、获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2、保存探店博文
boolean isSave = this.save(blog);
if (!isSave) {
return Result.fail("保存失败");
}
// 3、查询粉丝信息
LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Follow::getFollowUserId, user.getId());
List<Follow> follows = followService.list(queryWrapper);
// 4、推送博客id给粉丝
for (Follow follow : follows) {
// 4.1、获取粉丝id
Long userId = follow.getUserId();
// 4.2、推送博客id给粉丝
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
3.3、好友关注-实现滚动分页查询收邮箱
需求:在个人主页的“关注”卡片中,查询并展示推送的Blog信息:
具体操作如下:
1、每次查询完成后,我们要分析出查询出数据的最小时间戳,这个值会作为下一次查询的条件
2、我们需要找到与上一次查询相同的查询个数作为偏移量,下次查询时,跳过这些查询过的数据,拿到我们需要的数据
综上:我们的请求参数中就需要携带 lastId:上一次查询的最小时间戳 和偏移量这两个参数。
这两个参数第一次会由前端来指定,以后的查询就根据后台结果作为条件,再次传递到后台。
传统了分页在feed流是不适用的,因为我们的数据会随时发生变化
假设在t1 时刻,我们去读取第一页,此时page = 1 ,size = 5 ,那么我们拿到的就是10~6 这几条记录,假设现在t2时候又发布了一条记录,此时t3 时刻,我们来读取第二页,读取第二页传入的参数是page=2 ,size=5 ,那么此时读取到的第二页实际上是从6 开始,然后是6~2 ,那么我们就读取到了重复的数据,所以feed流的分页,不能采用原始方案来做。
Feed流的滚动分页
我们需要记录每次操作的最后一条,然后从这个位置开始去读取数据
举个例子:我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。我们这个地方可以采用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分页了
定义scrollResult包装类,存入BlogIds、最小时间戳和偏移量:
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
添加queryBlogOfFollow方法,查找当前登录用户关注作者的博文,且博文按时间戳倒序排序:
/**
* 查询当前登录用户关注的人的博文
* @param max
* @param offset
* @return
*/
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset) {
// 1.获取当前用户
Long userId = UserHolder.getUser().getId();
// 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count
String key = "feed:" + 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; }
}
os = minTime == max ? os : os + offset;
// 5.根据id查询blog
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = this.blogService.query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
for (Blog blog : blogs) {
// 5.1.查询blog有关的用户,附上用户nickname,图像url
User user = userService.getById(blog.getUserId());
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
// 5.2.查询blog是否被当前登录用户点赞
Long id = blog.getId();
UserDTO userlo = UserHolder.getUser();
if (userlo != null) {
// 获取登录用户Id
Long userloginId = userlo.getId();
// 判断用户是否点赞过使用redis的set集合
String key2 = BLOG_LIKED_KEY + id;
Double ismember = stringRedisTemplate.opsForZSet().score(key2, userloginId.toString());
if (ismember == null) {
blog.setIsLike(Boolean.valueOf(false)); }
else {
blog.setIsLike(Boolean.valueOf(true)); }
}
}
// 6.封装并返回
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r); }