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

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

作者:三哥,j3code.cn

项目文档:www.yuque.com/g/j3code/dv...

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

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

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

1、分析

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

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

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

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

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

  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
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,才会查询其评论数据。

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

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

一级

二级

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

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

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

下面看主要逻辑代码:

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;
}

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

相关推荐
恬淡虚无真气从之8 分钟前
go interface(接口)使用
开发语言·后端·golang
程序猿毕设源码分享网22 分钟前
基于springboot停车场管理系统源码和论文
数据库·spring boot·后端
Allen Bright24 分钟前
IDEA配置本地maven
java·maven·intellij-idea
总是学不会.25 分钟前
【贪心算法】绿洲之旅:最少次数补给探索
java·算法·intellij-idea
就玩一会_27 分钟前
谷粒商城-消息队列Rabbitmq
java·rabbitmq·java-rabbitmq·谷粒商城
Viktor_Ye29 分钟前
实现金蝶云星空与钉钉数据无缝集成的技术方法
java·大数据·钉钉
程序员学姐37 分钟前
基于SpringBoot+Vue的高校社团管理系统
java·开发语言·vue.js·spring boot·后端·mysql·spring
.生产的驴40 分钟前
Docker Seata分布式事务保护搭建 DB数据源版搭建 结合Nacos服务注册
数据库·分布式·后端·spring cloud·docker·容器·负载均衡
南宫生1 小时前
力扣-位运算-1【算法学习day.41】
java·学习·算法·leetcode
极客先躯1 小时前
高级java每日一道面试题-2024年11月22日-JVM篇-说说堆和栈的区别?
java·jvm··