个人项目:社交支付项目(小老板)
作者:三哥,j3code.cn
项目文档:www.yuque.com/g/j3code/dv...
预览地址(未开发完):admire.j3code.cn/small-boss
- 内网穿透部署,第一次访问比较慢
评论功能相信是很多论坛、视频的基础功能了,而本次我写的个人项目也会涉及到该功能,所以是时候出个文章好好聊聊评论功能了。
1、分析
相信面对评论的这个业务大家都不陌生,就好比你看到一条非常有意思的帖子肯定会忍不住的去称赞对方,进而去给他留言评论,就像下面这样:
上图就是一个非常简单的一级评论,直接针对帖子内容进行评论。
当然评论的目标肯定不光只局限于帖子内容,还可以针对别人的评论(一级)做评价,也称为二级评论,就像下面这样:
大伙不会觉得这就完事了吧,当然还有后续的评论了,如用户对二级的评论做评价,就像这样:
通过这一套流程下来,我们才是真正的把评论这个业务走完。那现在我们来捋一下这上面出现了几种评论:
- 直接评论帖子的一级评论
- 对一级评论进行评价的二级评论
- 对二级评论进行评价的二级评论
这里没有三级或者说套娃式的分层下去,我是觉得没必要这样,就如图上展示的那样,二级评论的相互评价显示成 XXX 回复 XXX 就一清二楚了。
确定好这些概念之后,咱们再来回过头看看一级评论,我们可以抽出那些字段出来:
- 被评论的帖子ID(这是帖子ID)
- 评论的用户ID
- 评论的内容
- 评论点赞数
好,那我们再来看看直接评论一级评论的二级评论能抽出那些字段出来:
- 被评论的父级评论ID(这是评论ID)
- 评论的用户ID
- 评论的内容
- 评论点赞数
最后,我们再来看看评论二级评论的二级评论能抽出那些字段出来:
- 父级评论ID(这是评论ID)
- 被回复的评论ID
- 评论的用户ID
- 评论的内容
- 评论点赞数
上面我只是大致的抽了一下从图中就能发现的字段,现在我们把关注点放在帖子上,其中是不是有一个帖子评论数量的字段,如果按照上面抽出来的字段,能否实现获取帖子的所有评论。
我想,你会现根据帖子ID,查询所有一级评论,然后再加上二级评论中父级评论是一级评论ID的数据,这样就可以得出评论总数。
但我觉得这样麻烦了,我直接给二级评论冗余一个帖子评论ID不好吗,这样直接查一下就出来了评论总数。
所以,我们在二级评论中在加一个字段:
- 被评论的帖子ID(这是帖子ID)
ok,到此貌似评论的字段都已经抽的差不多了,而紧接着我们会面临一个问题,就是这些评论是统一放在一张表中,还是两张表中。
我给出的答案是,一二级评论,拆开存储,用两张表来实现评论功能。
为啥?
虽然,一二级评论的字段只有仅仅的一两个只差,但是我觉得评论数据等增长量还是有一丢丢快的,而且有些业务只需要查一级评论即可,等进入到详情页面的时候才回去查询第二级评论,这样能很好的分散表的读写压力。
当然,我这个也不是标准选择,一起还是从你的系统、业务场景触发做出现在吧!
我因为有一二级分开查,评论数据量增长可能也比较快,所以会选择用两张表来分开存储,如果你们有不同的意见,欢迎评论讨论。
那确定好用两张表来存储评论数据之后,我们能得出如下 SQL:
- sb_post_comment_parent
- sb_post_comment_child
sql
CREATE TABLE `sb_post_comment_parent` (
`id` bigint(20) NOT NULL,
`item_id` bigint(20) NOT NULL COMMENT '条目id',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`like_count` int(4) DEFAULT '0' COMMENT '点赞数',
`is_publisher` tinyint(1) DEFAULT '0' COMMENT '是否为发布者',
`is_delete` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `k01` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci
CREATE TABLE `sb_post_comment_child` (
`id` bigint(20) NOT NULL,
`item_id` bigint(20) NOT NULL COMMENT '条目id',
`parent_id` bigint(20) NOT NULL COMMENT '父评论id,也即第一级评论',
`reply_id` bigint(20) DEFAULT NULL COMMENT '被回复的评论id(没有则是回复父级评论,有则是回复这个人的评论)',
`user_id` bigint(20) NOT NULL COMMENT '评论人id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`like_count` int(4) DEFAULT '0' COMMENT '点赞数',
`is_publisher` tinyint(1) DEFAULT '0' COMMENT '是否为发布者',
`is_delete` tinyint(1) DEFAULT '0' COMMENT '是否删除',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `k01` (`parent_id`),
KEY `k02` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci
表结构出来了,那就要开始编码实现了。
2、实现
2.1 插入评论
插入评论的逻辑很简单,先根据参数区分出是一级评论还是二级评论,然后就是组装参数保存到数据库即可。
1)controller
位置:cn.j3code.community.api.v1.controller
java
@Slf4j
@ResponseResult
@AllArgsConstructor
@RestController
@RequestMapping(UrlPrefixConstants.WEB_V1 + "/post")
public class PostController {
private final PostService postService;
/**
* 帖子评论
*
* @param request
*/
@PostMapping("/comment")
public CommentVO comment(@Validated @RequestBody PostCommentRequest request) {
return postService.comment(request);
}
}
PostCommentRequest 对象
位置:cn.j3code.community.api.v1.request
java
@Data
public class PostCommentRequest {
/**
* 帖子id
*/
@NotNull(message = "帖子id不为空")
private Long postId;
/**
* 回复id,如:a 回复 b 的评论,那么回复id 就是 b 的评论id
*/
private Long replyId;
/**
* 父级评论id
*/
private Long parentId;
/**
* 内容
*/
@NotNull(message = "评论内容不为空")
private String content;
}
CommentVO 对象
位置:cn.j3code.community.api.v1.vo
java
@Data
public class CommentVO {
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
/**
* 条目id
*/
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long itemId;
/**
* 评论用户id
*/
private Long userId;
/**
* 头像
*/
private String avatarUrl;
/**
* 昵称
*/
private String nickName;
/**
* 他的父级评论id
*/
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long parentId;
/**
* 被回复的评论id(没有则是回复父级评论,有则是回复这个人的评论)
*/
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long replyId;
private ReplyInfo replyInfo;
/**
* 内容
*/
private String content;
/**
* 是否为发布者
*/
private Boolean publisher;
/**
* 点赞数
*/
private Integer likeCount;
/**
* 当前登陆者是否点赞:true 已点赞
*/
private Boolean like;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 该评论的所有孩子评论(分页)
*/
private IPage<CommentVO> childCommentPage;
/**
* 这是一个冗余字段(是否回复),给前端用的,默认 false
*/
private Boolean reply = Boolean.FALSE;
/**
* 这是一个冗余字段(回复内容),给前端用的,
*/
private String replyContent;
@Data
public static class ReplyInfo {
/**
* 评论用户id
*/
private Long userId;
/**
* 被回复的内容
*/
private String content;
/**
* 头像
*/
private String avatarUrl;
/**
* 昵称
*/
private String nickName;
}
这个对象比较复杂,主要就是为了向前端展示评论 + 用户 + 被回复评论信息的一个复合对象。
2)service
位置:cn.j3code.community.service
java
public interface PostService extends IService<Post> {
CommentVO comment(PostCommentRequest request);
}
@Slf4j
@AllArgsConstructor
@Service
public class PostServiceImpl extends ServiceImpl<PostMapper, Post>
implements PostService {
private final CommentConverter commentConverter;
private final PostCommentParentService postCommentParentService;
private final PostCommentChildService postCommentChildService;
@Override
public CommentVO comment(PostCommentRequest request) {
CommentVO commentVO = null;
// 区分出一级评论还是二级评论
if (Objects.isNull(request.getParentId()) && Objects.isNull(request.getReplyId())) {
// 一级
commentVO = oneComment(request);
} else if (Objects.nonNull(request.getParentId()) && Objects.nonNull(request.getReplyId())) {
// 回复 二级 评论的 二级 评论
commentVO = twoComment(request);
} else if (Objects.nonNull(request.getParentId()) && Objects.isNull(request.getReplyId())) {
// 回复 一级 的 二级 评论
commentVO = twoComment(request);
} else {
throw new SysException("评论参数出错!");
}
commentVO.setPublisher(Boolean.TRUE);
commentVO.setLikeCount(0);
commentVO.setCreateTime(LocalDateTime.now());
return commentVO;
}
private CommentVO twoComment(PostCommentRequest request) {
PostCommentChild commentChild = commentConverter.converter(request);
commentChild.setUserId(SecurityUtil.getUserId());
postCommentChildService.save(commentChild);
return commentConverter.converter(commentChild);
}
private CommentVO oneComment(PostCommentRequest request) {
PostCommentParent commentParent = commentConverter.converterToOne(request);
commentParent.setUserId(SecurityUtil.getUserId());
postCommentParentService.save(commentParent);
return commentConverter.converter(commentParent);
}
}
2.2 评论列表
针对评论查询,我们先明确一件事情就是,应该针对业务数据去查它对应的评论,也即本篇一直说的帖子。所以,只有传入帖子 ID,才会查询其评论数据。
那现在考虑一下如何出数据?
分页肯定是跑不了的,而且不仅一级评论要进行分页,二级同样是如此,就像下面这样:
一级
二级
而点赞记录我就不在这里提了,上篇已经实现过这个功能。
现在我们能知道查询一级评论的基本业务流程了:
- 先查分页查询一级评论
- 然后在填充一级评论的二级评论,注意这也是分页
下面看主要逻辑代码:
java
@Override
public CommentListVO commentPage(CommentPageRequest request) {
CommentListVO vo = new CommentListVO();
// 先分页获取所有一级评论
vo.setCommentPageData(postCommentParentService.oneCommentPage(request));
if (CollectionUtils.isEmpty(vo.getCommentPageData().getRecords())) {
return vo;
}
// 用户评论点赞状态
Map<Long, Boolean> itemIdToLikeMap = likeService.getItemLikeState(vo.getCommentPageData().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT);
// redis 中评论点赞数量
Map<Long, Integer> itemIdToLikeCountMap = likeService.getItemLikeCount(vo.getCommentPageData().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT, Boolean.FALSE)
.getItemLikeCount();
// 填充评论点赞数量及当前用户点赞状态
vo.getCommentPageData().getRecords().forEach(parentComment -> {
parentComment.setLike(itemIdToLikeMap.get(parentComment.getId()));
parentComment.setLikeCount(parentComment.getLikeCount() + itemIdToLikeCountMap.get(parentComment.getId()));
// 再分页获取一级评论的二级评论,二级评论默认一页 3 条
request.setParentId(parentComment.getId());
request.setSize(3L);
request.setItemIdBefore(null);
parentComment.setChildCommentPage(postCommentChildService.twoCommentPage(request));
if (CollectionUtils.isNotEmpty(parentComment.getChildCommentPage().getRecords())) {
// 用户评论点赞状态
Map<Long, Boolean> childItemIdToLikeMap = likeService.getItemLikeState(parentComment.getChildCommentPage().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT);
// redis 中评论点赞数量
Map<Long, Integer> childIdToLikeCountMap = likeService.getItemLikeCount(parentComment.getChildCommentPage().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT, Boolean.FALSE)
.getItemLikeCount();
Set<Long> replyIds = new HashSet<>();
parentComment.getChildCommentPage().getRecords().forEach(childComment -> {
childComment.setLike(childItemIdToLikeMap.get(childComment.getId()));
childComment.setLikeCount(childComment.getLikeCount() + childIdToLikeCountMap.get(childComment.getId()));
if (Objects.nonNull(childComment.getReplyId())) {
replyIds.add(childComment.getReplyId());
}
});
// 回填二级评论的 回复信息(用户id)
if (CollectionUtils.isNotEmpty(replyIds)) {
Map<Long, PostCommentChild> replyIdMap = postCommentChildService.lambdaQuery()
.select(PostCommentChild::getId, PostCommentChild::getUserId, PostCommentChild::getContent)
.in(PostCommentChild::getId, replyIds)
.list().stream().distinct().collect(Collectors.toMap(PostCommentChild::getId, item -> item));
parentComment.getChildCommentPage().getRecords().forEach(childComment -> {
if (Objects.nonNull(childComment.getReplyId())) {
CommentVO.ReplyInfo replyInfo = new CommentVO.ReplyInfo();
replyInfo.setUserId(replyIdMap.get(childComment.getReplyId()).getUserId());
replyInfo.setContent(replyIdMap.get(childComment.getReplyId()).getContent());
childComment.setReplyInfo(replyInfo);
}
});
}
}
});
return vo;
}
CommentPageRequest 对象
java
@Data
public class CommentPageRequest extends QueryPage {
/**
* 评论条目id
*/
@NotNull(message = "评论条目id不为空")
private Long itemId;
/**
* 该条目之前的数据,分页情况下
*/
private Long itemIdBefore;
/**
* 评论类型
*/
@NotNull(message = "评论类型不为空")
private CommentTypeEnum type;
/**
* 评论的父级评论id
*/
private Long parentId;
}
这样,我们就实现了一个一级评论的分页查询,并且该列表数据也会顺带的把二级评论也查询出来。
那紧接着,如果我想要对查出来的二级评论进行分页查询呢?显然上面的逻辑就不行了,因为它是查询一级评论顺带把二级评论查出来一页而已。
而我们现在是要对一级评论的二级评论进行分页查询,所以就要重新写一个二级评论的分页接口了,其主要实现逻辑如下:
java
@Override
public IPage<CommentVO> towCommentPage(CommentPageRequest request) {
IPage<CommentVO> voiPage = postCommentChildService.twoCommentPage(request);
// 回填二级评论的 回复信息(用户id)
// 获取被回复的评论 id 集合
Set<Long> replyIds = voiPage.getRecords().stream().map(CommentVO::getReplyId)
.filter(Objects::nonNull).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(replyIds)) {
return voiPage;
}
Map<Long, PostCommentChild> replyIdMap = postCommentChildService.lambdaQuery()
.select(PostCommentChild::getId, PostCommentChild::getUserId, PostCommentChild::getContent)
.in(PostCommentChild::getId, replyIds)
.list().stream().distinct().collect(Collectors.toMap(PostCommentChild::getId, item -> item));
voiPage.getRecords().forEach(childComment -> {
if (Objects.nonNull(childComment.getReplyId())) {
CommentVO.ReplyInfo replyInfo = new CommentVO.ReplyInfo();
replyInfo.setUserId(replyIdMap.get(childComment.getReplyId()).getUserId());
replyInfo.setContent(replyIdMap.get(childComment.getReplyId()).getContent());
childComment.setReplyInfo(replyInfo);
}
});
return voiPage;
}
至此,我们的评论功能就完成了,如果对以上评论的设计与实现有任何疑问,或者不足的点,欢迎评论区讨论。