• 博客的评论与回复功能的实现


    你好呀,我是小邹。

    在之前的文章中,提到了个人博客的简单回复功能的实现,今天记录一下完整的评论功能的实现。

    实现思路

    数据库设计:评论表需要定义出当前博客id以便做关联,因为评论需要有回复功能,则需要定义当前评论有无上一级评论,需要定义出上级评论id。

    代码方面:点击评论需要获取当前博客id与自己评论数据进行插入,点击回复按钮需要获取上一条评论的id以及用户姓名作为回复,回复成功后,后台在数据库中查找出所有parentCommentId为-1的值进行遍历,因为上级id为-1则证明当前评论无父节点。在通过对父节点id的遍历查询出所有对应评论的子节点。

    页面效果

    实现的关键在于:新提交的评论排在最上面,三级评论排在二级评论的下面。
    在这里插入图片描述

    代码实现

    实体类

    package com.zou.blog.model.domain;
    
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    /**
     * @author: 邹祥发
     * @date: 2022/7/12 08:01
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @TableName("article_comments")
    public class Comments {
        @TableId
        private Integer id;
        private String nickname;
        private String email;
        private String content;
        private Date createTime;
        private Integer blogId;
        private Integer isVisible;
        private String avatar;
        private String blogUrl;
        private String province;
        private String ip;
        private Date updateTime;
        private Integer sort;
        //评论的父节点id
        private Integer parentId;
        private String parentName;
        //回复评论
        @TableField(exist = false)
        private List<Comments> replyComments = new ArrayList<>();
        @TableField(exist = false)
        private Comments parentComment;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    评论表单页面

    <form target="myiframe" style="padding-top: 20px">
        <input type="hidden" name="blogid" value="1"/>
        <input type="hidden" name="blogUrl" value="about"/>
        <input type="hidden" name="parentCommentId" value="-1">
        <div id="comment-form" class="ui form">
            <div class="field">
                <textarea id="aaa" name="content" placeholder="欢迎高质量的留言和交流,低俗和无意义的留言不会过审" 		                       required="required">
                textarea>
            div>
            <div class="fields">
                <div class="field m-mobile-wide m-margin-bottom-small">
                    <div class="ui left icon input">
                        <img id="avatar" src="https://q1.qlogo.cn/g?b=qq&nk=1565453341&s=100"
                             class="ui mini circular image" style="margin-top: 11px">
                    div>
                div>
                <div class="field m-mobile-wide m-margin-bottom-small" style="padding-top: 10px">
                    <div class="ui left icon input">
                        <i class="qq icon">i>
                        <input type="text" id="QQ" name="qq" placeholder="输入QQ号自动获取昵称头像"
                               required="required"/>
                    div>
                div>
                <div class="field m-mobile-wide m-margin-bottom-small" style="padding-top: 10px">
                    <div class="ui left icon input">
                        <i class="user icon">i>
                        <input type="text" id="nickname" name="nickname" placeholder="昵称"
                               required="required"/>
                    div>
                div>
                <div class="field m-mobile-wide m-margin-bottom-small" style="padding-top: 10px">
                    <div class="ui left icon input">
                        <input type="text" id="ccc" name="email" placeholder="邮箱"
                               hidden="hidden" required="required">
                    div>
                div>
                <div class="field m-margin-bottom-small m-mobile-wide" style="padding-top: 10px">
                     <button id="comment-btn" type="submit" class="ui violet button m-mobile-wide "><i
                             class="edit icon">i>发布
                     button>
                div>
            div>
        div>
    form>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    点击发布

    js有些多余的代码,会前端的可以自己删。

    <script type="application/javascript">
        <!--根据QQ自动获取头像信息-->
        $('#QQ').blur(function () {
            var QQ = $("#QQ").val();
            $.ajax({
                url: "https://api.usuuu.com/qq/" + QQ,
                type: "GET",
                dataType: "json",
                success: function (result) {
                    console.log(result["data"].name, result["data"].avatar);
                    $("#nickname").val(result["data"].name);
                    var obj = document.getElementById("avatar");
                    obj.src = result["data"].avatar;
                    $("#avatar").val(result["data"].avatar);
                    $("[name='email']").val(QQ + '@qq.com');
                }
            });
        });
    
        $(function () {
            $('#comment-btn').click(function () {
                var blogid = $("input[name='blogid']").val().trim();
                var blogUrl = $("input[name='blogUrl']").val().trim();
                var content = $("textarea[name='content']").val().trim();
                var nickname = $("input[name='nickname']").val().trim();
                var email = $("input[name='email']").val().trim();
                var avatar = $("#avatar").val();
                var parentId = $("input[name='parentCommentId']").val();
                if (reply1 != null) {
                    var parentName = reply1;
                } else {
                    parentName = nickname;
                }
                var data = {
                    blogid: blogid,
                    blogUrl: blogUrl,
                    content: content,
                    nickname: nickname,
                    email: email,
                    avatar: avatar,
                    parentId: parentId,
                    parentName: parentName
                };
                if (content !== "" && content !== null && content !== undefined && nickname !== "" && nickname !== null && nickname !== undefined) {
                    //验证邮箱格式
                    const emailReg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
                    if (!emailReg.test(email)) {
                        alert('邮箱格式错误');
                        return;
                    }
                    $.ajax({
                        type: "POST",
                        url: '/comments',
                        data: data,
                        dataType: 'json',
                        contentType: 'application/x-www-form-urlencoded',
                        success: function (req) {
                            console.log(req)
                        },
                        error: function (e) {
                            console.log(e)
                        }
                    })
                    alert('您的评论已成功投递至召田最帅boy,请耐心等待他审核吧!');
                    $('#aaa').val('');
                } else {
                    alert("昵称和评论内容不能为空!")
                    return;
                }
            })
        })
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    点击回复按钮,在评论区显示回复给哪个用户

    在这里插入图片描述

    <a class="reply" data-commentid="1" data-commentnickname="zou" th:attr="data-commentid=${reply.id}, data-commentnickname=${reply.nickname}" onclick="reply(this)">回复a>
    
    • 1

    对应函数

    	//回复
        var reply1;
    
        function reply(obj) {
            let commentId = $(obj).data('commentid');
            let commentNickname = $(obj).data('commentnickname');
            reply1 = commentNickname;
            //添加信息到评论表单
            $("[name='content']").attr("placeholder", "@" + commentNickname).focus();
            $("[name='parentCommentId']").val(commentId);
            //滚动到评论表单
            $(window).scrollTo($('#comment-form'), 500);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    后端controller层代码

    	/**
         * 发表评论
         */
        @ResponseBody
        @PostMapping(value = {"comments"})
        public void comments(HttpServletRequest request, @RequestBody @RequestParam("blogid") Integer blogId,
                             @RequestBody @RequestParam("blogUrl") String blogUrl,
                             @RequestBody @RequestParam("content") String content,
                             @RequestBody @RequestParam("nickname") String nickname,
                             @RequestBody @RequestParam("email") String email,
                             @RequestBody @RequestParam("avatar") String avatar,
                             @RequestBody @RequestParam("parentId") Integer parentId,
                             @RequestBody @RequestParam("parentName") String parentName) throws Exception {
            String ip = IpUtils.getIpAddr(request);
            String province = IpUtils.getIpPossession(ip);
            Comments comments = new Comments();
            comments.setId(Integer.parseInt(String.valueOf(System.currentTimeMillis() / 1000)));
            comments.setContent(content);
            comments.setEmail(email);
            comments.setCreateTime(new Date());
            comments.setBlogId(blogId);
            comments.setBlogUrl(blogUrl);
            comments.setProvince(province);
            comments.setIp(ip);
            comments.setUpdateTime(new Date());
            //数值越大则优先展示
            if (parentId == -1) {
                comments.setSort(1);
            } else {
                comments.setSort(Integer.parseInt(String.valueOf(System.currentTimeMillis() / 990)));
            }
            //未审核的评论默认不可见
            //暂时可见
            comments.setIsVisible(CommentStatus.VISIBLE.getStatus());
            //设置父节点id,-1为首节点
            comments.setParentId(parentId);
            comments.setParentName(parentName);
            comments.setNickname(nickname);
            comments.setAvatar(avatar);
            commentService.save(comments);
        }
        
        //根据文章id查询评论列表
        model.addAttribute("comments", commentService.listCommentByBlogId(article.getId()));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    评论列表-展示层级关系

    <div class="comment" th:each="comment : ${comments}">
        <a class="avatar">
            <img th:src="@{${comment.avatar}}">
        a>
    	<div class="content">
             <a class="author">
                <span th:text="${comment.nickname}">span>
             a>
             <div class="metadata">
                  <span class="date" th:text="${#dates.format(comment.createTime, 'yyyy-MM-dd HH:mm')}">					  span>
             div> 
             <span th:text="'来自'+${#strings.substring(comment.province,0,2)}"
                   style="color: darkgray">span>
             <div class="text" th:text="${comment.content}">div>
             <div class="actions">
                  <a class="reply" data-commentid="1" data-commentnickname="zou" th:attr="data-commentid=${comment.id}, data-commentnickname=${comment.nickname}" onclick="reply(this)">回复a>
            div>
        div>
        <div class="comments" th:if="${#arrays.length(comment.replyComments)} gt 0">
             <div class="comment" th:each="reply : ${comment.replyComments}">
                  <a class="avatar">
                     <img th:src="@{${reply.avatar}}">
                  a>
                  <div class="content">
                       <a class="author">
                          <span th:text="${reply.nickname}">span> 
                       a>
                       <span th:text="|@ ${reply.parentName}|" class="m-grey">span>
                       <div class="metadata">
                       <span class="date" th:text="${#dates.format(reply.createTime, 'yyyy-MM-dd HH:mm')}">						span>
                 div> 
                 <span th:text="'来自'+${#strings.substring(reply.province,0,2)}"
                       style="color: darkgray">span>
                 <div class="text" th:text="${reply.content}">div>
                      <div class="actions">
                           <a class="reply" data-commentid="1" data-commentnickname="zou"
                              th:attr="data-commentid=${reply.id}, data-commentnickname=${reply.nickname}"
                                  onclick="reply(this)">回复a>
                      div>
                 div>
            div>
        div>
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    后端service层代码

    先获取顶级的数据,在一层一层往下找、放入集合

    	@Override
        public List<Comments> listCommentByBlogId(Integer blogId) {
            QueryWrapper<Comments> wrapper = new QueryWrapper<Comments>().eq("blog_id", blogId).eq("is_visible", CommentStatus.VISIBLE.getStatus()).orderByAsc("sort").orderByDesc("create_time");
            wrapper.select("id", "nickname", "content", "create_time", "avatar", "parent_id", "province", "blog_id", "parent_name");
            List<Comments> comments = commentsMapper.selectList(wrapper);
            return firstComment(comments);
        }
    
        public List<Comments> firstComment(List<Comments> comments) {
            //存储父评论为根评论-1的评论
            ArrayList<Comments> list = new ArrayList<>();
            for (Comments comment : comments) {
                //其父id等于-1则为第一级别的评论
                if (comment.getParentId() == -1) {
                    //我们将该评论下的所有评论都查出来
                    comment.setReplyComments(findReply(comments, comment.getId()));
                    //这就是我们最终数组中的Comment
                    list.add(comment);
                }
            }
            return list;
        }
    
        /**
         * @param comments 我们所有的该博客下的评论
         * @param targetId 我们要查到的目标父id
         * @return 返回该评论下的所有评论
         */
        public List<Comments> findReply(List<Comments> comments, int targetId) {
            //第一级别评论的子评论集合
            ArrayList<Comments> reply = new ArrayList<>();
            for (Comments comment : comments) {
                //发现该评论的父id为targetId就将这个评论加入子评论集合
                if (find(comment.getParentId(), targetId)) {
                    reply.add(comment);
                }
            }
            return reply;
        }
    
        public boolean find(int id, int target) {
            //不将第一节评论本身加入自身的子评论集合
            if (id == -1) {
                return false;
            }
            //如果父id等于target,那么该评论就是id为target评论的子评论
            if (id == target) {
                return true;
            } else {
                //否则就再向上找
                return find(commentsMapper.selectById(id).getParentId(), target);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    总结

    本文较全面地介绍了博客评论以及回复功能的实现。实现逻辑:因为是个人博客普通用户不需要登录即可浏览,所以没有做普通用户登录功能,评论时只需输入自己的QQ号,自动拉取头像和昵称进行评论。

    相关链接:评论如何获取IP地址?

  • 相关阅读:
    【最优化】牛顿法、高斯-牛顿法
    检测图像的圆形 检测直线 Hough变换检测直线 圆形检测 圆心半径检测 -matlab
    什么是Redission可重入锁,其实现原理是什么?
    CSS3-2D缩放
    防火墙(Firewall)
    uniapp写支付的操作
    Docker-harbor私有仓库部署与管理
    Python中`*args`和`**kwargs`的用法
    【cmake实战八】cmake 常用变量
    基于SpringBoot的网上订餐系统
  • 原文地址:https://blog.csdn.net/Zou_05/article/details/126496224