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

  Java   35分钟   806浏览   4评论

你好呀,我是小邹。

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

实现思路

数据库设计:评论表需要定义出当前博客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;
}

评论表单页面

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

点击发布

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>

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

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

对应函数

    //回复
    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);
    }

后端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()));

评论列表-展示层级关系

<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>&nbsp;
         <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>&nbsp;
                   </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>&nbsp;
             <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>

后端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);
        }
    }

总结

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

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

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
😀
😃
😄
😁
😆
😅
🤣
😂
🙂
🙃
😉
😊
😇
🥰
😍
🤩
😘
😗
☺️
😚
😙
🥲
😋
😛
😜
🤪
😝
🤑
🤗
🤭
🫢
🫣
🤫
🤔
🤨
😐
😑
😶
😏
😒
🙄
😬
😮‍💨
🤤
😪
😴
😷
🤒
🤕
🤢
🤮
🤧
🥵
🥶
🥴
😵
😵‍💫
🤯
🥳
🥺
😠
😡
🤬
🤯
😈
👿
💀
☠️
💩
👻
👽
👾
🤖
😺
😸
😹
😻
😼
😽
🙀
😿
😾
👋
🤚
🖐️
✋️
🖖
🫱
🫲
🫳
🫴
🫷
🫸
👌
🤌
🤏
✌️
🤞
🫰
🤟
🤘
🤙
👈️
👉️
👆️
🖕
👇️
☝️
🫵
👍️
👎️
✊️
👊
🤛
🤜
👏
🙌
👐
🤲
🤝
🙏
✍️
💅
🤳
💪
🦾
🦿
🦵
🦶
👂
🦻
👃
👶
👧
🧒
👦
👩
🧑
👨
👩‍🦱
👨‍🦱
👩‍🦰
👨‍🦰
👱‍♀️
👱‍♂️
👩‍🦳
👨‍🦳
👩‍🦲
👨‍🦲
🧔‍♀️
🧔‍♂️
👵
🧓
👴
👲
👳‍♀️
👳‍♂️
🧕
👮‍♀️
👮‍♂️
👷‍♀️
👷‍♂️
💂‍♀️
💂‍♂️
🕵️‍♀️
🕵️‍♂️
👩‍⚕️
👨‍⚕️
👩‍🌾
👨‍🌾
👩‍🍳
👨‍🍳
🐶
🐱
🐭
🐹
🐰
🦊
🐻
🐼
🐨
🐯
🦁
🐮
🐷
🐸
🐵
🐔
🐧
🐦
🦅
🦉
🐴
🦄
🐝
🪲
🐞
🦋
🐢
🐍
🦖
🦕
🐬
🦭
🐳
🐋
🦈
🐙
🦑
🦀
🦞
🦐
🐚
🐌
🦋
🐛
🦟
🪰
🪱
🦗
🕷️
🕸️
🦂
🐢
🐍
🦎
🦖
🦕
🐊
🐢
🐉
🦕
🦖
🐘
🦏
🦛
🐪
🐫
🦒
🦘
🦬
🐃
🐂
🐄
🐎
🐖
🐏
🐑
🐐
🦌
🐕
🐩
🦮
🐕‍🦺
🐈
🐈‍⬛
🐓
🦃
🦚
🦜
🦢
🦩
🕊️
🐇
🦝
🦨
🦡
🦫
🦦
🦥
🐁
🐀
🐿️
🦔
🌵
🎄
🌲
🌳
🌴
🌱
🌿
☘️
🍀
🎍
🎋
🍃
🍂
🍁
🍄
🌾
💐
🌷
🌹
🥀
🌺
🌸
🌼
🌻
🌞
🌝
🌛
🌜
🌚
🌕
🌖
🌗
🌘
🌑
🌒
🌓
🌔
🌙
🌎
🌍
🌏
🪐
💫
🌟
🔥
💥
☄️
☀️
🌤️
🌥️
🌦️
🌧️
⛈️
🌩️
🌨️
❄️
☃️
🌬️
💨
💧
💦
🌊
🍇
🍈
🍉
🍊
🍋
🍌
🍍
🥭
🍎
🍏
🍐
🍑
🍒
🍓
🥝
🍅
🥥
🥑
🍆
🥔
🥕
🌽
🌶️
🥒
🥬
🥦
🧄
🧅
🍄
🥜
🍞
🥐
🥖
🥨
🥯
🥞
🧇
🧀
🍖
🍗
🥩
🥓
🍔
🍟
🍕
🌭
🥪
🌮
🌯
🥙
🧆
🥚
🍳
🥘
🍲
🥣
🥗
🍿
🧈
🧂
🥫
🍱
🍘
🍙
🍚
🍛
🍜
🍝
🍠
🍢
🍣
🍤
🍥
🥮
🍡
🥟
🥠
🥡
🦪
🍦
🍧
🍨
🍩
🍪
🎂
🍰
🧁
🥧
🍫
🍬
🍭
🍮
🍯
🍼
🥛
🍵
🍶
🍾
🍷
🍸
🍹
🍺
🍻
🥂
🥃
🥤
🧃
🧉
🧊
🗺️
🏔️
⛰️
🌋
🏕️
🏖️
🏜️
🏝️
🏞️
🏟️
🏛️
🏗️
🏘️
🏙️
🏚️
🏠
🏡
🏢
🏣
🏤
🏥
🏦
🏨
🏩
🏪
🏫
🏬
🏭
🏯
🏰
💒
🗼
🗽
🕌
🛕
🕍
⛩️
🕋
🌁
🌃
🏙️
🌄
🌅
🌆
🌇
🌉
🎠
🎡
🎢
💈
🎪
🚂
🚃
🚄
🚅
🚆
🚇
🚈
🚉
🚊
🚝
🚞
🚋
🚌
🚍
🚎
🚐
🚑
🚒
🚓
🚔
🚕
🚖
🚗
🚘
🚙
🚚
🚛
🚜
🏎️
🏍️
🛵
🦽
🦼
🛺
🚲
🛴
🛹
🚏
🛣️
🛤️
🛢️
🚨
🚥
🚦
🚧
🛶
🚤
🛳️
⛴️
🛥️
🚢
✈️
🛩️
🛫
🛬
🪂
💺
🚁
🚟
🚠
🚡
🛰️
🚀
🛸
🧳
📱
💻
⌨️
🖥️
🖨️
🖱️
🖲️
💽
💾
📀
📼
🔍
🔎
💡
🔦
🏮
📔
📕
📖
📗
📘
📙
📚
📓
📒
📃
📜
📄
📰
🗞️
📑
🔖
🏷️
💰
💴
💵
💶
💷
💸
💳
🧾
✉️
📧
📨
📩
📤
📥
📦
📫
📪
📬
📭
📮
🗳️
✏️
✒️
🖋️
🖊️
🖌️
🖍️
📝
📁
📂
🗂️
📅
📆
🗒️
🗓️
📇
📈
📉
📊
📋
📌
📍
📎
🖇️
📏
📐
✂️
🗃️
🗄️
🗑️
🔒
🔓
🔏
🔐
🔑
🗝️
🔨
🪓
⛏️
⚒️
🛠️
🗡️
⚔️
🔫
🏹
🛡️
🔧
🔩
⚙️
🗜️
⚗️
🧪
🧫
🧬
🔬
🔭
📡
💉
🩸
💊
🩹
🩺
🚪
🛏️
🛋️
🪑
🚽
🚿
🛁
🧴
🧷
🧹
🧺
🧻
🧼
🧽
🧯
🛒
🚬
⚰️
⚱️
🗿
🏧
🚮
🚰
🚹
🚺
🚻
🚼
🚾
🛂
🛃
🛄
🛅
⚠️
🚸
🚫
🚳
🚭
🚯
🚱
🚷
📵
🔞
☢️
☣️
❤️
🧡
💛
💚
💙
💜
🖤
💔
❣️
💕
💞
💓
💗
💖
💘
💝
💟
☮️
✝️
☪️
🕉️
☸️
✡️
🔯
🕎
☯️
☦️
🛐
🆔
⚛️
🉑
☢️
☣️
📴
📳
🈶
🈚
🈸
🈺
🈷️
✴️
🆚
💮
🉐
㊙️
㊗️
🈴
🈵
🈹
🈲
🅰️
🅱️
🆎
🆑
🅾️
🆘
🛑
💢
💯
💠
♨️
🚷
🚯
🚳
🚱
🔞
📵
🚭
‼️
⁉️
🔅
🔆
🔱
⚜️
〽️
⚠️
🚸
🔰
♻️
🈯
💹
❇️
✳️
🌐
💠
Ⓜ️
🌀
💤
🏧
🚾
🅿️
🈳
🈂️
🛂
🛃
🛄
🛅
  4 条评论
三岁   福建省厦门市

厉害

常宁市市委书记欧某人   广东省中山市

教我啊,高手