欢迎光临散文网 会员登陆 & 注册

评论功能有多简单,两张表就行

2023-08-28 08:30 作者:J3code  | 我要投稿

个人项目:社交支付项目(小老板)

作者:三哥,https://j3code.cn

项目文档:https://www.yuque.com/g/j3code/dvnbr5/collaborator/join?token=CFMcFNwMdhpp6u2s&source=book_collaborator#

预览地址(未开发完):http://admire.j3code.cn/small-boss

  • 内网穿透部署,第一次访问比较慢

评论功能相信是很多论坛、视频的基础功能了,而本次我写的个人项目也会涉及到该功能,所以是时候出个文章好好聊聊评论功能了。

1、分析

相信面对评论的这个业务大家都不陌生,就好比你看到一条非常有意思的帖子肯定会忍不住的去称赞对方,进而去给他留言评论,就像下面这样:

Snipaste_2023-08-22_11-29-20.png

上图就是一个非常简单的一级评论,直接针对帖子内容进行评论。

当然评论的目标肯定不光只局限于帖子内容,还可以针对别人的评论(一级)做评价,也称为二级评论,就像下面这样:

Snipaste_2023-08-22_11-34-35.png

大伙不会觉得这就完事了吧,当然还有后续的评论了,如用户对二级的评论做评价,就像这样:

Snipaste_2023-08-22_11-38-16.png

通过这一套流程下来,我们才是真正的把评论这个业务走完。那现在我们来捋一下这上面出现了几种评论:

  1. 直接评论帖子的一级评论

  2. 对一级评论进行评价的二级评论

  3. 对二级评论进行评价的二级评论

这里没有三级或者说套娃式的分层下去,我是觉得没必要这样,就如图上展示的那样,二级评论的相互评价显示成 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

1CREATE TABLE `sb_post_comment_parent` (
2    `id` bigint(20NOT NULL,
3    `item_id` bigint(20NOT NULL COMMENT '条目id',
4    `user_id` bigint(20NOT NULL COMMENT '用户id',
5    `content` varchar(1000COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
6    `like_count` int(4DEFAULT '0' COMMENT '点赞数',
7    `is_publisher` tinyint(1DEFAULT '0' COMMENT '是否为发布者',
8    `is_delete` tinyint(1DEFAULT '0' COMMENT '是否删除',
9    `create_time` datetime DEFAULT NULL COMMENT '创建时间',
10    PRIMARY KEY (`id`),
11    KEY `k01` (`item_id`)
12ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci
13
14CREATE TABLE `sb_post_comment_child` (
15    `id` bigint(20NOT NULL,
16    `item_id` bigint(20NOT NULL COMMENT '条目id',
17    `parent_id` bigint(20NOT NULL COMMENT '父评论id,也即第一级评论',
18    `reply_id` bigint(20DEFAULT NULL COMMENT '被回复的评论id(没有则是回复父级评论,有则是回复这个人的评论)',
19    `user_id` bigint(20NOT NULL COMMENT '评论人id',
20    `content` varchar(1000COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
21    `like_count` int(4DEFAULT '0' COMMENT '点赞数',
22    `is_publisher` tinyint(1DEFAULT '0' COMMENT '是否为发布者',
23    `is_delete` tinyint(1DEFAULT '0' COMMENT '是否删除',
24    `create_time` datetime DEFAULT NULL COMMENT '创建时间',
25    PRIMARY KEY (`id`),
26    KEY `k01` (`parent_id`),
27    KEY `k02` (`item_id`)
28ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

表结构出来了,那就要开始编码实现了。

2、实现

2.1 插入评论

插入评论的逻辑很简单,先根据参数区分出是一级评论还是二级评论,然后就是组装参数保存到数据库即可。

1)controller

位置:cn.j3code.community.api.v1.controller

1@Slf4j
2@ResponseResult
3@AllArgsConstructor
4@RestController
5@RequestMapping(UrlPrefixConstants.WEB_V1 + "/post")
6public class PostController {
7    private final PostService postService;
8
9    /**
10     * 帖子评论
11     *
12     * @param request
13     */

14    @PostMapping("/comment")
15    public CommentVO comment(@Validated @RequestBody PostCommentRequest request) {
16        return postService.comment(request);
17    }
18}

PostCommentRequest 对象

位置:cn.j3code.community.api.v1.request

1@Data
2public class PostCommentRequest {
3    /**
4     * 帖子id
5     */

6    @NotNull(message = "帖子id不为空")
7    private Long postId;
8    /**
9     * 回复id,如:a 回复 b 的评论,那么回复id 就是 b 的评论id
10     */

11    private Long replyId;
12
13    /**
14     * 父级评论id
15     */

16    private Long parentId;
17
18    /**
19     * 内容
20     */

21    @NotNull(message = "评论内容不为空")
22    private String content;
23}

CommentVO 对象

位置:cn.j3code.community.api.v1.vo

 1@Data
 2public class CommentVO {
 3
 4    @JsonFormat(shape = JsonFormat.Shape.STRING)
 5    private Long id;
 6
 7    /**
 8     * 条目id
 9     */

10    @JsonFormat(shape = JsonFormat.Shape.STRING)
11    private Long itemId;
12
13    /**
14     * 评论用户id
15     */

16    private Long userId;
17
18    /**
19     * 头像
20     */

21    private String avatarUrl;
22
23    /**
24     * 昵称
25     */

26    private String nickName;
27
28    /**
29     * 他的父级评论id
30     */

31    @JsonFormat(shape = JsonFormat.Shape.STRING)
32    private Long parentId;
33
34    /**
35     * 被回复的评论id(没有则是回复父级评论,有则是回复这个人的评论)
36     */

37    @JsonFormat(shape = JsonFormat.Shape.STRING)
38    private Long replyId;
39
40    private ReplyInfo replyInfo;
41
42    /**
43     * 内容
44     */

45    private String content;
46
47    /**
48     * 是否为发布者
49     */

50    private Boolean publisher;
51
52    /**
53     * 点赞数
54     */

55    private Integer likeCount;
56
57    /**
58     * 当前登陆者是否点赞:true 已点赞
59     */

60    private Boolean like;
61
62    /**
63     * 创建时间
64     */

65    private LocalDateTime createTime;
66
67    /**
68     * 该评论的所有孩子评论(分页)
69     */

70    private IPage<CommentVO> childCommentPage;
71
72
73    /**
74     * 这是一个冗余字段(是否回复),给前端用的,默认 false
75     */

76    private Boolean reply = Boolean.FALSE;
77
78    /**
79     * 这是一个冗余字段(回复内容),给前端用的,
80     */

81    private String replyContent;
82
83    @Data
84    public static class ReplyInfo {
85        /**
86         * 评论用户id
87         */

88        private Long userId;
89
90        /**
91         * 被回复的内容
92         */

93        private String content;
94
95        /**
96         * 头像
97         */

98        private String avatarUrl;
99
100        /**
101         * 昵称
102         */

103        private String nickName;
104    }

这个对象比较复杂,主要就是为了向前端展示评论 + 用户 + 被回复评论信息的一个复合对象。

2)service

位置:cn.j3code.community.service

1public interface PostService extends IService<Post{
2    CommentVO comment(PostCommentRequest request);
3}
4@Slf4j
5@AllArgsConstructor
6@Service
7public class PostServiceImpl extends ServiceImpl<PostMapperPost>
8    implements PostService 
{
9
10    private final CommentConverter commentConverter;
11    private final PostCommentParentService postCommentParentService;
12    private final PostCommentChildService postCommentChildService;
13
14    @Override
15    public CommentVO comment(PostCommentRequest request) {
16        CommentVO commentVO = null;
17        // 区分出一级评论还是二级评论
18        if (Objects.isNull(request.getParentId()) && Objects.isNull(request.getReplyId())) {
19            // 一级
20            commentVO = oneComment(request);
21        } else if (Objects.nonNull(request.getParentId()) && Objects.nonNull(request.getReplyId())) {
22            // 回复 二级 评论的 二级 评论
23            commentVO = twoComment(request);
24        } else if (Objects.nonNull(request.getParentId()) && Objects.isNull(request.getReplyId())) {
25            // 回复 一级 的 二级 评论
26            commentVO = twoComment(request);
27        } else {
28            throw new SysException("评论参数出错!");
29        }
30
31        commentVO.setPublisher(Boolean.TRUE);
32        commentVO.setLikeCount(0);
33        commentVO.setCreateTime(LocalDateTime.now());
34        return commentVO;
35    }
36
37    private CommentVO twoComment(PostCommentRequest request) {
38        PostCommentChild commentChild = commentConverter.converter(request);
39        commentChild.setUserId(SecurityUtil.getUserId());
40
41        postCommentChildService.save(commentChild);
42
43        return commentConverter.converter(commentChild);
44    }
45
46    private CommentVO oneComment(PostCommentRequest request) {
47        PostCommentParent commentParent = commentConverter.converterToOne(request);
48        commentParent.setUserId(SecurityUtil.getUserId());
49
50        postCommentParentService.save(commentParent);
51
52        return commentConverter.converter(commentParent);
53    }
54}

2.2 评论列表

针对评论查询,我们先明确一件事情就是,应该针对业务数据去查它对应的评论,也即本篇一直说的帖子。所以,只有传入帖子 ID,才会查询其评论数据。

那现在考虑一下如何出数据?

分页肯定是跑不了的,而且不仅一级评论要进行分页,二级同样是如此,就像下面这样:

一级

Snipaste_2023-08-22_17-20-04.png

二级

Snipaste_2023-08-22_17-20-58.png

而点赞记录我就不在这里提了,上篇已经实现过这个功能。

现在我们能知道查询一级评论的基本业务流程了:

  1. 先查分页查询一级评论

  2. 然后在填充一级评论的二级评论,注意这也是分页

下面看主要逻辑代码:

1@Override
2public CommentListVO commentPage(CommentPageRequest request) {
3    CommentListVO vo = new CommentListVO();
4    // 先分页获取所有一级评论
5    vo.setCommentPageData(postCommentParentService.oneCommentPage(request));
6
7    if (CollectionUtils.isEmpty(vo.getCommentPageData().getRecords())) {
8        return vo;
9    }
10
11    // 用户评论点赞状态
12    Map<Long, Boolean> itemIdToLikeMap = likeService.getItemLikeState(vo.getCommentPageData().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT);
13    // redis 中评论点赞数量
14    Map<Long, Integer> itemIdToLikeCountMap = likeService.getItemLikeCount(vo.getCommentPageData().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT, Boolean.FALSE)
15            .getItemLikeCount();
16
17    // 填充评论点赞数量及当前用户点赞状态
18    vo.getCommentPageData().getRecords().forEach(parentComment -> {
19        parentComment.setLike(itemIdToLikeMap.get(parentComment.getId()));
20        parentComment.setLikeCount(parentComment.getLikeCount() + itemIdToLikeCountMap.get(parentComment.getId()));
21
22        // 再分页获取一级评论的二级评论,二级评论默认一页 3 条
23        request.setParentId(parentComment.getId());
24        request.setSize(3L);
25        request.setItemIdBefore(null);
26        parentComment.setChildCommentPage(postCommentChildService.twoCommentPage(request));
27
28        if (CollectionUtils.isNotEmpty(parentComment.getChildCommentPage().getRecords())) {
29            // 用户评论点赞状态
30            Map<Long, Boolean> childItemIdToLikeMap = likeService.getItemLikeState(parentComment.getChildCommentPage().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT);
31            // redis 中评论点赞数量
32            Map<Long, Integer> childIdToLikeCountMap = likeService.getItemLikeCount(parentComment.getChildCommentPage().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()), CommentTypeEnum.POST_COMMENT, Boolean.FALSE)
33                    .getItemLikeCount();
34
35            Set<Long> replyIds = new HashSet<>();
36            parentComment.getChildCommentPage().getRecords().forEach(childComment -> {
37                childComment.setLike(childItemIdToLikeMap.get(childComment.getId()));
38                childComment.setLikeCount(childComment.getLikeCount() + childIdToLikeCountMap.get(childComment.getId()));
39
40                if (Objects.nonNull(childComment.getReplyId())) {
41                    replyIds.add(childComment.getReplyId());
42                }
43            });
44
45            // 回填二级评论的 回复信息(用户id)
46            if (CollectionUtils.isNotEmpty(replyIds)) {
47                Map<Long, PostCommentChild> replyIdMap = postCommentChildService.lambdaQuery()
48                        .select(PostCommentChild::getId, PostCommentChild::getUserId, PostCommentChild::getContent)
49                        .in(PostCommentChild::getId, replyIds)
50                        .list().stream().distinct().collect(Collectors.toMap(PostCommentChild::getId, item -> item));
51
52                parentComment.getChildCommentPage().getRecords().forEach(childComment -> {
53                    if (Objects.nonNull(childComment.getReplyId())) {
54                        CommentVO.ReplyInfo replyInfo = new CommentVO.ReplyInfo();
55                        replyInfo.setUserId(replyIdMap.get(childComment.getReplyId()).getUserId());
56                        replyInfo.setContent(replyIdMap.get(childComment.getReplyId()).getContent());
57                        childComment.setReplyInfo(replyInfo);
58                    }
59                });
60            }
61
62        }
63    });
64
65    return vo;
66}

CommentPageRequest 对象

1@Data
2public class CommentPageRequest extends QueryPage {
3
4    /**
5     * 评论条目id
6     */

7    @NotNull(message = "评论条目id不为空")
8    private Long itemId;
9
10    /**
11     * 该条目之前的数据,分页情况下
12     */

13    private Long itemIdBefore;
14
15    /**
16     * 评论类型
17     */

18    @NotNull(message = "评论类型不为空")
19    private CommentTypeEnum type;
20
21    /**
22     * 评论的父级评论id
23     */

24    private Long parentId;
25}

这样,我们就实现了一个一级评论的分页查询,并且该列表数据也会顺带的把二级评论也查询出来。

那紧接着,如果我想要对查出来的二级评论进行分页查询呢?显然上面的逻辑就不行了,因为它是查询一级评论顺带把二级评论查出来一页而已。

而我们现在是要对一级评论的二级评论进行分页查询,所以就要重新写一个二级评论的分页接口了,其主要实现逻辑如下:

1@Override
2public IPage<CommentVO> towCommentPage(CommentPageRequest request) {
3    IPage<CommentVO> voiPage = postCommentChildService.twoCommentPage(request);
4
5    // 回填二级评论的 回复信息(用户id)
6
7    // 获取被回复的评论 id 集合
8    Set<Long> replyIds = voiPage.getRecords().stream().map(CommentVO::getReplyId)
9        .filter(Objects::nonNull).collect(Collectors.toSet());
10    if (CollectionUtils.isEmpty(replyIds)) {
11        return voiPage;
12    }
13
14    Map<Long, PostCommentChild> replyIdMap = postCommentChildService.lambdaQuery()
15        .select(PostCommentChild::getId, PostCommentChild::getUserId, PostCommentChild::getContent)
16        .in(PostCommentChild::getId, replyIds)
17        .list().stream().distinct().collect(Collectors.toMap(PostCommentChild::getId, item -> item));
18
19    voiPage.getRecords().forEach(childComment -> {
20        if (Objects.nonNull(childComment.getReplyId())) {
21            CommentVO.ReplyInfo replyInfo = new CommentVO.ReplyInfo();
22            replyInfo.setUserId(replyIdMap.get(childComment.getReplyId()).getUserId());
23            replyInfo.setContent(replyIdMap.get(childComment.getReplyId()).getContent());
24            childComment.setReplyInfo(replyInfo);
25        }
26    });
27    return voiPage;
28}

至此,我们的评论功能就完成了,如果对以上评论的设计与实现有任何疑问,或者不足的点,欢迎评论区讨论。


评论功能有多简单,两张表就行的评论 (共 条)

分享到微博请遵守国家法律