評(píng)論功能有多簡(jiǎn)單,兩張表就行

個(gè)人項(xiàng)目:社交支付項(xiàng)目(小老板)
作者:三哥,https://j3code.cn
項(xiàng)目文檔:https://www.yuque.com/g/j3code/dvnbr5/collaborator/join?token=CFMcFNwMdhpp6u2s&source=book_collaborator#
預(yù)覽地址(未開發(fā)完):http://admire.j3code.cn/small-boss
內(nèi)網(wǎng)穿透部署,第一次訪問比較慢
評(píng)論功能相信是很多論壇、視頻的基礎(chǔ)功能了,而本次我寫的個(gè)人項(xiàng)目也會(huì)涉及到該功能,所以是時(shí)候出個(gè)文章好好聊聊評(píng)論功能了。
1、分析
相信面對(duì)評(píng)論的這個(gè)業(yè)務(wù)大家都不陌生,就好比你看到一條非常有意思的帖子肯定會(huì)忍不住的去稱贊對(duì)方,進(jìn)而去給他留言評(píng)論,就像下面這樣:

上圖就是一個(gè)非常簡(jiǎn)單的一級(jí)評(píng)論,直接針對(duì)帖子內(nèi)容進(jìn)行評(píng)論。
當(dāng)然評(píng)論的目標(biāo)肯定不光只局限于帖子內(nèi)容,還可以針對(duì)別人的評(píng)論(一級(jí))做評(píng)價(jià),也稱為二級(jí)評(píng)論,就像下面這樣:

大伙不會(huì)覺得這就完事了吧,當(dāng)然還有后續(xù)的評(píng)論了,如用戶對(duì)二級(jí)的評(píng)論做評(píng)價(jià),就像這樣:

通過這一套流程下來(lái),我們才是真正的把評(píng)論這個(gè)業(yè)務(wù)走完。那現(xiàn)在我們來(lái)捋一下這上面出現(xiàn)了幾種評(píng)論:
直接評(píng)論帖子的一級(jí)評(píng)論
對(duì)一級(jí)評(píng)論進(jìn)行評(píng)價(jià)的二級(jí)評(píng)論
對(duì)二級(jí)評(píng)論進(jìn)行評(píng)價(jià)的二級(jí)評(píng)論
這里沒有三級(jí)或者說套娃式的分層下去,我是覺得沒必要這樣,就如圖上展示的那樣,二級(jí)評(píng)論的相互評(píng)價(jià)顯示成 XXX 回復(fù) XXX 就一清二楚了。
確定好這些概念之后,咱們?cè)賮?lái)回過頭看看一級(jí)評(píng)論,我們可以抽出那些字段出來(lái):
被評(píng)論的帖子ID(這是帖子ID)
評(píng)論的用戶ID
評(píng)論的內(nèi)容
評(píng)論點(diǎn)贊數(shù)
好,那我們?cè)賮?lái)看看直接評(píng)論一級(jí)評(píng)論的二級(jí)評(píng)論能抽出那些字段出來(lái):
被評(píng)論的父級(jí)評(píng)論ID(這是評(píng)論ID)
評(píng)論的用戶ID
評(píng)論的內(nèi)容
評(píng)論點(diǎn)贊數(shù)
最后,我們?cè)賮?lái)看看評(píng)論二級(jí)評(píng)論的二級(jí)評(píng)論能抽出那些字段出來(lái):
父級(jí)評(píng)論ID(這是評(píng)論ID)
被回復(fù)的評(píng)論ID
評(píng)論的用戶ID
評(píng)論的內(nèi)容
評(píng)論點(diǎn)贊數(shù)
上面我只是大致的抽了一下從圖中就能發(fā)現(xiàn)的字段,現(xiàn)在我們把關(guān)注點(diǎn)放在帖子上,其中是不是有一個(gè)帖子評(píng)論數(shù)量的字段,如果按照上面抽出來(lái)的字段,能否實(shí)現(xiàn)獲取帖子的所有評(píng)論。
我想,你會(huì)現(xiàn)根據(jù)帖子ID,查詢所有一級(jí)評(píng)論,然后再加上二級(jí)評(píng)論中父級(jí)評(píng)論是一級(jí)評(píng)論ID的數(shù)據(jù),這樣就可以得出評(píng)論總數(shù)。
但我覺得這樣麻煩了,我直接給二級(jí)評(píng)論冗余一個(gè)帖子評(píng)論ID不好嗎,這樣直接查一下就出來(lái)了評(píng)論總數(shù)。
所以,我們?cè)诙?jí)評(píng)論中在加一個(gè)字段:
被評(píng)論的帖子ID(這是帖子ID)
ok,到此貌似評(píng)論的字段都已經(jīng)抽的差不多了,而緊接著我們會(huì)面臨一個(gè)問題,就是這些評(píng)論是統(tǒng)一放在一張表中,還是兩張表中。
我給出的答案是,一二級(jí)評(píng)論,拆開存儲(chǔ),用兩張表來(lái)實(shí)現(xiàn)評(píng)論功能。
為啥?
雖然,一二級(jí)評(píng)論的字段只有僅僅的一兩個(gè)只差,但是我覺得評(píng)論數(shù)據(jù)等增長(zhǎng)量還是有一丟丟快的,而且有些業(yè)務(wù)只需要查一級(jí)評(píng)論即可,等進(jìn)入到詳情頁(yè)面的時(shí)候才回去查詢第二級(jí)評(píng)論,這樣能很好的分散表的讀寫壓力。
當(dāng)然,我這個(gè)也不是標(biāo)準(zhǔn)選擇,一起還是從你的系統(tǒng)、業(yè)務(wù)場(chǎng)景觸發(fā)做出現(xiàn)在吧!
我因?yàn)橛幸欢?jí)分開查,評(píng)論數(shù)據(jù)量增長(zhǎng)可能也比較快,所以會(huì)選擇用兩張表來(lái)分開存儲(chǔ),如果你們有不同的意見,歡迎評(píng)論討論。
那確定好用兩張表來(lái)存儲(chǔ)評(píng)論數(shù)據(jù)之后,我們能得出如下 SQL:
sb_post_comment_parent
sb_post_comment_child
1CREATE?TABLE?`sb_post_comment_parent`?(
2????`id`?bigint(20)?NOT?NULL,
3????`item_id`?bigint(20)?NOT?NULL?COMMENT?'條目id',
4????`user_id`?bigint(20)?NOT?NULL?COMMENT?'用戶id',
5????`content`?varchar(1000)?COLLATE?utf8mb4_german2_ci?NOT?NULL?COMMENT?'內(nèi)容',
6????`like_count`?int(4)?DEFAULT?'0'?COMMENT?'點(diǎn)贊數(shù)',
7????`is_publisher`?tinyint(1)?DEFAULT?'0'?COMMENT?'是否為發(fā)布者',
8????`is_delete`?tinyint(1)?DEFAULT?'0'?COMMENT?'是否刪除',
9????`create_time`?datetime?DEFAULT?NULL?COMMENT?'創(chuàng)建時(shí)間',
10????PRIMARY?KEY?(`id`),
11????KEY?`k01`?(`item_id`)
12)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_german2_ci
13
14CREATE?TABLE?`sb_post_comment_child`?(
15????`id`?bigint(20)?NOT?NULL,
16????`item_id`?bigint(20)?NOT?NULL?COMMENT?'條目id',
17????`parent_id`?bigint(20)?NOT?NULL?COMMENT?'父評(píng)論id,也即第一級(jí)評(píng)論',
18????`reply_id`?bigint(20)?DEFAULT?NULL?COMMENT?'被回復(fù)的評(píng)論id(沒有則是回復(fù)父級(jí)評(píng)論,有則是回復(fù)這個(gè)人的評(píng)論)',
19????`user_id`?bigint(20)?NOT?NULL?COMMENT?'評(píng)論人id',
20????`content`?varchar(1000)?COLLATE?utf8mb4_german2_ci?NOT?NULL?COMMENT?'內(nèi)容',
21????`like_count`?int(4)?DEFAULT?'0'?COMMENT?'點(diǎn)贊數(shù)',
22????`is_publisher`?tinyint(1)?DEFAULT?'0'?COMMENT?'是否為發(fā)布者',
23????`is_delete`?tinyint(1)?DEFAULT?'0'?COMMENT?'是否刪除',
24????`create_time`?datetime?DEFAULT?NULL?COMMENT?'創(chuàng)建時(shí)間',
25????PRIMARY?KEY?(`id`),
26????KEY?`k01`?(`parent_id`),
27????KEY?`k02`?(`item_id`)
28)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4?COLLATE=utf8mb4_german2_ci
表結(jié)構(gòu)出來(lái)了,那就要開始編碼實(shí)現(xiàn)了。
2、實(shí)現(xiàn)
2.1 插入評(píng)論
插入評(píng)論的邏輯很簡(jiǎn)單,先根據(jù)參數(shù)區(qū)分出是一級(jí)評(píng)論還是二級(jí)評(píng)論,然后就是組裝參數(shù)保存到數(shù)據(jù)庫(kù)即可。
1)controller
位置:cn.j3code.community.api.v1.controller
1 4j
2
3
4
5 (UrlPrefixConstants.WEB_V1?+?"/post")
6public?class?PostController?{
7????private?final?PostService?postService;
8
9????/**
10?????*?帖子評(píng)論
11?????*
12?????*?@param?request
13?????*/
14???? ("/comment")
15????public?CommentVO?comment(@Validated?@RequestBody?PostCommentRequest?request)?{
16????????return?postService.comment(request);
17????}
18}
PostCommentRequest 對(duì)象
位置:cn.j3code.community.api.v1.request
1
2public?class?PostCommentRequest?{
3????/**
4?????*?帖子id
5?????*/
6???? (message?=?"帖子id不為空")
7????private?Long?postId;
8????/**
9?????*?回復(fù)id,如:a?回復(fù)?b?的評(píng)論,那么回復(fù)id?就是?b?的評(píng)論id
10?????*/
11????private?Long?replyId;
12
13????/**
14?????*?父級(jí)評(píng)論id
15?????*/
16????private?Long?parentId;
17
18????/**
19?????*?內(nèi)容
20?????*/
21???? (message?=?"評(píng)論內(nèi)容不為空")
22????private?String?content;
23}
CommentVO 對(duì)象
位置:cn.j3code.community.api.v1.vo
?1
?2public?class?CommentVO?{
?3
?4???? (shape?=?JsonFormat.Shape.STRING)
?5????private?Long?id;
?6
?7????/**
?8?????*?條目id
?9?????*/
10???? (shape?=?JsonFormat.Shape.STRING)
11????private?Long?itemId;
12
13????/**
14?????*?評(píng)論用戶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?????*?他的父級(jí)評(píng)論id
30?????*/
31???? (shape?=?JsonFormat.Shape.STRING)
32????private?Long?parentId;
33
34????/**
35?????*?被回復(fù)的評(píng)論id(沒有則是回復(fù)父級(jí)評(píng)論,有則是回復(fù)這個(gè)人的評(píng)論)
36?????*/
37???? (shape?=?JsonFormat.Shape.STRING)
38????private?Long?replyId;
39
40????private?ReplyInfo?replyInfo;
41
42????/**
43?????*?內(nèi)容
44?????*/
45????private?String?content;
46
47????/**
48?????*?是否為發(fā)布者
49?????*/
50????private?Boolean?publisher;
51
52????/**
53?????*?點(diǎn)贊數(shù)
54?????*/
55????private?Integer?likeCount;
56
57????/**
58?????*?當(dāng)前登陸者是否點(diǎn)贊:true?已點(diǎn)贊
59?????*/
60????private?Boolean?like;
61
62????/**
63?????*?創(chuàng)建時(shí)間
64?????*/
65????private?LocalDateTime?createTime;
66
67????/**
68?????*?該評(píng)論的所有孩子評(píng)論(分頁(yè))
69?????*/
70????private?IPage<CommentVO>?childCommentPage;
71
72
73????/**
74?????*?這是一個(gè)冗余字段(是否回復(fù)),給前端用的,默認(rèn)?false
75?????*/
76????private?Boolean?reply?=?Boolean.FALSE;
77
78????/**
79?????*?這是一個(gè)冗余字段(回復(fù)內(nèi)容),給前端用的,
80?????*/
81????private?String?replyContent;
82
83????
84????public?static?class?ReplyInfo?{
85????????/**
86?????????*?評(píng)論用戶id
87?????????*/
88????????private?Long?userId;
89
90????????/**
91?????????*?被回復(fù)的內(nèi)容
92?????????*/
93????????private?String?content;
94
95????????/**
96?????????*?頭像
97?????????*/
98????????private?String?avatarUrl;
99
100????????/**
101?????????*?昵稱
102?????????*/
103????????private?String?nickName;
104????}
這個(gè)對(duì)象比較復(fù)雜,主要就是為了向前端展示評(píng)論 + 用戶 + 被回復(fù)評(píng)論信息的一個(gè)復(fù)合對(duì)象。
2)service
位置:cn.j3code.community.service
1public?interface?PostService?extends?IService<Post>?{
2????CommentVO?comment(PostCommentRequest?request);
3}
4 4j
5
6
7public?class?PostServiceImpl?extends?ServiceImpl<PostMapper,?Post>
8????implements?PostService?{
9
10????private?final?CommentConverter?commentConverter;
11????private?final?PostCommentParentService?postCommentParentService;
12????private?final?PostCommentChildService?postCommentChildService;
13
14????
15????public?CommentVO?comment(PostCommentRequest?request)?{
16????????CommentVO?commentVO?=?null;
17????????//?區(qū)分出一級(jí)評(píng)論還是二級(jí)評(píng)論
18????????if?(Objects.isNull(request.getParentId())?&&?Objects.isNull(request.getReplyId()))?{
19????????????//?一級(jí)
20????????????commentVO?=?oneComment(request);
21????????}?else?if?(Objects.nonNull(request.getParentId())?&&?Objects.nonNull(request.getReplyId()))?{
22????????????//?回復(fù)?二級(jí)?評(píng)論的?二級(jí)?評(píng)論
23????????????commentVO?=?twoComment(request);
24????????}?else?if?(Objects.nonNull(request.getParentId())?&&?Objects.isNull(request.getReplyId()))?{
25????????????//?回復(fù)?一級(jí)?的?二級(jí)?評(píng)論
26????????????commentVO?=?twoComment(request);
27????????}?else?{
28????????????throw?new?SysException("評(píng)論參數(shù)出錯(cuò)!");
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 評(píng)論列表
針對(duì)評(píng)論查詢,我們先明確一件事情就是,應(yīng)該針對(duì)業(yè)務(wù)數(shù)據(jù)去查它對(duì)應(yīng)的評(píng)論,也即本篇一直說的帖子。所以,只有傳入帖子 ID,才會(huì)查詢其評(píng)論數(shù)據(jù)。
那現(xiàn)在考慮一下如何出數(shù)據(jù)?
分頁(yè)肯定是跑不了的,而且不僅一級(jí)評(píng)論要進(jìn)行分頁(yè),二級(jí)同樣是如此,就像下面這樣:
一級(jí)

二級(jí)

而點(diǎn)贊記錄我就不在這里提了,上篇已經(jīng)實(shí)現(xiàn)過這個(gè)功能。
現(xiàn)在我們能知道查詢一級(jí)評(píng)論的基本業(yè)務(wù)流程了:
先查分頁(yè)查詢一級(jí)評(píng)論
然后在填充一級(jí)評(píng)論的二級(jí)評(píng)論,注意這也是分頁(yè)
下面看主要邏輯代碼:
1
2public?CommentListVO?commentPage(CommentPageRequest?request)?{
3????CommentListVO?vo?=?new?CommentListVO();
4????//?先分頁(yè)獲取所有一級(jí)評(píng)論
5????vo.setCommentPageData(postCommentParentService.oneCommentPage(request));
6
7????if?(CollectionUtils.isEmpty(vo.getCommentPageData().getRecords()))?{
8????????return?vo;
9????}
10
11????//?用戶評(píng)論點(diǎn)贊狀態(tài)
12????Map<Long,?Boolean>?itemIdToLikeMap?=?likeService.getItemLikeState(vo.getCommentPageData().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()),?CommentTypeEnum.POST_COMMENT);
13????//?redis?中評(píng)論點(diǎn)贊數(shù)量
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????//?填充評(píng)論點(diǎn)贊數(shù)量及當(dāng)前用戶點(diǎn)贊狀態(tài)
18????vo.getCommentPageData().getRecords().forEach(parentComment?->?{
19????????parentComment.setLike(itemIdToLikeMap.get(parentComment.getId()));
20????????parentComment.setLikeCount(parentComment.getLikeCount()?+?itemIdToLikeCountMap.get(parentComment.getId()));
21
22????????//?再分頁(yè)獲取一級(jí)評(píng)論的二級(jí)評(píng)論,二級(jí)評(píng)論默認(rèn)一頁(yè)?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????????????//?用戶評(píng)論點(diǎn)贊狀態(tài)
30????????????Map<Long,?Boolean>?childItemIdToLikeMap?=?likeService.getItemLikeState(parentComment.getChildCommentPage().getRecords().stream().map(CommentVO::getId).collect(Collectors.toList()),?CommentTypeEnum.POST_COMMENT);
31????????????//?redis?中評(píng)論點(diǎn)贊數(shù)量
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????????????//?回填二級(jí)評(píng)論的?回復(fù)信息(用戶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 對(duì)象
1
2public?class?CommentPageRequest?extends?QueryPage?{
3
4????/**
5?????*?評(píng)論條目id
6?????*/
7???? (message?=?"評(píng)論條目id不為空")
8????private?Long?itemId;
9
10????/**
11?????*?該條目之前的數(shù)據(jù),分頁(yè)情況下
12?????*/
13????private?Long?itemIdBefore;
14
15????/**
16?????*?評(píng)論類型
17?????*/
18???? (message?=?"評(píng)論類型不為空")
19????private?CommentTypeEnum?type;
20
21????/**
22?????*?評(píng)論的父級(jí)評(píng)論id
23?????*/
24????private?Long?parentId;
25}
這樣,我們就實(shí)現(xiàn)了一個(gè)一級(jí)評(píng)論的分頁(yè)查詢,并且該列表數(shù)據(jù)也會(huì)順帶的把二級(jí)評(píng)論也查詢出來(lái)。
那緊接著,如果我想要對(duì)查出來(lái)的二級(jí)評(píng)論進(jìn)行分頁(yè)查詢呢?顯然上面的邏輯就不行了,因?yàn)樗遣樵円患?jí)評(píng)論順帶把二級(jí)評(píng)論查出來(lái)一頁(yè)而已。
而我們現(xiàn)在是要對(duì)一級(jí)評(píng)論的二級(jí)評(píng)論進(jìn)行分頁(yè)查詢,所以就要重新寫一個(gè)二級(jí)評(píng)論的分頁(yè)接口了,其主要實(shí)現(xiàn)邏輯如下:
1
2public?IPage<CommentVO>?towCommentPage(CommentPageRequest?request)?{
3????IPage<CommentVO>?voiPage?=?postCommentChildService.twoCommentPage(request);
4
5????//?回填二級(jí)評(píng)論的?回復(fù)信息(用戶id)
6
7????//?獲取被回復(fù)的評(píng)論?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}
至此,我們的評(píng)論功能就完成了,如果對(duì)以上評(píng)論的設(shè)計(jì)與實(shí)現(xiàn)有任何疑問,或者不足的點(diǎn),歡迎評(píng)論區(qū)討論。