达人探店
约 1731 字大约 6 分钟
2025-07-09
8.1 发布探店笔记
探店笔记类似于点评网站的评价,通常采用图文结合的形式,用于分享个人体验和推荐。主要涉及两个表:
tb_blog
:探店笔记表,包含标题、文字、图片等信息。tb_blog_comments
:用户对探店笔记的评价。
源代码已完成这部分功能。在 App 首页,点击最下方菜单栏中的 “+” 按钮,即可进入发布探店图文的界面。点击 “发布” 按钮,即可完成探店图文的发布。

8.2 查看探店笔记
在主页展示热门探店博客列表,需要返回以下信息:
- 博客基础内容(如博客标题、图片,由前端控制)。
- 作者头像,昵称。
/**
* 查询热门博客列表
* @param current
* @return
*/
@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);
});
return Result.ok(records);
}
查看探店笔记详情时,需要返回以下信息:
- 博客的全部内容。
- 作者头像,昵称。
/**
* 根据 id 返回博客信息
*
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
// 1. 查询 blog
Blog blog = getById(id);
if (blog == null) {
return Result.fail(BLOG_NOT_FOUND);
}
// 2. 查询发布 blog 的用户信息
queryBlogUser(blog);
return Result.ok(blog);
}
/**
* 根据 blog 设置用户相关字段
*
* @param blog
*/
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setIcon(user.getIcon());
blog.setName(user.getNickName());
}
8.3 点赞功能
初始代码如下:
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
//修改点赞数量
blogService.update().setSql("liked = liked +1 ").eq("id",id).update();
return Result.ok();
}
初始代码只实现了点赞数量的简单递增,存在一个用户可以无限点赞的问题。
我们需要完善博客点赞功能:
- 同一个用户只能点赞一次,再次点击则取消点赞。
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段 Blog 类的
isLike
属性)。
实现步骤:
- 给
Blog
类中添加一个isLike
字段,标识是否被当前用户点赞。 - 修改点赞功能,利用 Redis 的 Set 集合判断是否点赞过,未点赞过则点赞数 +1,已点赞过则点赞数 -1。
- 修改根据 ID 查询
Blog
的业务,判断当前登录用户是否点赞过,赋值给isLike
字段。 - 修改分页查询
Blog
业务,判断当前登录用户是否点赞过,赋值给isLike
字段。
为什么采用 Set 集合
Set 集合中的数据不能重复,可以保证每个用户只能点赞一次。
Blog
@TableField(exist = false)
private Boolean isLike;
queryById
@Override
public Result queryById(Long id) {
// 1. 查询 blog
Blog blog = getById(id);
if (blog == null) {
return Result.fail(BLOG_NOT_FOUND);
}
// 2. 查询发布 blog 的用户信息
queryBlogUser(blog);
// 3. 查询是否点赞该 blog
isLikedBlog(blog);
return Result.ok(blog);
}
isLikedBlog
/**
* 根据 blog 设置是否点赞该博客
* @param blog
*/
private void isLikedBlog(Blog blog) {
UserDTO user = UserHolder.getUser();
if (user != null) {
String key = BLOG_LIKED_KEY + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, user.getId().toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
}
likeBlog
/**
* 根据 id 点赞博客
* @param id
*/
@Override
public void 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 更新数据库
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
// 3.2 写入 Redis
if (isSuccess) {
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
} else {
// 4. 如果已点赞,取消点赞
// 4.1 更新数据库
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
// 4.2 写入 Redis
if (isSuccess) {
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
}
queryHotBlog
@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.isLikedBlog(blog);
});
return Result.ok(records);
}
8.4 点赞排行榜
在探店笔记的详情页面,为了增强互动性和用户体验,我们需要将给该笔记点赞的用户展示出来,形成一个点赞排行榜。目标是展示最早点赞的 Top 5 用户。
之前的方案是将点赞用户存储在 Set
集合中,但 Set
集合无法排序,因此不符合需求。我们需要一种能够排序且保证元素唯一性的数据结构。Redis 中可使用的数据结构如下表所示:
List | Set | SortedSet | |
---|---|---|---|
排序方式 | 按添加顺序排序 | 无法排序 | 根据 score 值排序 |
唯一性 | 不唯一 | 唯一 | 唯一 |
查找方式 | 按索引查找或首尾查找 | 根据元素查找 | 根据元素查找 |
根据需求,我们需要存储唯一的点赞用户 ID,并且能够根据点赞时间进行排序,因此 Sorted Set
是最合适的选择。Sorted Set
既能保证元素的唯一性,又能根据 score
值进行排序
首先需要将之前的代码中 Set 更换为 Sorted Set。
likeBlog
@Override
public void 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 更新数据库
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
// 3.2 写入 Redis
if (isSuccess) {
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}
} else {
// 4. 如果已点赞,取消点赞
// 4.1 更新数据库
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
// 4.2 写入 Redis
if (isSuccess) {
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
}
isLikedBlog
private void isLikedBlog(Blog blog) {
UserDTO user = UserHolder.getUser();
if (user != null) {
String key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, user.getId().toString());
blog.setIsLike(score != null);
}
}
代码解释:
- 在 Sorted Set 中,并没有与
ISMEMBER
类似的命令,但是可以使用ZSCORE
判断有序集中是否含有该成员:含有则返回具体分数,未含有则返回null
。
接着添加点赞排行榜相关代码。
控制器
/**
* 根据 id 查询博客的点赞列表
* @param id
* @return
*/
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
return blogService.queryBlogLikes(id);
}
实现类
/**
* 根据 id 查询博客的点赞列表
* @param id
* @return
*/
@Override
public Result queryBlogLikes(Long id) {
String key = BLOG_LIKED_KEY + id;
// 1. 查询 top5 点赞用户
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 idsStr = StrUtil.join(",", ids);
// 3. 根据用户 id 查询用户
List<UserDTO> userDTOS = userService.query()
.in("id", ids).last("ORDER BY FIELD(id, " + idsStr + ")").list()
.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOS);
}
代码解释:
ORDER BY FIELD(id, 5, 1)
:这部分 SQL 用于按照指定的 ID 顺序排序,保证查询结果的顺序与点赞顺序一致