Redis实战(黑马点评--达人探店点赞)
发布探店笔记
tb_blog
Field | Type | Collation | Null | Key | Default | Extra | Comment |
---|---|---|---|---|---|---|---|
id | bigint unsigned | (NULL) | NO | PRI | (NULL) | auto_increment | 主键 |
shop_id | bigint | (NULL) | NO | (NULL) | 商户id | ||
user_id | bigint unsigned | (NULL) | NO | (NULL) | 用户id | ||
title | varchar(255) | utf8mb4_unicode_ci | NO | (NULL) | 标题 | ||
images | varchar(2048) | utf8mb4_general_ci | NO | (NULL) | 探店的照片,最多9张,多张以”,”隔开 | ||
content | varchar(2048) | utf8mb4_unicode_ci | NO | (NULL) | 探店的文字描述 | ||
liked | int unsigned | (NULL) | YES | 0 | 点赞数量 | ||
comments | int unsigned | (NULL) | YES | (NULL) | 评论数量 | ||
create_time | timestamp | (NULL) | NO | CURRENT_TIMESTAMP | DEFAULT_GENERATED | 创建时间 | |
update_time | timestamp | (NULL) | NO | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | 更新时间 |
tb_blog_comments
Field | Type | Collation | Null | Key | Default | Extra | Comment |
---|---|---|---|---|---|---|---|
id | bigint unsigned | (NULL) | NO | PRI | (NULL) | auto_increment | 主键 |
user_id | bigint unsigned | (NULL) | NO | (NULL) | 用户id | ||
blog_id | bigint unsigned | (NULL) | NO | (NULL) | 探店id | ||
parent_id | bigint unsigned | (NULL) | NO | (NULL) | 关联的1级评论id,如果是一级评论,则值为0 | ||
answer_id | bigint unsigned | (NULL) | NO | (NULL) | 回复的评论id | ||
content | varchar(255) | utf8mb4_general_ci | NO | (NULL) | 回复的内容 | ||
liked | int unsigned | (NULL) | YES | (NULL) | 点赞数 | ||
status | tinyint unsigned | (NULL) | YES | (NULL) | 状态,0:正常,1:被举报,2:禁止查看 | ||
create_time | timestamp | (NULL) | NO | CURRENT_TIMESTAMP | DEFAULT_GENERATED | 创建时间 | |
update_time | timestamp | (NULL) | NO | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | 更新时间 |
对应的实体类
1 @Data 2 @EqualsAndHashCode(callSuper = false) 3 @Accessors(chain = true) 4 @TableName("tb_blog") 5 public class Blog implements Serializable { 6 7 private static final long serialVersionUID = 1L; 8 9 /** 10 * 主键 11 */ 12 @TableId(value = "id", type = IdType.AUTO) 13 private Long id; 14 /** 15 * 商户id 16 */ 17 private Long shopId; 18 /** 19 * 用户id 20 */ 21 private Long userId; 22 /** 23 * 用户图标 24 */ 25 @TableField(exist = false) 26 private String icon; 27 /** 28 * 用户姓名 29 */ 30 @TableField(exist = false) 31 private String name; 32 /** 33 * 是否点赞过了 34 */ 35 @TableField(exist = false) 36 private Boolean isLike; 37 38 /** 39 * 标题 40 */ 41 private String title; 42 43 /** 44 * 探店的照片,最多9张,多张以","隔开 45 */ 46 private String images; 47 48 /** 49 * 探店的文字描述 50 */ 51 private String content; 52 53 /** 54 * 点赞数量 55 */ 56 private Integer liked; 57 58 /** 59 * 评论数量 60 */ 61 private Integer comments; 62 63 /** 64 * 创建时间 65 */ 66 private LocalDateTime createTime; 67 68 /** 69 * 更新时间 70 */ 71 private LocalDateTime updateTime; 72 }
效果图
对应的代码
@PostMapping public Result saveBlog(@RequestBody Blog blog) { // 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 保存探店博文 blogService.save(blog); // 返回id return Result.ok(blog.getId()); }
上传图片的代码
@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); } }
查看探店笔记
需求:点击首页的探店笔记,会进入详情页面,我们现在需要实现页面的查询接口
随便点击一张图片,查看发送的请求
请求网址: http://localhost:8080/api/blog/6 请求方法: GET
是BlogController
下的方法,请求方式为GET,那我们直接来编写对应的方法
@GetMapping("/{id}") public Result queryById(@PathVariable Integer id){ return blogService.queryById(id); }
@Override public Result queryById(Integer id) { Blog blog = getById(id); if (blog == null) { return Result.fail("评价不存在或已被删除"); } 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()); }
将queryHotBlog
也修改一下,原始代码将业务逻辑写到了Controller中,修改后的完整代码如下
@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 queryById(@PathVariable Integer id){ return blogService.queryById(id); } }
@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 queryById(Integer id) { Blog blog = getById(id); if (blog == null) { return Result.fail("评价不存在或已被删除"); } 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()); } }
点赞功能
点击点赞按钮,查看发送的请求
请求网址: http://localhost:8080/api/blog/like/4 请求方法: PUT
看样子是BlogController中的like方法,源码如下
@PutMapping("/like/{id}") public Result likeBlog(@PathVariable("id") Long id) { // 修改点赞数量 blogService.update().setSql("liked = liked + 1").eq("id", id).update(); return Result.ok(); }
- 问题分析:这种方式会导致一个用户无限点赞,明显是不合理的
- 造成这个问题的原因是,我们现在的逻辑,发起请求只是给数据库+1,所以才会出现这个问题
- 需求
- 同一个用户只能对同一篇笔记点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
-
实现步骤
- 修改点赞功能,利用Redis中的set集合来判断是否点赞过,未点赞则点赞数
+1
,已点赞则点赞数-1
- 修改根据id查询的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改点赞功能,利用Redis中的set集合来判断是否点赞过,未点赞则点赞数
@PutMapping("/like/{id}") public Result likeBlog(@PathVariable("id") Long id) { return blogService.likeBlog(id); }
@Override public Result likeBlog(Long id) { //1. 获取当前用户信息 Long userId = UserHolder.getUser().getId(); //2. 如果当前用户未点赞,则点赞数 +1,同时将用户加入set集合 String key = BLOG_LIKED_KEY + id; Boolean isLiked = stringRedisTemplate.opsForSet().isMember(key, userId.toString()); if (BooleanUtil.isFalse(isLiked)) { //点赞数 +1 boolean success = update().setSql("liked = liked + 1").eq("id", id).update(); //将用户加入set集合 if (success) { stringRedisTemplate.opsForSet().add(key, userId.toString()); } //3. 如果当前用户已点赞,则取消点赞,将用户从set集合中移除 }else { //点赞数 -1 boolean success = update().setSql("liked = liked - 1").eq("id", id).update(); if (success){ //从set集合移除 stringRedisTemplate.opsForSet().remove(key, userId.toString()); } } return Result.ok(); }
修改完毕之后,页面上还不能立即显示点赞完毕的后果,我们还需要修改查询Blog业务,判断Blog是否被当前用户点赞过
@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 -> { queryBlogUser(blog); //追加判断blog是否被当前用户点赞,逻辑封装到isBlogLiked方法中 isBlogLiked(blog); }); return Result.ok(records); } @Override public Result queryById(Integer id) { Blog blog = getById(id); if (blog == null) { return Result.fail("评价不存在或已被删除"); } queryBlogUser(blog); //追加判断blog是否被当前用户点赞,逻辑封装到isBlogLiked方法中 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()); //3. 如果点赞了,则将isLike设置为true blog.setIsLike(BooleanUtil.isTrue(isMember)); }
点赞排行榜
- 当我们点击探店笔记详情页面时,应该按点赞顺序展示点赞用户,比如显示最早点赞的TOP5,形成点赞排行榜,就跟QQ空间发的说说一样,可以看到有哪些人点了赞
- 之前的点赞是放到Set集合中,但是Set集合又不能排序,所以这个时候,我们就可以改用SortedSet(Zset)
- 那我们这里顺便就来对比一下这些集合的区别
List | Set | SortedSet | |
---|---|---|---|
排序方式 | 按添加顺序排序 | 无法排序 | 根据score值排序 |
唯一性 | 不唯一 | 唯一 | 唯一 |
查找方式 | 按索引查找或首尾查找 | 根据元素查找 | 根据元素查找 |
修改BlogServiceImpl
由于ZSet没有isMember方法,所以这里只能通过查询score来判断集合中是否有该元素,如果有该元素,则返回值是对应的score,如果没有该元素,则返回值为null
@Override public Result likeBlog(Long id) { //1. 获取当前用户信息 Long userId = UserHolder.getUser().getId(); //2. 如果当前用户未点赞,则点赞数 +1,同时将用户加入set集合 String key = BLOG_LIKED_KEY + id; //尝试获取score Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); //为null,则表示集合中没有该用户 if (score == null) { //点赞数 +1 boolean success = update().setSql("liked = liked + 1").eq("id", id).update(); //将用户加入set集合 if (success) { stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); } //3. 如果当前用户已点赞,则取消点赞,将用户从set集合中移除 } else { //点赞数 -1 boolean success = update().setSql("liked = liked - 1").eq("id", id).update(); if (success) { //从set集合移除 stringRedisTemplate.opsForZSet().remove(key, userId.toString()); } } return Result.ok(); }
同时修改isBlogLiked方法,在原有逻辑上,判断用户是否已登录,登录状态下才会继续判断用户是否点赞
private void isBlogLiked(Blog blog) { //1. 获取当前用户信息 UserDTO userDTO = UserHolder.getUser(); //当用户未登录时,就不判断了,直接return结束逻辑 if (userDTO == null) { return; } //2. 判断当前用户是否点赞 String key = BLOG_LIKED_KEY + blog.getId(); Double score = stringRedisTemplate.opsForZSet().score(key, userDTO.getId().toString()); blog.setIsLike(score != null); }
那我们继续来完善显示点赞列表功能,查看浏览器请求,这个请求目前应该是404的,因为我们还没有写,他需要一个list返回值,显示top5点赞的用户
请求网址: http://localhost:8080/api/blog/likes/4 请求方法: GET
在Controller层中编写对应的方法,点赞查询列表,具体逻辑写到BlogServiceImpl中
@GetMapping("/likes/{id}") public Result queryBlogLikes(@PathVariable Integer id){ return blogService.queryBlogLikes(id); }
具体逻辑如下
@Override public Result queryBlogLikes(Integer id) { String key = BLOG_LIKED_KEY + id; //zrange key 0 4 查询zset中前5个元素 Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); //如果是空的(可能没人点赞),直接返回一个空集合 if (top5 == null || top5.isEmpty()) { return Result.ok(Collections.emptyList()); } List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); //将ids使用`,`拼接,SQL语句查询出来的结果并不是按照我们期望的方式进行排 //所以我们需要用order by field来指定排序方式,期望的排序方式就是按照查询出来的id进行排序 String idsStr = StrUtil.join(",", ids); //select * from tb_user where id in (ids[0], ids[1] ...) order by field(id, ids[0], ids[1] ...) 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); }