点赞功能
业务逻辑分析
该点赞功能业务旨在实现用户对博客的点赞与取消点赞操作,同时确保每个用户仅能点赞一次,且前端能根据点赞状态高亮显示按钮。
- 数据结构设计:
- 在Blog类中添加isLike字段,用于标识当前登录用户是否对该博客点赞。
- 点赞逻辑(利用Redis的Set集合):
- 以 "blog:liked:" + 博客id 为键,value为用户ID集合。检查用户ID是否存在于对应博客的Set中:
- 若不存在,将用户ID加入Set,并将博客点赞数 +1 (表示点赞成功)
- 若存在,从Set中移除用户ID,并将博客点赞数 -1 (表示取消点赞)
- 以 "blog:liked:" + 博客id 为键,value为用户ID集合。检查用户ID是否存在于对应博客的Set中:
SADD blog:liked:1001 123 // 用户ID=123点赞博客ID=1001
SREM blog:liked:1001 123 // 用户取消点赞
SISMEMBER blog:liked:1001 123 // 检查用户是否已点赞
- 查询逻辑:
- 单个博客查询:根据博客ID查询时,检查当前登录用户ID是否在 blog:{blogId}:likes 的Set中,若存在则 isLike = true,否则 isLike = false。
- 分页查询:对分页结果中的每个博客,执行上述检查,为每个博客的isLike字段赋值。
- 前端展示:
- 前端根据isLike字段的值,决定点赞按钮是否高亮显示(已实现)。
避免重复点赞:使用Redis的Set集合存储每个博客的点赞用户ID,通过SADD、SREM、SISMEMBER命令实现用户唯一性校验,确保单用户仅能点赞一次。
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 查询热门博客(分页查询)
* @param current
* @return
*/
@Override
public Result queryHotBlog(Integer current) {
// 1.根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 2.获取当前页数据
List<Blog> records = page.getRecords();
// 3.查询用户 以及 blog是否被点赞了(给isLike属性赋值)
records.forEach(blog -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
/**
* 根据id查询博客(单个查询)
*
* @param id
* @return
*/
@Override
public Result queryBlogById(Long id) {
// 1.查询博客blog
Blog blog = getById(id);
if (blog == null) {
return Result.fail("笔记不存在!");
}
// 2.查询blog有关的用户
queryBlogUser(blog);
// 3.查询blog是否被点赞了(给isLike属性赋值)
isBlogLiked(blog);
return Result.ok(blog);
}
/**
* 判断博客是否被点赞了
* @param blog
*/
private void isBlogLiked(Blog blog) {
// 1. 获取登录用户(判空)
UserDTO user = UserHolder.getUser();
if (user == null) {
// 未登录用户默认未点赞
blog.setIsLike(false);
return;
}
Long userId = user.getId();
// 2. 判断是否点赞
String key = "blog:liked:" + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
/**
* 点赞博客
*
* @param id
* @return
*/
@Override
public Result likeBlog(Long id) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登录用户是否已经点赞
String key = "blog:liked:" + 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();
//4.2 将用户从redis的set集合中移除
if(isSuccess){
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
return Result.ok();
}
/**
* 查询用户
*
* @param blog
*/
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}
点赞排行榜
1. 核心需求
- 展示给用户“最早点赞该博客的TOP N用户列表”
2. 技术实现
数据结构选择:
- Redis Sorted Set(ZSet):存储每个博客的点赞用户ID及时间戳(作为排序依据)。
- Key格式:BLOG_LIKED_KEY + 博客id
- Value格式:用户id + 时间戳(作为排序依据)
3. 业务逻辑
- 写入排行榜:用户点赞时,记录时间戳到ZSet
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
- 查询排行榜:
- 按时间升序(最早点赞在前)获取TOP N用户
-
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); //前5名
- 根据用户ID列表查询用户信息(头像、昵称)
- 顺序保持:SQL中使用IN语句和 ORDER BY FIELD () 保证返回顺序与Redis中一致
// 3.根据用户id查询用户 where id in(5,1) ORDER BY FIELD(id ,5, 1)
String idStr = StrUtil.join(",", ids);
List<User> users = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
//将User对象转换为UserDTO对象
List<UserDTO> userDTOS = users
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
实现代码
代码功能解析
1. 点赞功能(likeBlog方法)
/**
* 点赞博客
*
* @param id
* @return
*/
@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集合中 zadd key value score
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();
//4.2 将用户从redis的set集合中移除
if (isSuccess) {
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
return Result.ok();
}
-
实现逻辑:
-
使用
ZSet
检查用户是否已点赞(score
是否存在)。 -
未点赞时:数据库
liked
字段+1,并写入Redis(用户ID + 时间戳)。 -
已点赞时:数据库
liked
字段-1,并从Redis删除用户ID。
-
-
优点:
-
使用时间戳作为排序依据,天然支持最早点赞排行。
-
通过
ZSet
的score
判断用户是否点赞,效率较高。
-
2. 查询点赞排行榜(queryBlogLikes方法)
/**
* 查询点赞列表(排行榜)
*
* @param id
* @return
*/
public Result queryBlogLikes(Long id) {
String key = BLOG_LIKED_KEY + id;
// 1.查询top5的点赞用户(最早点赞的前五位用户) zrange key 0 4 查到的是redis中的值即是userId
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 2.解析出这些用户id(将Redis查询到的字符串类型用户ID集合(Set<String>)转换为Long类型列表)
//创建流管道 使用Long.valueOf转换每个元素 收集元素到List集合。
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
/*List<Object> ids = new ArrayList<>();
//top5.forEach(s -> ids.add(Long.valueOf(id)));
for (String s : top5) {
ids.add(Long.valueOf(s));
}*/
// 3.根据用户id查询用户 where id in(5,1) ORDER BY FIELD(id ,5, 1)
String idStr = StrUtil.join(",", ids);
List<User> users = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
//将User对象转换为UserDTO对象
List<UserDTO> userDTOS = users
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 4.将用户信息返回
return Result.ok(userDTOS);
}
-
实现逻辑:
-
从Redis的
ZSet
中获取前5个用户ID(按时间升序)。 -
将用户ID转换为
Long
列表,使用ORDER BY FIELD
保持顺序。 -
转换为
UserDTO
返回前端(脱敏)。
-
-
优点:
-
通过
ORDER BY FIELD
保证数据库查询结果与Redis顺序一致。 -
返回脱敏后的
UserDTO
,避免敏感信息泄露。
-