【Redis实战篇】达人探店
1. 达人探店-发布探店笔记
发布探店笔记
探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:
tb_blog
:探店笔记表,包含笔记中的标题、文字、图片等
tb_blog_comments
:其他用户对探店笔记的评价
具体发布流程:
图片上传接口:
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {@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);}}@GetMapping("/blog/delete")public Result deleteBlogImg(@RequestParam("name") String filename) {File file = new File(SystemConstants.IMAGE_UPLOAD_DIR, filename);if (file.isDirectory()) {return Result.fail("错误的文件名称");}FileUtil.del(file);return Result.ok();}private String createNewFileName(String originalFilename) {// 获取后缀String suffix = StrUtil.subAfter(originalFilename, ".", true);// 生成目录String name = UUID.randomUUID().toString();int hash = name.hashCode();int d1 = hash & 0xF;int d2 = (hash >> 4) & 0xF;// 判断目录是否存在File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2));if (!dir.exists()) {dir.mkdirs();}// 生成文件名return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);}
}
注意:读者在操作时,需要修改SystemConstants.IMAGE_UPLOAD_DIR
自己图片所在的地址,在实际开发中图片一般会放在nginx上或者是阿里OSS云存储服务上。
保存博客笔记:
BlogController
@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;/*** 保存博客* @param blog* @return*/@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn Result.ok(blog.getId());}
}
2. 达人探店-查看探店笔记
实现查看发布探店笔记的接口
实现代码:
BlogServiceImpl
/*** 根据博客id查询博客* @param id* @return*/@Overridepublic Result queryBlogById(Long id) {//1.查询blogBlog blog = getById(id);if(blog == null) return Result.fail("blog不存在!");//2.根据blog查询相关用户queryBlogUser(blog);return Result.ok(blog);}/*** 增加博客关联的用户名及用户头像信息* @param blog*/private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
3. 达人探店-点赞功能
初始代码
BlogController类
@GetMapping("/likes/{id}")
public Result likeBlog(@PathVariable("id") Long id) {//修改点赞数量blogService.update().setSql("liked = liked +1 ").eq("id",id).update();return Result.ok();
}
问题分析:这种方式会导致一个用户无限点赞,明显是不合理的
造成这个问题的原因是,我们现在的逻辑,发起请求只是给数据库+1,所以才会出现这个问题。
完善点赞功能
需求:
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的
isLike
属性)
实现步骤:
- 给Blog类中添加一个
isLike
字段,标示是否被当前用户点赞 - 修改点赞功能,利用Redis的
set
集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1 - 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给
isLike
字段 - 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给
isLike
字段
为什么采用set集合:
因为我们的数据是不能重复的,一个用户只能点赞一次,要么在set集合中存在,sadd
添加当前用户的userId
;要么取消点赞,则srem
移除set中的用户userId
具体步骤:
1、在 Blog
类 添加一个字段
@TableField(exist = false)
private Boolean isLike;
2、修改代码
/*** 设置点赞功能* @param id* @return*/@Overridepublic Result likeBlog(Long id) {//1.获取当前登录用户Long userId = UserHolder.getUser().getId();String key = RedisConstants.BLOG_LIKED_KEY + id;//2.判断当前登录用户是否已经点赞Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());if(BooleanUtil.isFalse(isMember)) {//3.如果未点赞, 可以点赞//3.1数据库字段liked + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();//3.2将当前用户保存到set集合当中if(isSuccess)stringRedisTemplate.opsForSet().add(key, userId.toString());}else {//4.如果已点赞, 则取消点赞//4.1数据库字段liked - 1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();//4.2将当前用户从set集合当中移除if(isSuccess)stringRedisTemplate.opsForSet().remove(key, userId.toString());}return Result.ok();}
/*** 博客分页数据* @param current* @return*/@Overridepublic 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 -> {this.queryBlogUser(blog);this.isBlogLiked(blog);});return Result.ok(records);}/*** 根据博客id查询博客* @param id* @return*/@Overridepublic Result queryBlogById(Long id) {//1.查询blogBlog blog = getById(id);if(blog == null) return Result.fail("blog不存在!");//2.根据blog查询相关用户queryBlogUser(blog);//3.查询blog是否已被点赞isBlogLiked(blog);return Result.ok(blog);}/*** 设置博客是否被点赞(blog的IsLike属性)* @param blog*/private void isBlogLiked(Blog blog) {//1.获取当前登录用户UserDTO user = UserHolder.getUser();//如果不判空, 对于未登录用户,再去走user.getId()会报空指针异常if(user == null) return;//如果用户没有登录, 则不用走该博客是否被点赞的逻辑Long userId = user.getId();String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();//2.判断当前登录用户是否已经点赞Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(BooleanUtil.isTrue(isMember));}/*** 增加博客关联的用户名及用户头像信息* @param blog*/private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
4. 达人探店-点赞排行榜
在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:
之前的点赞是放到set
集合,但是set
集合是不能排序的,所以这个时候,咱们可以采用一个可以排序的set
集合,就是redis中的另一种数据类型sortedSet
我们接下来来对比一下这些集合的区别是什么
所有点赞的人,需要是唯一的,所以我们应当使用set
或者是sortedSet
其次我们需要排序,就可以直接锁定使用sortedSet
啦
回顾一下zsort
的redis命令语法,zsort
会按照score
这个字段值进行排序,那么我们可以score中存入时间戳,根据时间戳来判断用户先后点赞的顺序。
另外需要注意的是,zset
没有判断成员是否在集合中存在sismember
这个方法,但是我们可以使用zscore
判断集合中成员的score
属性值是否为空,若返回nil
则说明没有这个成员。
修改代码:将之前使用的set
集合数据全部改为zset
类型数据
BlogServiceImpl
点赞逻辑代码:
/*** 设置点赞功能* @param id* @return*/@Overridepublic Result likeBlog(Long id) {//1.获取当前登录用户Long userId = UserHolder.getUser().getId();String key = RedisConstants.BLOG_LIKED_KEY + id;//2.判断当前登录用户是否已经点赞Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if(score == null) {//3.如果未点赞, 可以点赞//3.1数据库字段liked + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();//3.2将当前用户保存到zset集合当中 zadd key score memberif(isSuccess)stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}else {//4.如果已点赞, 则取消点赞//4.1数据库字段liked - 1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();//4.2将当前用户从zset集合当中移除if(isSuccess)stringRedisTemplate.opsForZSet().remove(key, userId.toString());}return Result.ok();}/*** 博客分页数据* @param current* @return*/@Overridepublic 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 -> {this.queryBlogUser(blog);this.isBlogLiked(blog);});return Result.ok(records);}/*** 根据博客id查询博客* @param id* @return*/@Overridepublic Result queryBlogById(Long id) {//1.查询blogBlog blog = getById(id);if(blog == null) return Result.fail("blog不存在!");//2.根据blog查询相关用户queryBlogUser(blog);//3.查询blog是否已被点赞isBlogLiked(blog);return Result.ok(blog);}/*** 设置博客是否被点赞(blog的IsLike属性)* @param blog*/private void isBlogLiked(Blog blog) {//1.获取当前登录用户UserDTO user = UserHolder.getUser();//如果不判空, 对于未登录用户,再去走user.getId()会报空指针异常if(user == null) return;//如果用户没有登录, 则不用走该博客是否被点赞的逻辑Long userId = user.getId();String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();//2.判断当前登录用户是否已经点赞Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score != null);}
点赞排行榜查询列表
BlogController
/*** 点赞排行榜* @param id* @return*/@GetMapping("/likes/{id}")public Result queryBlogLikes(@PathVariable("id") Long id){return blogService.queryBlogLikes(id);}
BlogService
/*** 点赞排行榜* @param id* @return*/@Overridepublic Result queryBlogLikes(Long id) {//1.查询top5的点赞用户 zrange key min maxString key = RedisConstants.BLOG_LIKED_KEY + id;Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if(top5 == null || top5.isEmpty()) return Result.ok(Collections.emptyList());//2.解析出用户idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());//3.根据用户id查询用户 where id in(5, 4) order by field(id, 5, 4)String idStr = StrUtil.join(",", ids);List<UserDTO> userDTOS = userService//.query().in("id", ids).last("order by field(id," + idStr + ")") .list().listByIds(ids).stream().map(user -> {return BeanUtil.copyProperties(user, UserDTO.class);}).collect(Collectors.toList());return Result.ok(userDTOS);}
最后我们重启代码,发现点赞头像出现了顺序混乱问题,
最后我们发现是因为数据库中,系统不会按照in
关键字中值的顺序来进行排序并返回。所以我们需要使用order by filed()
实现自定义排序。
修改java中的代码:
再次刷新,顺序已经被调整过来了。