Redis实战(黑马点评--点赞关注)

一、发布、查看探店笔记

保存blog

复制代码
@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());
    }
}
复制代码

思考:实体之间由id属性关联(作为查询实体信息入口)。将关联实体id写入自己的属性,将那个实体的给出的属性,定义到类,但不写入表。

当具体要用到某个实体信息时,此时再根据id查询赋给此实体。并进行返回此时的实体。

【举例】:user实体、blog实体。

前端要展示blog实体,同时,发布者(user实体)的icon、nickName。

将userId、userIcon、userName作为属性写入blog结构。

每次查询时,进行涉及关联的实体信息查询,赋值。不查询,则没有必要进行,关联实体的查询与赋值。

注:此处关联指,因为前端展示需求,而关联起来、涉及不同实体信息捆绑返回的关系。

二、点赞功能

点击点赞按钮,查看发送的请求

请求网址: http://localhost:8080/api/blog/like/4
请求方法: PUT

@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字段

修改一下

@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();
}
View Code
复制代码

 

 思考:点赞还是取消点赞,对应到更新此博客表的liked字段的加减1操作。是通过过去的状态(数据库保存的数据状态值)进行流程方向的选择的。

如果只有两种状态,可以使用redis的set集合展现存在状态。

 

展示点赞、未点赞状态:页面上还不能立即显示点赞完毕的后果,我们还需要修改查询Blog业务,判断Blog是否被当前用户点赞过   

1点赞功能使用者:当前用户。

2如何展示点赞状态:当请求queryblog的时候,需返回blog的点赞状态。

liked状态根据:通过当前用户id查询redis的set集合,根据是否存在,返回此blog实体前,赋给对应liked值。

复制代码
@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));
}
View Code
复制代码

 

 三、点赞排行榜

  • 当我们点击探店笔记详情页面时,应该按点赞顺序展示点赞用户,比如显示最早点赞的TOP5,形成点赞排行榜,就跟QQ空间发的说说一样,可以看到有哪些人点了赞
  • 之前的点赞是放到Set集合中,但是Set集合又不能排序,所以这个时候,我们就可以改用SortedSet(Zset)

由于ZSet没有isMember方法,所以这里只能通过查询score来判断集合中是否有该元素,如果有该元素,则返回值是对应的score,如果没有该元素,则返回值为null。

逻辑未改变,存储结构变了,对应结构下的能使用的方法变了。

 

 点赞功能前加一个判断逻辑:用户是否登录(从userHolder是否能拿到user),不能则return结束逻辑。

复制代码
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);
}

 

 

修改BlogServiceImpl
由于ZSet没有isMember方法,所以这里只能通过查询score来判断集合中是否有该元素,如果有该元素,则返回值是对应的score,如果没有该元素,则返回值为null

复制代码
@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);
}
复制代码

 四、关注功能

当我们进入到笔记详情页面时,会发送一个请求,判断当前登录用户是否关注了笔记博主

请求网址: http://localhost:8080/api/follow/or/not/2
请求方法: GET

当我们点击关注按钮时,会发送一个请求,实现关注/取关

请求网址: http://localhost:8080/api/follow/2/true
请求方法: PUT

关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示

复制代码
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_follow")
public class Follow implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 关联的用户id
     */
    private Long followUserId;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;
}
View Code
复制代码

Controller层中编写对应的两个方法

复制代码
@RestController
@RequestMapping("/follow")
public class FollowController {
    @Resource
    private IFollowService followService;
    //判断当前用户是否关注了该博主
    @GetMapping("/or/not/{id}")
    public Result isFollow(@PathVariable("id") Long followUserId) {
        return followService.isFollow(followUserId);
    }
    //实现取关/关注
    @PutMapping("/{id}/{isFollow}")
    public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFellow) {
        return followService.follow(followUserId,isFellow);
    }
}
复制代码

复制代码
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {

    @Override
    public Result isFollow(Long followUserId) {
        //获取当前登录的userId
        Long userId = UserHolder.getUser().getId();
        LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
        //查询当前用户是否关注了该笔记的博主
        queryWrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId);
        //只查询一个count就行了
        int count = this.count(queryWrapper);
        return Result.ok(count > 0);
    }

    @Override
    public Result follow(Long followUserId, Boolean isFellow) {
        //获取当前用户id
        Long userId = UserHolder.getUser().getId();
        //判断是否关注
        if (isFellow) {
            //关注,则将信息保存到数据库
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            save(follow);
        } else {
            //取关,则将数据从数据库中移除
            LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId);
            remove(queryWrapper);
        }
        return Result.ok();
    }
}
View Code
复制代码
比较点赞取消点赞、关注取消关注功能,发现都是根据所传数据,1设置查找条件,2根据结果,确定数据记录变更(添/删)操作。
而展示功能,则是对数据的查,最后传回一个boolean值,或封装在实体里,传回实体。

五、共同关注

点击用户头像,进入到用户详情页,可以查看用户发布的笔记,和共同关注列表

查看发送的请求

检测NetWork选项卡,查看发送的请求

查询用户信息

请求网址: http://localhost:8080/api/user/2
请求方法: GET

查看共同关注

请求网址: http://localhost:8080/api/follow/common/undefined
请求方法: GET

编写查询用户信息方法=user转userDTO返回

复制代码
@GetMapping("/{id}")
public Result queryById(@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);
}
复制代码

重启服务器,现在可以看到用户信息,但是不能看到用户发布的笔记信息,查看NetWork检测的请求,我们还需要完成这个需求

请求网址: http://localhost:8080/api/blog/of/user?&id=2&current=1
请求方法: GET

编写查询用户笔记方法=分页查询

复制代码
@GetMapping("/of/user")
    public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current, @RequestParam("id") Long id) {
        LambdaQueryWrapper<Blog> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Blog::getUserId, id);
        Page<Blog> pageInfo = new Page<>(current, SystemConstants.MAX_PAGE_SIZE);
        blogService.page(pageInfo, queryWrapper);
        List<Blog> records = pageInfo.getRecords();
        return Result.ok(records);
    }


//下面这是老师的代码,上面为另一个方法
// BlogController  根据id查询博主的探店笔记
@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);
}
View Code
复制代码

 

更改关注关系存储结构

复制代码
@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result follow(Long followUserId, Boolean isFellow) {
    //获取当前用户id
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    //判断是否关注
    if (isFellow) {
        //关注,则将信息保存到数据库
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        //如果保存成功
        boolean success = save(follow);
        //则将数据也写入Redis
        if (success) {
            stringRedisTemplate.opsForSet().add(key, followUserId.toString());
        }
    } else {
        //取关,则将数据从数据库中移除
        LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId);
        //如果取关成功
        boolean success = remove(queryWrapper);
        //则将数据也从Redis中移除
        if (success){
            stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
        }
    }
    return Result.ok();
}
View Code
复制代码

实现共同关注代码

@GetMapping("/common/{id}")
public Result followCommons(@PathVariable Long id){
    return followService.followCommons(id);
}
复制代码
@Override
public Result followCommons(Long id) {
    //获取当前用户id
    Long userId = UserHolder.getUser().getId();
    String key1 = "follows:" + id;
    String key2 = "follows:" + userId;
    //对当前用户和博主用户的关注列表取交集
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
    if (intersect == null || intersect.isEmpty()) {
        //无交集就返回个空集合
        return Result.ok(Collections.emptyList());
    }
    //将结果转为list
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    //之后根据ids去查询共同关注的用户,封装成UserDto再返回
    List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user ->
            BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
    return Result.ok(userDTOS);
}
复制代码

 

posted @   Anne起飞记  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示