Redis--黑马点评--达人探店功能实现详解
达人探店
发布探店笔记
探店笔记类似于点评网站的评价,往往是图文结合,对应的表有两个:
-
tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
-
tb_blog_comments:其他用户对探店笔记的评价
tb_blog表结构如下:
可以从结构中看出,跟发布探店笔记相关的字段有shop_id,title,images,content,liked,comments
业务流程:
点击首页最下方菜单栏的+按钮,即可发布探店图文:
需要注意的是发布照片与发布笔记是两个分离的功能,因为上传照片的功能不仅仅是发笔记有这个需求,在其他业务中可能也会需要发布照片,因此上传照片功能是一个独立功能,点击上传照片时会先发出一个请求实现上传,上传成功后返回这张图片的地址,也就是上传之后的可访问的地址,这个地址就会做为表单的参数,在发布笔记时一起提交到后台,即在发布笔记时上传的其实不是照片的本身,而是上传成功后的图片地址,因此发布照片和发布笔记是不同的功能,会发起两个不同的请求。
而黑马点评这边已经将接口写好了,http: //127.0.0.1:8081/upload/blog,这个接口是为了上传图片的,因此需要修改存放的地址,是将其存放在前端服务器中的目录中,另一个接口http: //127.0.0.1:8081/blog,上传博客。
测试接口:
可以查看数据库表:
而在发布完探店笔记后,发现没有实现查看探店笔记的接口。
因此:
案例展示:实现查看探店笔记的接口
需求:点击首页的探店笔记,会进入详情页面,实现该页面的查询接口:
controller层:
@GetMapping("/{id}")
public Result queryBlog(@PathVariable("id") Long id){return blogService.queryBlogById(id);
}
service层
@Override
public Result queryBlogById(Long id) {// 1.查询blogBlog blog = getById(id);if (blog == null) {return Result.fail("笔记不存在!");}// 2.查询blog有关的用户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());}
测试:
点赞
在首页的探店笔记排行榜和探店图文详情页面都有点赞功能:
在Java代码中直接利用MyBatisPlus来修改数据库中的liked字段,每一次点赞都是进行一次接口请求,并且没有点赞限制,没有进行用户判断。而且是直接修改数据库,影响性能。
案例:完善点赞功能
需求:
-
同一个用户只能点赞一次,再次点击则为取消点赞
-
如果当前用户已经点赞,则点赞按钮高亮(前端已实现,判断字段blog类的isLike属性)
实现思路:可以在新建一个表,记录笔记的id,以及点赞用户的id,这样每次点赞只需要看表中存在不存在这个用户的值即可,不需要去使用MySQL数据库,可以使用轻量级的数据库,比如redis,可以使用集合类的数据结构,key为笔记的id,value则为点赞的用户id,并且一个用户只能点赞一次,所以value不能重复,所以可以使用set数据结构。
实现步骤:
-
给blog类中添加一个isLike字段,标识是否被当前用户点赞
-
修改点赞功能,利用redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1,并且点赞过的用户需要添加在set集合中。
-
修改根据id查询blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
-
修改分页查询blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
代码实现:
/*** 用户图标*/@TableField(exist = false)private String icon;/*** 用户姓名*/@TableField(exist = false)private String name;/*** 是否点赞过了*///该注解表示该成员变量不是数据库表中的字段@TableField(exist = false)private Boolean isLike;
service层业务代码:
public Result 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(Boolean.FALSE.equals(isMember)){//3.如果未点赞,可以点赞//3.1点赞数+1boolean success = lambdaUpdate().set(Blog::getLiked, getById(id).getLiked() + 1).eq(Blog::getId, id).update();//3.2,将用户id存储到redis的set集合中if (success) {stringRedisTemplate.opsForSet().add(key, userId.toString());}}else {//4.如果已点赞,取消点赞//4.1.数据库点赞数-1,boolean success = lambdaUpdate().set(Blog::getLiked, getById(id).getLiked() - 1).eq(Blog::getId, id).update();//4.2将用户id从Redis的set集合中删除if (success) {stringRedisTemplate.opsForSet().remove(key, userId.toString());}}return Result.ok();}
其次,我们还需要修改查询blog的业务代码,在查询用户之后,还需要查询该博客是否被点赞,再去修改isLiked的值。
由于查询有两种查询,一种分页查询,一种普通查询,因此我们将是否被点赞这个业务封装成函数,进行简化代码。
代码实现:
private void isBlogLiked(Blog blog) {Long userId = UserHolder.getUser().getId();String key = BLOG_LIKED_KEY + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());//防止包装类为空blog.setIsLike(Boolean.TRUE.equals(isMember));}
进行测试:
进行点赞:
检查redis数据库:
取消点赞:
再次查看Redis数据库:
发现已经不存在。
至此业务实现成功。
点赞排行榜
业务详情:
在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的top5,形成点赞排行榜:
我们可以借鉴微信朋友圈点赞的机制。谁先点赞,谁在前面
接口信息:
案例实现:实现查询点赞排行榜的接口
需求:按照点赞时间先后排序,返回top5的用户
思路分析:
我们需要按照点赞时间先后排序,但是点赞的用户信息存储在redis中的set集合里,set集合是无序集合,因此我们需要去更换集合,同时还要满足唯一性与有序性,将Redis中的集合数据结构进行对比。
List | Set | SortedSet | |
---|---|---|---|
排序方式 | 按添加顺序排序 | 无序 | 可排序,根据score值 |
唯一性 | 不唯一 | 唯一 | 唯一 |
查找方式 | 按照索引查找或者首尾查找 | 按照元素查找 | 根据元素查找 |
由此看来,SortedSet更符合业务需求。
但是SortedSet和set有很多不同之处,就别去set中有sismember命令去查询某个元素是否在集合内,但是SortedSet就没有这样的命令,但是我们可以使用zscore命令来查询score值,如果元素不存在则score值就不存在,如果存在则有score值。这个命令也可以判断元素是否存在。如果查询排行榜可以使用zrange命令来查询。zrange命令就是查找范围内的元素。并且会天然的排序,可以按照时间戳来排序时,就会按照时间戳从小到大排序,最早插入的在最前面,这样就可以完成查询排行榜的业务。
思路实现:
改造之前的点赞业务,将set集合替换为SortedSet集合:
private void isBlogLiked(Blog blog) {Long userId = UserHolder.getUser().getId();String key = BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());//防止包装类为空blog.setIsLike(score != null);}@Overridepublic 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点赞数+1boolean success = lambdaUpdate().set(Blog::getLiked, getById(id).getLiked() + 1).eq(Blog::getId, id).update();//3.2,将用户id存储到redis的set集合中if (success) {stringRedisTemplate.opsForZSet().add(key, userId.toString(),System.currentTimeMillis());}}else {//4.如果已点赞,取消点赞//4.1.数据库点赞数-1,boolean success = lambdaUpdate().set(Blog::getLiked, getById(id).getLiked() - 1).eq(Blog::getId, id).update();//4.2将用户id从Redis的set集合中删除if (success) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return Result.ok();}
实现排行榜接口:
@Overridepublic Result queryBlogLikes(Long id) {//1.获取当前blog的点赞top5String key = BLOG_LIKED_KEY + id;Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);//2.解析出其中的用户id,map映射,将long类型的字符串转为Longif (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());//3.根据用户ID查询用户,这里直接返回的是user对象,但是在前端页面应该显示的是UserDTO,所以这里需要将user对象转为UserDTO对象List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());//4.返回return Result.ok(userDTOS);}
进行测试:
先尝试一个账号点赞:
测试成功,在进行多个账户点赞:
发现bug:新开网页时,发现下面的博客不登录看不到,根据报错提示发现是空指针异常,即userid为空,解决方案,在从UserHolder中取user时先进行判断,如果为空,则直接return;
代码演示:
private void isBlogLiked(Blog blog) {UserDTO user = UserHolder.getUser();if (user == null){return;}Long userId = user.getId();String key = BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());//防止包装类为空blog.setIsLike(score != null);}
解决;
继续测试:
发现顺序不对,去IDEA中检查:
发现顺序是对的,在进入数据库中查询:
发现是使用关键字in的问题,
因此要使用order by ,但不能直接指定id,因为默认order by id 的话是按顺序从小到大的,因此要指定顺序:
因此需要修改查询语句,又因为MyBatisPlus没有封装该方法,只能去自定义SQL语句。
修改代码如下:
//3.根据用户ID查询用户,这里直接返回的是user对象,但是在前端页面应该显示的是UserDTO,所以这里需要将user对象转为UserDTO对象//where id in (ids) order by field(id,ids)List<UserDTO> userDTOS = userService.lambdaQuery().in(User::getId, ids).last("order by field(id,"+ idStr+")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
再次进行刷新测试:
至此,点赞排行榜业务实现成功。
希望对大家有所帮助!