目录
- 发布达人探店笔记
- 实现步骤
- 查看探店笔记
- 点赞功能
- 问题分析:
- 功能完善
- 具体实现
- 点赞排行榜
- 实现需求
- 实现步骤
发布达人探店笔记
实现类似于大众点评的发布个人笔记的效果
实现步骤
- 准备数据表如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for tb_blog
-- ----------------------------
DROP TABLE IF EXISTS `tb_blog`;
CREATE TABLE `tb_blog` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`shop_id` bigint(20) NOT NULL COMMENT '商户id',`user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',`images` varchar(2048) NOT NULL COMMENT '探店的照片,最多9张,多张以","隔开',`content` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '探店的文字描述',`liked` int(8) unsigned DEFAULT '0' COMMENT '点赞数量',`comments` int(8) unsigned DEFAULT NULL COMMENT '评论数量',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;-- ----------------------------
-- Table structure for tb_blog_comments
-- ----------------------------
DROP TABLE IF EXISTS `tb_blog_comments`;
CREATE TABLE `tb_blog_comments` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',`blog_id` bigint(20) unsigned NOT NULL COMMENT '探店id',`parent_id` bigint(20) unsigned NOT NULL COMMENT '关联的1级评论id,如果是一级评论,则值为0',`answer_id` bigint(20) unsigned NOT NULL COMMENT '回复的评论id',`content` varchar(255) NOT NULL COMMENT '回复的内容',`liked` int(8) unsigned DEFAULT NULL COMMENT '点赞数',`status` tinyint(1) unsigned DEFAULT NULL COMMENT '状态,0:正常,1:被举报,2:禁止查看',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;SET FOREIGN_KEY_CHECKS = 1;
tb_blog:
探店店笔记表,包含笔记中的标题、文字、图片等
tb_blog_comments:
其他用户对探店笔记的评价
- 对应的实体类:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_blog")
public class Blog implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Long id;/*** 商户id*/private Long shopId;/*** 用户id*/private Long userId;/*** 用户图标*/@TableField(exist = false)private String icon;/*** 用户姓名*/@TableField(exist = false)private String name;/*** 是否点赞过了*/@TableField(exist = false)private Boolean isLike;/*** 标题*/private String title;/*** 探店的照片,最多9张,多张以","隔开*/private String images;/*** 探店的文字描述*/private String content;/*** 点赞数量*/private Integer liked;/*** 评论数量*/private Integer comments;/*** 创建时间*/private LocalDateTime createTime;/*** 更新时间*/private LocalDateTime updateTime;
}
- 发布笔记接口
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn 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);}
}
注意:这里需要修改SystemConstants.IMAGE_UPLOAD_DIR 为自己图片所在的地址,在实际开发中图片一般会放在nginx上或者是云存储上。
查看探店笔记
需求:点击首页的探店笔记,会进入详情页面,我们现在需要实现页面的查询接口
实现代码:
BlogController
@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn 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);}
}
BlogServiceImpl:
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@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(this::queryBlogUser);return Result.ok(records);}@Overridepublic 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());}
}
上述的功能实现是简单的添加和查询的mysql实现,但是功能并不完善,主要是为之后功能加入Redis做准备
点赞功能
需求:点击点赞按钮,查看发送的请求
请求网址: 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字段
具体实现
- Controller层
具体业务逻辑写在Service层
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);
}
- BlogServiceImpl业务实现
在BlogService接口中创建对应方法,在Impl中实现
@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)) {//点赞数 +1boolean success = update().setSql("liked = liked + 1").eq("id", id).update();//将用户加入set集合if (success) {stringRedisTemplate.opsForSet().add(key, userId.toString());}//3. 如果当前用户已点赞,则取消点赞,将用户从set集合中移除}else {//点赞数 -1boolean 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设置为trueblog.setIsLike(BooleanUtil.isTrue(isMember));
}
至此通过简单的Redis的Set数据结构实现了点赞的功能,set的键值为"blogliked:笔记id",值为点赞的用户id。
点赞排行榜
实现需求
当我们点击探店笔记详情页面时,应该按点赞顺序展示点赞用户,比如显示最早点赞的TOP5,形成点赞排行榜,就跟QQ空间发的说说一样,可以看到有哪些人点了赞。
之前的点赞是放到Set集合中,但是Set集合是无序的存储的,所以这个时候,我们就可以改用SortedSet(Zset)
对比一下这些集合的区别:
实现步骤
- 修改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;//尝试获取scoreDouble score = stringRedisTemplate.opsForZSet().score(key, userId.toString());//为null,则表示集合中没有该用户if (score == null) {//点赞数 +1boolean success = update().setSql("liked = liked + 1").eq("id", id).update();//将用户加入set集合if (success) {stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}//3. 如果当前用户已点赞,则取消点赞,将用户从set集合中移除} else {//点赞数 -1boolean 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);
}
需要注意的点是:zrange key 0 4 查询zset中前5个元是我们需要的已经排好序的用户id,但是使用该list拼接sql使用in(,)的时候,返回的结果并不是按照我们期望的方式进行排序,所以我们需要用order by field来指定排序方式,期望的排序方式就是按照查询出来的id进行排序
select * from tb_user where id in (ids[0], ids[1] ...) order by field(id, ids[0], ids[1] ...)