文章评论树(后台),快速解决文章评论树

这是我参与 11 月更文挑战的第 4 天,活动详情查看:2021 最后一次更文挑战

文章评论树

文章的评论功能如何实现?应该怎么的展现?

评论表

最近,因为一些原因,在考虑这些问题,也做了一个简易的实现。首先,先设计一张评论表 Comment,它具备如下的基础字段

  • commentId:评论 ID(评论的唯一 ID)
  • articleId:文章 ID(评论所属的文章,最好通过文章 Title)
  • commentName:评论人(发表该条评论的人)
  • commentContent:评论内容(评论的内容)
  • replyId:父级评论 ID(子评论所依赖的父级评论 ID(commentId), 即被回复评论)
  • replyName:父级评论人(父级评论的评论人(commentName),即被回复人)
  • replyComment:父级评论内容(当前评论所回复的目标评论的评论内容)
  • child:二级回复(父级评论下的子级评论列表 (Comment),当前评论下的所有子级评论)

对于可能存在评论人头像、身份、时间等信息,可以选择性加入,具体的操作代码不对这些做操作,无影响

为了方便起见,引入了如下的两个依赖库

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.9</version>
</dependency>
复制代码

实体类

接着,新建实体类 Comment,属性与数据库字段保持一致即可,由于代码较长,无用注释已删除

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Comment {

    private String commentId;

    private String articleId;

    private String commentName;

    private String commentContent;

    private String replyId;

    private String replyName;

    private String replyComment;

    private List<Comment> child;

}
复制代码

转换实现

最后,就是具体的代码操作。值得一说的是,为了清晰和便捷,我将数据库 SQL 提取出来,模拟插入

另外,replyNamereplyCommentchild 在数据库中可以置空(强烈建议),这些字段数据可以根据 replyId 拿到

import com.google.gson.Gson;

import java.util.*;

public class Test {

    public static void main(String[] args) {

        /* 模拟数据 */
        Comment comment1 = new Comment("1", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "优弧", "你们掘金的活动真的太多了,太讨厌了。。。", null, null, null, null);
        Comment comment2 = new Comment("2", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "王中阳 Go", "这么说算自言自语么~", "1", null, null, null);
        Comment comment3 = new Comment("3", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "西瓜 waterm", "这是在凡尔赛吗", "1", null, null, null);
        Comment comment4 = new Comment("4", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "荣顶", "这么说算自言自语么~", "1", null, null, null);
        Comment comment5 = new Comment("5", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "阿 Q 说代码", "哈哈 来者不惧", "1", null, null, null);
        Comment comment6 = new Comment("6", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "用户 356807", "范德萨发撒的发", "3", null, null, null);
        Comment comment7 = new Comment("7", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "三掌柜", "我要参与 2021 最后一次更文挑战", null, null, null, null);
        Comment comment8 = new Comment("8", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "三掌柜", "11 月更文挑战第一天:juejin.cn", "7", null, null, null);
        Comment comment9 = new Comment("9", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "三掌柜", "11 月更文挑战第二天:juejin.cn", "7", null, null, null);
        Comment comment10 = new Comment("10", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "三掌柜", "11 月更文挑战第三天:juejin.cn", "7", null, null, null);
        Comment comment11 = new Comment("11", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "xbhog", "已经没有东西了可以榨干了", null, null, null, null);
        Comment comment12 = new Comment("12", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "StartFromThird", "代码文字比不得超过 70% 是指 代码字数 / (文章总字数: 即 代码字数 + 其他内容字数 ) ≤ 70 / 100 吗?", null, null, null, null);
        Comment comment13 = new Comment("13", "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来", "六脉神剑", "去年的时候 根本没几个活动 今年真的活动多", null, null, null, null);

        /* 当前文章下的所有评论,以 HashMap 形式存储,键为评论 ID(getCommentId) */
        HashMap<String, Comment> commentMap = new HashMap<>();
        commentMap.put(comment1.getCommentId(), comment1);
        commentMap.put(comment2.getCommentId(), comment2);
        commentMap.put(comment3.getCommentId(), comment3);
        commentMap.put(comment4.getCommentId(), comment4);
        commentMap.put(comment5.getCommentId(), comment5);
        commentMap.put(comment6.getCommentId(), comment6);
        commentMap.put(comment7.getCommentId(), comment7);
        commentMap.put(comment8.getCommentId(), comment8);
        commentMap.put(comment9.getCommentId(), comment9);
        commentMap.put(comment10.getCommentId(), comment10);
        commentMap.put(comment11.getCommentId(), comment11);
        commentMap.put(comment12.getCommentId(), comment12);
        commentMap.put(comment13.getCommentId(), comment13);

        /* Map key 排序(升序) */
        //HashMap<String, Comment> sortMapByKeyAscend = sortMapByKeyAscend(commentMap);

        /* 获取级联形式的评论列表 */
        List<Comment> commentList = new ArrayList<>();
        /* 遍历所有评论 */
        for (String commendId : commentMap.keySet()) {
            /* 获取评论对象 */
            Comment commentEntity = commentMap.get(commendId);
            /* 评论对象的父级评论是否为空(replyId),为空则作为顶级的评论存在 */
            if (commentEntity.getReplyId() == null) {
                /* 获取当前顶级评论下的,所有二级评论 */
                List<Comment> child = getChild(commendId, commentMap);
                /* 二级评论填入当前顶级评论的 child 字段 */
                commentEntity.setChild(child);
                /* 已整合的顶级评论,填入文章评论列表中 */
                commentList.add(commentEntity);
            }
        }

        /* List 升序排序 */
        commentList.sort(Comparator.comparing(Comment::getCommentId));

        System.out.println(commentList);
        Gson gson = new Gson();
        /* 将得到的级联形式的评论列表转换为 JSON 对象 */
        System.out.println(gson.toJson(commentList));
    }

    /**
     * 获取当前顶级评论下的,二级子评论
     *
     * @param id:当前顶级评论的评论 ID
     * @param commentMap:文章评论,所有
     */
    public static List<Comment> getChild(String id, HashMap<String, Comment> commentMap) {
        /* 二级子评论列表 */
        List<Comment> commentList = new ArrayList<>();
        /* 当前,已经记录的二级子评论的评论 ID */
        List<String> commentIdList = new ArrayList<>();
        Gson gson = new Gson();
        /* 遍历每一条评论 */
        for (String commendId : commentMap.keySet()) {
            /* 获取评论的实体对象 */
            Comment commentEntity = commentMap.get(commendId);
            /* 获取二级子评论的唯一父评论实体对象 */
            Comment commentTop = gson.fromJson(gson.toJson(commentMap.get(commentEntity.getReplyId())), Comment.class);
            /* 当前遍历的评论,其父级评论 ID(replyId)是否与当前的父评论 ID(commentId)一致 */
            if (Objects.equals(commentEntity.getReplyId(), id)) {
                /* 记录当前二级评论的被回复人,可以是父评论人,也可以是同为二级的回复人 */
                commentEntity.setReplyName(commentTop.getCommentName());
                commentEntity.setReplyComment(commentTop.getCommentContent());
                /* 加入二级列表 */
                commentList.add(commentEntity);
                /* 加入已记录评论的评论 ID */
                commentIdList.add(commentEntity.getCommentId());
                /* 之后遍历的评论,其父级评论 ID 是否已经被收录 (commentIdList) */
            } else if (commentIdList.contains(commentEntity.getReplyId())) {
                commentEntity.setReplyName(commentTop.getCommentName());
                commentEntity.setReplyComment(commentTop.getCommentContent());
                commentList.add(commentEntity);
                /* 必须存在!!! 1:14 */
                commentIdList.add(commentEntity.getCommentId());
            }
        }
        // System.out.println(commentIdList);
        return commentList;
    }

    public static <K extends Comparable<? super K>, V> HashMap<K, V> sortMapByKeyAscend(HashMap<K, V> map) {
        ArrayList<Map.Entry<K, V>> entries = new ArrayList<>(map.entrySet());
        entries.sort(Map.Entry.comparingByKey());
        HashMap<K, V> sortedMap = new LinkedHashMap<>();
        for (Map.Entry<K, V> entry : entries) {
            sortedMap.put(entry.getKey(), entry.getValue());
        }
        return sortedMap;
    }
}
复制代码

上述的代码,粘贴即可运行,实际转换的是一个 JSON 树,转换效果如下,数据来源网址

[
  {
    "commentId": "1",
    "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
    "commentName": "优弧",
    "commentContent": "你们掘金的活动真的太多了,太讨厌了。。。",
    "child": [
      {
        "commentId": "2",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "王中阳 Go",
        "commentContent": "这么说算自言自语么~",
        "replyId": "1",
        "replyName": "优弧",
        "replyComment": "你们掘金的活动真的太多了,太讨厌了。。。"
      },
      {
        "commentId": "3",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "西瓜 waterm",
        "commentContent": "这是在凡尔赛吗",
        "replyId": "1",
        "replyName": "优弧",
        "replyComment": "你们掘金的活动真的太多了,太讨厌了。。。"
      },
      {
        "commentId": "4",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "荣顶",
        "commentContent": "这么说算自言自语么~",
        "replyId": "1",
        "replyName": "优弧",
        "replyComment": "你们掘金的活动真的太多了,太讨厌了。。。"
      },
      {
        "commentId": "5",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "阿 Q 说代码",
        "commentContent": "哈哈 来者不惧",
        "replyId": "1",
        "replyName": "优弧",
        "replyComment": "你们掘金的活动真的太多了,太讨厌了。。。"
      },
      {
        "commentId": "6",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "用户 356807",
        "commentContent": "范德萨发撒的发",
        "replyId": "3",
        "replyName": "西瓜 waterm",
        "replyComment": "这是在凡尔赛吗"
      }
    ]
  },
  {
    "commentId": "11",
    "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
    "commentName": "xbhog",
    "commentContent": "已经没有东西了可以榨干了",
    "child": []
  },
  {
    "commentId": "12",
    "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
    "commentName": "StartFromThird",
    "commentContent": "代码文字比不得超过 70% 是指 代码字数 / (文章总字数: 即 代码字数 + 其他内容字数 ) ≤ 70 / 100 吗?",
    "child": []
  },
  {
    "commentId": "13",
    "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
    "commentName": "六脉神剑",
    "commentContent": "去年的时候 根本没几个活动 今年真的活动多",
    "child": []
  },
  {
    "commentId": "7",
    "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
    "commentName": "三掌柜",
    "commentContent": "我要参与 2021 最后一次更文挑战",
    "child": [
      {
        "commentId": "8",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "三掌柜",
        "commentContent": "11 月更文挑战第一天:juejin.cn",
        "replyId": "7",
        "replyName": "三掌柜",
        "replyComment": "我要参与 2021 最后一次更文挑战"
      },
      {
        "commentId": "9",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "三掌柜",
        "commentContent": "11 月更文挑战第二天:juejin.cn",
        "replyId": "7",
        "replyName": "三掌柜",
        "replyComment": "我要参与 2021 最后一次更文挑战"
      },
      {
        "commentId": "10",
        "articleId": "2021 最后一次更文挑战,玩法升级奖品多,全新体验等你来",
        "commentName": "三掌柜",
        "commentContent": "11 月更文挑战第三天:juejin.cn",
        "replyId": "7",
        "replyName": "三掌柜",
        "replyComment": "我要参与 2021 最后一次更文挑战"
      }
    ]
  }
]
复制代码

具体的转换代码,注释很详细,需要的自取。今天周五,月入一千八,开机混底薪!YES!