2025.09.25

黑马点评开发日志Day06——达人探店、好友关注

一、达人探店

1.1 发布探店笔记

  • 探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:
    • tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
    • tb_blog_comments:其他用户对探店笔记的评价

发布探店笔记页面原型

1.1.1 UploadController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {

@PostMapping("blog")
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}

@GetMapping("/blog/delete")
public Result deleteBlogImg(@RequestParam("name") String filename) {
File file = new File(SystemConstants.IMAGE_UPLOAD_DIR, filename);
if (file.isDirectory()) {
return Result.fail("错误的文件名称");
}
FileUtil.del(file);
return Result.ok();
}

private String createNewFileName(String originalFilename) {
// 获取后缀
String suffix = StrUtil.subAfter(originalFilename, ".", true);
// 生成目录
String name = UUID.randomUUID().toString();
int hash = name.hashCode();
int d1 = hash & 0xF;
int d2 = (hash >> 4) & 0xF;
// 判断目录是否存在
File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2));
if (!dir.exists()) {
dir.mkdirs();
}
// 生成文件名
return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);
}
}

1.1.2 BlogController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/blog")
public class BlogController {

@Resource
private IBlogService blogService;


@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}
}

1.2 查看探店笔记

  • 需求:点击首页的探店笔记,会进入详情页面,实现该页面的查询接口:

查看探店笔记页面原型

1.2.1 BlogController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@RestController
@RequestMapping("/blog")
public class BlogController {

@Resource
private IBlogService blogService;


@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}

@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
// 修改点赞数量
blogService.update()
.setSql("liked = liked + 1").eq("id", id).update();
return Result.ok();
}

@GetMapping("/of/me")
public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}

@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
return blogService.queryHotBlog(current);
}

@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id) {
return blogService.queryBlogById(id);
}
}

1.2.2 IBlogService

1
2
3
4
5
6
public interface IBlogService extends IService<Blog> {

Result queryBlogById(Long id);

Result queryHotBlog(Integer current);
}

1.2.3 BlogServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

@Resource
private IUserService userService;

@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(this::queryBlogUser);
return Result.ok(records);
}

@Override
public Result queryBlogById(Long id) {
// 1. 查询blog
Blog blog = getById(id);
if (blog == null) {
return Result.fail("笔记不存在");
}
// 2. 查询blog有关的用户
queryBlogUser(blog);
return Result.ok(blog);
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}

1.3 点赞探店笔记

  • 在首页的探店笔记排行榜和探店图文详情页面都有点赞的功能:
    点赞探店笔记页面原型
  • 需求:
    • 同一个用户只能点赞一次,再次点击则取消点赞
    • 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
  • 实现步骤:
    1. 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
    2. 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
    3. 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
    4. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

1.3.1 BlogServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

@Resource
private IUserService userService;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}

@Override
public Result queryBlogById(Long id) {
// 1. 查询blog
Blog blog = getById(id);
if (blog == null) {
return Result.fail("笔记不存在");
}
// 2. 查询blog有关的用户
queryBlogUser(blog);
// 3. 查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}

private void isBlogLiked(Blog blog) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}

@Override
public Result likeBlog(Long id) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if (BooleanUtil.isFalse(isMember)) {
// 3. 如果未点赞,可以点赞
// 3.1 数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
// 3.2 保存用户到Redis的set集合
if (isSuccess) {
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
}else {
// 4. 如果已点赞,取消点赞
// 4.1 数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
isSuccess = update().eq("id", id).update();
// 4.2 把用户从Redis的set集合移除
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
return Result.ok();
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}

1.4 点赞排行榜

  • 在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:
    点赞排行榜页面原型

1.4.1 BlogController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@RestController
@RequestMapping("/blog")
public class BlogController {

@Resource
private IBlogService blogService;


@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}

@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
return blogService.likeBlog(id);
}

@GetMapping("/of/me")
public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}

@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
return blogService.queryHotBlog(current);
}

@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id) {
return blogService.queryBlogById(id);
}

@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
return blogService.queryBlogLikes(id);
}
}

1.4.2 IBlogService

1
2
3
4
5
6
7
8
9
10
public interface IBlogService extends IService<Blog> {

Result queryBlogById(Long id);

Result queryHotBlog(Integer current);

Result likeBlog(Long id);

Result queryBlogLikes(Long id);
}

1.4.3 BlogServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

@Resource
private IUserService userService;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}

@Override
public Result queryBlogById(Long id) {
// 1. 查询blog
Blog blog = getById(id);
if (blog == null) {
return Result.fail("笔记不存在");
}
// 2. 查询blog有关的用户
queryBlogUser(blog);
// 3. 查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}

private void isBlogLiked(Blog blog) {
// 1. 获取登录用户
UserDTO user = UserHolder.getUser();
if (user == null) {
// 用户未登录,无需查询是否点赞
return;
}
Long userId = user.getId();
// 2. 判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score != null);
}

@Override
public Result likeBlog(Long id) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
if (score == null) {
// 3. 如果未点赞,可以点赞
// 3.1 数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
// 3.2 保存用户到Redis的set集合
if (isSuccess) {
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}
}else {
// 4. 如果已点赞,取消点赞
// 4.1 数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
isSuccess = update().eq("id", id).update();
// 4.2 把用户从Redis的set集合移除
if (isSuccess) {
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
return Result.ok();
}

@Override
public Result queryBlogLikes(Long id) {
// 1. 查询top5点赞用户 zrange key 0 4
String key = BLOG_LIKED_KEY + id;
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查询用户
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);
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}

二、好友关注

2.1 关注和取关

  • 在探店图文的详情页面中,可以关注发布笔记的作者:
    关注和取关页面原型
  • 需求:基于该表数据结构,实现两个接口:
    • 关注和取关接口
    • 判断是否关注的接口
  • 关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:
    关注表结构

2.1.1 FollowController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/follow")
public class FollowController {

@Resource
private IFollowService followService;

@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable boolean isFollow) {
return followService.follow(followUserId, isFollow);
}

@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId) {
return followService.isFollow(followUserId);
}
}

2.1.2 IFollowService

1
2
3
4
5
6
public interface IFollowService extends IService<Follow> {

Result follow(Long followUserId, boolean isFollow);

Result isFollow(Long followUserId);
}

2.1.3 FollowServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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) {
// 3. 关注,新增数据
Follow follow = new Follow();
follow.setFollowUserId(followUserId);
follow.setUserId(userId);
save(follow);
}else {
// 4. 取关,删除 delete from tb_follow where userId = ? and follow_user_id = ?
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. 查询是否关注 select count(*) from tb_follow where ...
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
// 3. 判断
return Result.ok(count > 0);

}

}

2.2 共同关注

  • 点击博主头像,可以进入博主首页:
    博主首页页面原型
  • 博主个人首页依赖两个接口:
    • 根据id查询user信息:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @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);
      }
    • 根据id查询博主的探店笔记:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @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中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
    共同关注页面原型

2.2.1 FollowController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/follow")
public class FollowController {

@Resource
private IFollowService followService;

@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable boolean isFollow) {
return followService.follow(followUserId, isFollow);
}

@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId) {
return followService.isFollow(followUserId);
}

@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id) {
return followService.followCommons(id);
}
}

2.2.2 IFollowService

1
2
3
4
5
6
7
8
public interface IFollowService extends IService<Follow> {

Result follow(Long followUserId, boolean isFollow);

Result isFollow(Long followUserId);

Result followCommons(Long id);
}

2.2.3 FollowServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private UserServiceImpl userService;

@Override
public Result follow(Long followUserId, boolean isFollow) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;

// 2. 判断到底是关注还是取关
if (isFollow) {
// 3. 关注,新增数据
Follow follow = new Follow();
follow.setFollowUserId(followUserId);
follow.setUserId(userId);
boolean isSuccess = save(follow);
if (isSuccess) {
// 把关注的用户的id放入redis的set集合
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
}else {
// 4. 取关,删除 delete from tb_follow where userId = ? and follow_user_id = ?
boolean isSuccess = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));
// 把关注的用户的id移除redis的set集合
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}

@Override
public Result isFollow(Long followUserId) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 查询是否关注 select count(*) from tb_follow where ...
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
// 3. 判断
return Result.ok(count > 0);

}

@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. 解析集合
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);
}
}

2.3 关注推送

  • 关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。

  • Feed流产品有两种常见模式:

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

    • 拉模式
    • 推模式
    • 推拉结合
  • 拉模式:也叫做读扩散。

  • 推模式:也叫做写扩散。

  • 推拉结合:也叫做读写混合,兼具推和拉两种模式的优点。

  • 三种模式比较

拉模式 推模式 推拉结合
写比例
读比例
用户读取延迟
实现难度 复杂 简单 很复杂
使用场景 很少使用 用户量少、没有大V 过千万的用户量、有大V

2.4 基于推模式实现关注推送功能

  • 需求:

    • 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
    • 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
    • 查询收件箱数据时,可以实现分页查询
  • Feed流的分页问题:Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

  • Feed流的滚动分页:Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

2.4.1 BlogController

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/blog")
public class BlogController {

@Resource
private IBlogService blogService;


@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
}

2.4.2 IBlogService

1
2
3
4
5
public interface IBlogService extends IService<Blog> {


Result saveBlog(Blog blog);
}

2.4.3 BlogServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

@Resource
private IUserService userService;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private IFollowService followService;

@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. 查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
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());
}

}

2.5 实现关注推送页面的分页查询

  • 需求:在个人主页的“关注”卡片中,查询并展示推送的Blog信息:
    关注推送页面原型

  • 分页查询参数

    • max:上一次查询的最小时间戳或当前时间(第一次查询)
    • min:0
    • offset:与上一次查询最小时间戳一致的所有元素的个数或0(第一次查询)
    • count:分页大小

2.5.1 BlogController

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/blog")
public class BlogController {

@Resource
private IBlogService blogService;

@GetMapping("/of/follow")
public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset ) {
return blogService.queryBlogOfFollow(max, offset);
}
}

2.5.2 IBlogService

1
2
3
4
public interface IBlogService extends IService<Blog> {

Result queryBlogOfFollow(Long max, Integer offset);
}

2.5.3 BlogServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

@Resource
private IUserService userService;

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private IFollowService followService;

private void isBlogLiked(Blog blog) {
// 1. 获取登录用户
UserDTO user = UserHolder.getUser();
if (user == null) {
// 用户未登录,无需查询是否点赞
return;
}
Long userId = user.getId();
// 2. 判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score != null);
}

@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);
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;
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
// 3.1 获取id
ids.add(Long.valueOf(tuple.getValue()));
// 3.2 获取分数
long time = tuple.getScore().longValue();
if (time == minTime) {
os++;
}else {
minTime = time;
os = 1;
}
}
// 4. 根据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) {
// 4.1 查询blog有关的用户
queryBlogUser(blog);
// 4.2 查询blog是否被点赞
isBlogLiked(blog);
}
// 5. 封装并返回
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}

private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}