Redis实战(黑马点评--达人探店点赞)

发布时间 2023-07-08 23:45:36作者: 夏雪冬蝉

发布探店笔记

tb_blog

FieldTypeCollationNullKeyDefaultExtraComment
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

FieldTypeCollationNullKeyDefaultExtraComment
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 }
View Code

效果图

 对应的代码

@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,所以才会出现这个问题
  • 需求
    1. 同一个用户只能对同一篇笔记点赞一次,再次点击则取消点赞
    2. 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
  • 实现步骤

    1. 修改点赞功能,利用Redis中的set集合来判断是否点赞过,未点赞则点赞数+1,已点赞则点赞数-1
    2. 修改根据id查询的业务,判断当前登录用户是否点赞过,赋值给isLike字段
    3. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
@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)
  • 那我们这里顺便就来对比一下这些集合的区别

 

ListSetSortedSet
排序方式 按添加顺序排序 无法排序 根据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);
}