评论设计开发
1、需求
假如我们开发了一个文库的网站,用户可以对文章进行评论,要实现二级评论的功能。就是只展示两层,第二层包含对第一层的回复和第二层回复的回复....
效果例如bilibili:
2、评论表设计(comment)
字段 | 描述 |
id | 评论记录id,主键 |
doc_id | 被评论的文章id |
user_id | 用户id |
root_comment_id | 根评论id,如果是一级评论,则为0,如果是二级评论,则为一级评论的id |
reply_to_comment_id | 被回复的评论id,如果是一级评论,则为0 |
reply_to_user_id | 被回复的用户id,如果是一级评论,则为null; |
content | 评论的内容 |
count | 一级评论下二级评论数量, |
DROP TABLE IF EXISTS "public"."comment";
CREATE TABLE "public"."comment" ("id" int4 NOT NULL DEFAULT nextval('comment_id_seq'::regclass),"doc_id" int4 NOT NULL,"user_id" varchar(32) COLLATE "pg_catalog"."default","root_comment_id" int4,"reply_to_comment_id" int4,"reply_to_user_id" varchar(32) COLLATE "pg_catalog"."default","content" text COLLATE "pg_catalog"."default","count" int4,"create_time" timestamp(6),"update_time" timestamp(6),"creator" varchar(64) COLLATE "pg_catalog"."default","updater" varchar(64) COLLATE "pg_catalog"."default"
)
;
COMMENT ON COLUMN "public"."comment"."id" IS '主键';
COMMENT ON COLUMN "public"."comment"."doc_id" IS '文档id';
COMMENT ON COLUMN "public"."comment"."user_id" IS '评论用户id';
COMMENT ON COLUMN "public"."comment"."root_comment_id" IS '根评论id,如果是一级评论,则为0,如果是二级评论,则为一级评论的id';
COMMENT ON COLUMN "public"."comment"."reply_to_comment_id" IS '指向被回复的评论id,如果是一级评论,则为0';
COMMENT ON COLUMN "public"."comment"."reply_to_user_id" IS '指向被回复的用户id,如果是一级评论,则为null;这个属于冗余字段,便于查询';
COMMENT ON COLUMN "public"."comment"."content" IS '内容';
COMMENT ON COLUMN "public"."comment"."count" IS '一级评论下二级评论数量,当前记录是一级评论时,此值才有意义,当前记录是二级评论,此值为0';
COMMENT ON COLUMN "public"."comment"."create_time" IS '创建时间';
COMMENT ON COLUMN "public"."comment"."update_time" IS '更新时间';
COMMENT ON COLUMN "public"."comment"."creator" IS '创建人';
COMMENT ON COLUMN "public"."comment"."updater" IS '更新人';
- 性能优先:单次查询解决排序,避免递归开销。
- 简单直接:字段少、索引轻、逻辑清晰,降低开发复杂度。
- 扩展灵活:支持无限嵌套和动态调整,适配评论、消息流等场景。
4、通过sql模拟实现大致功能
批量插入
sql脚本如下:
-- 插入测试数据(假设 video_id=1001)
INSERT INTO comment (id, doc_id, user_id, content, create_time, root_comment_id, reply_to_comment_id, reply_to_user_id
) VALUES
-- 一级评论(根评论)
(1, 1001, 'userA', '文章真棒!', '2025-07-12 10:00:00', 0, 0, NULL),
-- 二级评论:回复 userA
(2, 1001, 'userB', '同意!', '2025-07-12 10:01:00', 1, 1, 'userA',),
-- 三级评论:回复 userB(支持多级扩展)
(3, 1001, 'userC', '在线', '2025-07-12 10:02:00', 1, 2, 'userB'),
-- 二级评论:再回复 userA(跨回复链)
(4, 1001, 'userD', '绝了', '2025-05-12 10:03:00', 1, 1, 'userA'),
-- 一级评论(新根评论)
(5, 1001, 'userE', '好听', '2025-07-12 10:04:00', 0, 0, NULL);
查询一级评论
SELECTid, user_id, content, create_time,reply_to_user_id, thread_path
FROMcomment
WHEREdoc_id = 1001
AND root_comment_id = 0
ORDER BYcreate_time DESC
LIMIT 20 OFFSET 0;
5 userE 好听 2025-07-12 10:04:00 5
1 userA 文章真棒! 2025-07-12 10:00:00 1
在一级评论下查询二级评论
SELECT id, user_id, content, create_time,reply_to_user_id, thread_path
FROM comment
WHERE doc_id = 1001 AND root_comment_id = 1 -- 指定文章
ORDER BYcreate_time DESC,thread_path; -- 同层级内按时间正序
结果:
6 userB 同意! 2025-07-12 12:01:00 userA
3 userC 在线 2025-07-12 10:02:00 userB
2 userB 同意! 2025-07-12 10:01:00 userA
4 userD 绝了 2025-05-12 10:03:00 userA
添加索引优化性能
目前表中没有添加索引,所以上面这个sql查询比较慢,我们可以添加一个组合索引,4个字段:(doc_id, root_comment_id, create_time)
创建索引脚本如下
create index idx_doc_id on comment (doc_id, root_comment_id, create_time,thread_path);
5、发表评论的接口
不管是发表一级评论,还是对一级评论进行回复,还是对二级评论进行回复,还是对回复进行回复,可以共用一个接口。
下面看下这个接口如何设计,代码如下
入参:
@Data
public class AddCommentDTO {private Integer docId;private String userId;private String content;private Long replyToCommentId;
}
实现:
@Override@Transactional(rollbackFor = Exception.class)public Boolean addComment(AddCommentDTO dto) {//处理一级评论if (dto.getReplyToCommentId() == null){Comment comment = new Comment();comment.setContent(dto.getContent());comment.setDocId(dto.getDocId());comment.setUserId(dto.getUserId());comment.setRootCommentId(0L);comment.setReplyToCommentId(dto.getReplyToCommentId());comment.setCount(0);comment.setCreateTime(LocalDateTime.now());return save(comment);}//处理二级评论else {//查询被回复的评论Comment comment = this.getOne(new LambdaQueryWrapper<Comment>().eq(Comment::getId, dto.getReplyToCommentId()));//获取一级评论idLong rootCommentId = comment.getRootCommentId();if (rootCommentId == 0){rootCommentId = comment.getId();}//被回复用户idString replyToUserId = comment.getUserId();Comment replyComment = new Comment();replyComment.setContent(dto.getContent());replyComment.setDocId(dto.getDocId());replyComment.setUserId(dto.getUserId());replyComment.setRootCommentId(rootCommentId);replyComment.setReplyToCommentId(rootCommentId);replyComment.setReplyToUserId(replyToUserId);replyComment.setCount(0);replyComment.setCreateTime(LocalDateTime.now());boolean save = save(replyComment);if (save){//更新根节点countthis.update(new LambdaUpdateWrapper<Comment>().set(Comment::getCount, comment.getCount() + 1).eq(Comment::getId, rootCommentId));}return save;}}
查询:
查询入参
@Data
public class QueryCommentDTO {private Integer pageNo;private Integer pageSize;private Long commentId;private Integer docId;
}
@Overridepublic IPage<Comment> queryComment(QueryCommentDTO dto) {LambdaQueryWrapper<Comment> commentLambdaQueryWrapper = new LambdaQueryWrapper<Comment>().eq(Comment::getRootCommentId, dto.getCommentId()).eq(Comment::getDocId, dto.getDocId()).orderByDesc(Comment::getCreateTime);IPage<Comment> objectPage = new Page<>(dto.getPageNo(), dto.getPageSize());return this.page(objectPage,commentLambdaQueryWrapper);}
新增查询一级评论
新增二级评论
对二级评论回复