SpringBoot开源项目个人博客——(10)博客详情页

SpringBoot开源项目个人博客——(10)博客详情页

博客详情页面包括博客文章内容评论两大部分。博客详情和评论功能都是独立的,这里将评论单独用一个类来编写接口,查询博客详情就直接放在首页的控制器进行处理。

在跳转博客详情页面的时候,返回model时携带文章详情内容,评论列表在进入blog页面时,用ajax请求刷新处理。


一、博客文章内容

1. 博客详情实体类

根据前端需求,博客详情除了显示博客信息外,还需要显示分类信息。在VO包下创建DetailedBlog博客详情实体类(省略get、set、构造、toString)

/*vo存博客详情,用户信息,分类名*/
public class DetailedBlog {
    private Long id; //主键id
    private String firstPicture; //首图地址
    private String title; //博客标题
    private String content; //博客内容
    private String flag; //是否为原创

    private Integer views; //浏览次数
    private Integer commentCount; //评论次数

    private Date updateTime;//更新时间

    private boolean appreciation; //是否开启赞赏
    private boolean shareStatement; //是否开启版权
    private boolean commentabled;//是否开启评论

    private String nickname;

    private String typeName; //分类名称
}

2. Markdown编辑器工具类

我们现在需要把markdown代码转换为HTML进行存储,这样typo样式才能应用到。

  • 添加依赖Editor相关依赖

在pom.xml中添加

<dependency>
    <groupId>com.atlassian.commonmark</groupId>
    <artifactId>commonmark</artifactId>
    <version>0.10.0</version>
</dependency>

<dependency>
    <groupId>com.atlassian.commonmark</groupId>
    <artifactId>commonmark-ext-heading-anchor</artifactId>
    <version>0.10.0</version>
</dependency>

<dependency>
    <groupId>com.atlassian.commonmark</groupId>
    <artifactId>commonmark-ext-gfm-tables</artifactId>
    <version>0.10.0</version>
</dependency>
  • 添加MarkdownUtils工具类

在util工具包下添加MarkdownUtils工具类:

public class MarkdownUtils {
    /**
     * markdown格式转换成HTML格式
     * @param markdown
     * @return
     */
    public static String markdownToHtml(String markdown) {
        Parser parser = Parser.builder().build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder().build();
        return renderer.render(document);
    }

    /**
     * 增加扩展[标题锚点,表格生成]
     * Markdown转换成HTML
     * @param markdown
     * @return
     */
    public static String markdownToHtmlExtensions(String markdown) {
        //h标题生成id
        Set<Extension> headingAnchorExtensions = Collections.singleton(HeadingAnchorExtension.create());
        //转换table的HTML
        List<Extension> tableExtension = Arrays.asList(TablesExtension.create());
        Parser parser = Parser.builder()
                .extensions(tableExtension)
                .build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder()
                .extensions(headingAnchorExtensions)
                .extensions(tableExtension)
                .attributeProviderFactory(new AttributeProviderFactory() {
                    public AttributeProvider create(AttributeProviderContext context) {
                        return new CustomAttributeProvider();
                    }
                })
                .build();
        return renderer.render(document);
    }

    /**
     * 处理标签的属性
     */
    static class CustomAttributeProvider implements AttributeProvider {
        @Override
        public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
            //改变a标签的target属性为_blank
            if (node instanceof Link) {
                attributes.put("target", "_blank");
            }
            if (node instanceof TableBlock) {
                attributes.put("class", "ui celled table");
            }
        }
    }

    public static void main(String[] args) {
        String table = "| hello | hi   | 哈哈哈   |\n" +
                "| ----- | ---- | ----- |\n" +
                "| 斯维尔多  | 士大夫  | f啊    |\n" +
                "| 阿什顿发  | 非固定杆 | 撒阿什顿发 |\n" +
                "\n";
        String a = "[ONESTAR](https://onestar.newstar.net.cn/)";
        System.out.println(markdownToHtmlExtensions(a));
    }
}

3. 持久层接口

这里把跳转到博客详情页面的接口代码放在博客业务这一块。

在BlogDao接口中添加查询博客详情、文章访问更新(点击一次就更新)、查询评论数量接口

    /*===========博客详情(根据id)==============*/

    //查询博客详情
    DetailedBlog getDetailBlog(Long id);

    //更新后 在服务层调用,设置detailedBlog
    //文章访问更新
    int updateView(Long id);

    //根据博客id更新评论数量
    int getCommentCountById(Long id);

4. mapper

mapper文件夹下的BlogDao.xml文件中添加如下SQL:

<!--==========================博客详情(根据博客id)===================-->
    <resultMap id="detailedBlog" type="DetailedBlog">
    <!--    deatiledBlog 只需要 typeName 对应-->
        <result column="name" property="typeName"/>
    </resultMap>

    <!--查询博客详情-->
    <select id="getDetailBlog" resultMap="detailedBlog">
        select b.id, b.appreciation, b.commentabled, b.content, b.first_picture, b.flag, b.share_statement, b.title, b.update_time, b.views, b.comment_count,
                t.name,
                u.nickname
        from t_blog b,t_type t,t_user u
        where b.type_id = t.id and b.user_id = u.id
          and b.id = #{id};
    </select>

    <!--访问增加-->
    <update id="updateView">
        update t_blog
        set views = views+1
        where id=#{id};
    </update>


    <!--根据博客id查询出评论数量 并 更新(评论更新后,就可以更新数量了)-->
    <update id="getCommentCountById">
        update t_blog b
        set b.comment_count = (
            select count(*)
            from t_comment c where  c.blog_id = #{id} and b.id = #{id}
        )
        where b.id = #{id};
    </update>

5. Service业务层

在BlogService接口下添加

    /*========博客详情================*/
    DetailedBlog getDetailBlog(Long id);

BlogServiceImpl接口实现类添加

接口实现主要是设置文章显示格式,文章访问自增和文章评论的统计

    /*========博客详情================*/

    @Override
    public DetailedBlog getDetailBlog(Long id) {
        DetailedBlog detailedBlog = blogDao.getDetailBlog(id);
        if(detailedBlog == null){
            throw new NotFoundException("该博客不存在");
        }

        /*设置文章显示格式 marktohtml,这样typo样式才能应用*/
        String content = detailedBlog.getContent();
        detailedBlog.setContent(MarkdownUtils.markdownToHtmlExtensions(content));

        //文章访问数量自增 设置(这样获取就是最新的)
        blogDao.updateView(id);
        //评论数量 更新设置
        blogDao.getCommentCountById(id);

        return detailedBlog;
    }

6. 控制器

IndexController类中添加接口

    /*跳转到 博客详情页 /blog/{id}*/
    @GetMapping("/blog/{id}")
    public String blog(@PathVariable Long id,Model model){
        DetailedBlog detailedBlog = blogService.getDetailBlog(id);
        model.addAttribute("blog", detailedBlog);

        return "blog";//没有携带评论数据(在blog 中 ajax刷新)
    }

二、评论功能

评论稍微复杂些,本博客不会做太多详细讲述,详情请移步oneStar的博客。

(笔者开始也不是很理解评论功能的实现,过一遍代码流程,然后手打几次后,对评论功能的理解能更熟一点)

在这里插入图片描述

1. 持久层接口

dao包下创建CommentDao接口,添加如下接口:

@Mapper
@Repository
public interface CommentDao {

    //新增一个评论
    int savaComment(Comment comment);

    //删除一个评论
    void deleteComment(Long id);

    /*查询父级评论 查询一级回复 查询二级回复*/

    //多个参数 用@param

    //查询父级评论 : 它的评论类的父评论的-1
    List<Comment> findByBlogIdParentIdNull(@Param("blogId") Long blogId,@Param("commentParentId")Long commentParentId);

    //查询一级回复 : 根据父评论id
    List<Comment> findByBlogIdParentNotNull(@Param("blogId") Long blogId,@Param("parentId")Long parentId);

    //查询二级回复 : 根据子回复 id
    List<Comment> findByBlogAndReplyId(@Param("blogId")Long blogId,@Param("childId")Long childId);


}

/*
* 查询评论信息需要:
* 查询父级评论(findByBlogIdParentIdNull)、
* 查询一级回复(findByBlogIdParentIdNotNull)、
* 查询二级回复(findByBlogIdAndReplayId)
* =====================
*
    根据id为“-1”和博客id查询出所有父评论(父级评论id为‘-1’)
    根据父评论的id查询出一级子回复
    根据子回复的id循环迭代查询出所有子集回复
    将查询出来的子回复放到一个集合中

* */

2.mapper

mapper目录下创建CommentDao.xml文件,添加如下

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.hxj.dao.CommentDao">

    <!--新增一个评论-->
    <insert id="savaComment" parameterType="Comment">
        insert into t_comment (nickname, email, content, avatar, create_time, blog_id, parent_comment_id, admin_comment)
        values (#{nickname},#{email},#{content},#{avatar},#{createTime},#{blogId},#{parentCommentId},#{adminComment});
    </insert>


    <!--删除一个评论-->
    <delete id="deleteComment">
        delete
        from t_comment
        where id=#{id};
    </delete>

    <!--查询父级评论 : 它的评论类的父评论的-1(没爹)-->
    <select id="findByBlogIdParentIdNull" resultType="Comment">
        select *
        from t_comment
        where blog_id=#{blogId} and parent_comment_id = #{commentParentId}
        order by create_time desc;
    </select>

    <!--查询一级回复 : 根据父评论id(有爹)-->
    <select id="findByBlogIdParentNotNull" resultType="Comment">
        select *
        from t_comment
        where blog_id = #{blogId} and parent_comment_id = #{parentId}
        order by create_time desc;
    </select>

    <!--根据子回复 id-->
    <select id="findByBlogAndReplyId" resultType="Comment">
        select *
        from t_comment
        where blog_id = #{blogId} and parent_comment_id = #{childId}
        order by create_time desc;
    </select>

</mapper>

3. 业务层

service包下创建CommentService接口

public interface CommentService {

    //新增一个评论
    int savaComment(Comment comment);

    //删除一个评论 Comment comment
    void deleteComment(Long blogId,Long id);

    /*查询评论 根据博客id查询评论信息*/
    List<Comment> listCommentByBlogId(Long blogId);
}

Impl包下创建接口实现类:CommentServiceImpl接口实现类

@Service
public class CommentServiceImpl implements CommentService {

    @Autowired
    private CommentDao commentDao;
    @Autowired
    private BlogDao blogDao; //评论数据更新后, 也要更新博客的 评论数据

    //存放迭代找出的所有"子代评论"的集合
    private List<Comment> tempReplys = new ArrayList<>();


    @Override
    public int savaComment(Comment comment) {
        comment.setCreateTime(new Date());
        int comments = commentDao.savaComment(comment);
        //文章评论统计 更新
        blogDao.getCommentCountById(comment.getBlogId());
        return comments; //返回影响数
    }

    @Override
    public void deleteComment(Long blogId,Long id) {
        //dao只需要根据id 删除即可
        commentDao.deleteComment(id);
        blogDao.getCommentCountById(/*comment.getBlogId()*/ blogId);
    }

    /*查询评论*/
    @Override
    public List<Comment> listCommentByBlogId(Long blogId) {
        //查出 父节点, 没有爹的(pid = -1)
        List<Comment> comments = commentDao.findByBlogIdParentIdNull(blogId, (long) -1);

        //为每个父评论 找 它的子代评论
        for (Comment comment : comments) {
            Long id = comment.getId();
            String Nickname = comment.getNickname();

            List<Comment> childComments = commentDao.findByBlogIdParentNotNull(blogId, id);//查 一级子评论

            //查询出子(一)评论
            combineChildren(blogId,childComments,Nickname);

            comment.setReplyComments(tempReplys);//最后查出所有 子代评论后,就赋给 该评论.
            //System.out.println(tempReplys);
            //tempReplys.clear(); //(然后清空temp,存下一个 评论的 子代) 不能这样, 会清了内容, controller拿不到,地址要留着
            tempReplys=new ArrayList<>();
        }

        return comments;
    }

    //查询出子(一级)评论   (blogId,childComments, 上一代评论名 做为 下一代的父评论名)
    private void combineChildren(Long blogId,List<Comment> childComments,String parentNickname){

        //判断是否有 一级子评论
        if(childComments.size() > 0){
            //循环找出子评论的 id
            for (Comment childComment : childComments) {
                String parentNickname1 = childComment.getNickname(); //子评论的 nickname

                childComment.setParentNickname(parentNickname); //在这里才设置 回复的 parentNickname

                tempReplys.add(childComment);//存 入子代评论中

                Long childId = childComment.getId();
                //查询出子二级评论
                recursively(blogId,childId,parentNickname1);
            }
        }
    }

    //查询出子二级评论  (blogId,parentNickname, 上一代评论名 做为 下一代的父评论名)
    private void recursively(Long blogId,Long childId,String parentNickname){
        //根据一级评论 的id 找到二级评论
        List<Comment> replyComments = commentDao.findByBlogAndReplyId(blogId, childId);

        if(replyComments.size() > 0){
            for (Comment replyComment : replyComments) {
                String parentNickname2 = replyComment.getNickname();

                replyComment.setParentNickname(parentNickname);
                Long replyId = replyComment.getId();

                tempReplys.add(replyComment); //把二级评论加入
                //评论可能 还有子代评论 .... 接着找
                recursively(blogId, replyId, parentNickname2);
            }
        }
    }

}

4. 控制器

引用oneStar的内容

添加图片配置 在评论中,需要显示头像,这里直接在配置文件里面进行配置,在application.yml中添加如下配置(这里直接把后面留言的显示图片也一起添加了):

comment.avatar: /images/avatar.png
message.avatar: /images/avatar.png

controller包下创建CommentController类,编写如下代码:

@Controller
public class CommentController {

    @Autowired
    private CommentService commentService;

    @Value("${comment.avatar}")
    private String avatar;


    private String subject="HXJ的博客评论回复";

    //查询评论列表
    @GetMapping("/comments/{blogId}")
    public String comments(@PathVariable("blogId") Long blogId, Model model){
        List<Comment> comments = commentService.listCommentByBlogId(blogId);
        System.out.println(comments);
        model.addAttribute("comments", comments);
        return  "blog :: commentList";
    }

    //新增评论(有session 就是 管理员(后台登录了),游客就没有,(发评论时就没有传session)所以也删除不了)
    @PostMapping("/comments")
    public String post(Comment comment, HttpSession session,Model model){
        Long blogId = comment.getBlogId();
        User user =(User)session.getAttribute("user");
        if(user!=null){
            comment.setAvatar(user.getAvatar());
            comment.setAdminComment(true);
        }else {
            comment.setAvatar(avatar);
        }

        comment.setContent(sensitiveFilterUtil.filter(comment.getContent()));

        //前端 传来的是 parentComment.id
        if(comment.getParentComment().getId() !=null ){
            //是评论
            comment.setParentCommentId(comment.getParentComment().getId()); //到impl中 的展示才设置parentNickname

            //是子评论
            if(comment.getParentComment().getId()!=-1){
                Comment parentComment = commentService.findByParentId(comment.getParentComment().getId());//获得父评论(目的为了它的昵称)
                
            }

        }
        commentService.savaComment(comment);//新增评论

        List<Comment> comments = commentService.listCommentByBlogId(blogId);//得到最新评论
        //System.out.println(comments);
        model.addAttribute("comments", comments);

        return "blog :: commentList";
    }



    //删除 评论
    @PostMapping("/comments/delete")
    public String delete(Model model,@RequestParam("blogId")  Long blogId,@RequestParam("commentId") Long id){
        commentService.deleteComment(blogId,id);

        List<Comment> comments = commentService.listCommentByBlogId(blogId);
        System.out.println(comments);
        model.addAttribute("comments", comments);

        return "blog :: commentList";
        
    }
}

实现评论操作的局部刷新

基于oneStar的前端改动而来:

在这里插入图片描述

删除操作也改成局部刷新(注释掉的就是原版)

<div class="actions">
    <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${comment.id},data-commentnickname=${comment.nickname}" onclick="reply(this)">回复</a>
    <a class="delete" onclick="shanchu(this)" th:attr="data-blogid=${comment.blogId},data-commentid=${comment.id}"  th:if="${session.user}">删除</a>

    <!--<a class="delete" href="#" th:href="@{/comment/{param1}/{param2}/delete(param1=${comment.blogId},param2=${comment.id})}" οnclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a>-->
    <!--<a class="delete" href="#" th:href="@{/comment/{id}/delete(id=${comment.id})}" οnclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a>-->

</div>

.....
....
.....
<div class="actions">
    <a class="reply" data-commentid="1" data-commentnickname="Matt" th:attr="data-commentid=${reply.id},data-commentnickname=${reply.nickname}" onclick="reply(this)">回复</a>
    <a class="delete" href="#" onclick="shanchu(this)" th:attr="data-blogid=${reply.blogId},data-commentid=${reply.id}"  th:if="${session.user}">删除</a>

    <!--<a class="delete" href="#" th:href="@{/comment/{param1}/{param2}/delete(param1=${reply.blogId},param2=${reply.id})}" οnclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a>-->
    <!--<a class="delete" href="#" th:href="@{/comment/{id}/delete(id=${reply.id})}" οnclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a>-->

</div>

JS内容如下:

    $(function () {
      $("#comment-container").load(/*[[@{/comments/{id}(id=${blog.id})}]]*/"comments/6");
    });



    $('#commentpost-btn').click(function () {
        var boo = $('.ui.form').form('validate form');
        if (boo) {
            console.log('校验成功');
            postData();
        } else {
            console.log('校验失败');
        }
    });



    function postData() {
        $("#comment-container").load(/*[[@{/comments}]]*/"/comments",{
            "parentComment.id" : $("[name='parentComment.id']").val(),
            "blogId" : $("[name='blogId']").val(),
            "nickname": $("[name='nickname']").val(),
            "email"   : $("[name='email']").val(),
            "content" : $("[name='content']").val()
        },function (responseTxt, statusTxt, xhr) {
            //$(window).scrollTo($('#goto'),500);
          $(window).scrollTo($('#comment-container'),500);
          clearContent();
        });
    }

    function clearContent() {
        $("[name='nickname']").val('');
        $("[name='email']").val('');

      var user = /*[[${session.user}]]*/ null;

      /*因为我们在 admin的logincontroller 把user 传进 session*/

      console.log(user);
        if( user ){
        $("[name='nickname']").val(user.nickname);
        $("[name='email']").val(user.email);
      }
      $("[name='content']").val('');
      $("[name='parentComment.id']").val(-1);
      $("[name='content']").attr("placeholder", "请输入评论信息...");
  }

  function reply(obj) {
    var commentId = $(obj).data('commentid');
    var commentNickname = $(obj).data('commentnickname');
    $("[name='content']").attr("placeholder", "@"+commentNickname).focus();
    $("[name='parentComment.id']").val(commentId);
    //$(window).scrollTo($('#comment-container'),500);
    $(window).scrollTo($('#comment-form'),500);

  }

  /*改造成,删除了只改动评论区. 不然模板包太大,评论区又会问题..目前只能这样*/
  function shanchu(obj){
    var flag =  confirm('确定要删除该评论吗?三思啊! 删了可就没了!');
    if(flag == true){
      var blogId = $(obj).data('blogid');
      var commentId = $(obj).data('commentid');

      $("#comment-container").load(/*[[@{/comments/delete}]]*/"",{
        "blogId" : blogId,
        "commentId" : commentId
      });
    }
      $(window).scrollTo($('#comment-container'),500);
  }


评论新增功能(敏感词过滤/回复发邮件)

敏感词过滤

参考: 敏感词

直接代码拷贝用就行

  1. resources目录下新建sensitive-words.txt,按照参考那样写点敏感词。

  2. utils包下新建一个SensitiveFilterUtil工具类。

@Component
public class SensitiveFilterUtil {

    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilterUtil.class);

    private static final String REPLACEMENT = "**";
    //根节点
    private static final TrieNode rootNode = new TrieNode();

    // @PostConstruct 表明这是一个初始化方法,在类被实例化的时候就执行。
    @PostConstruct
    public void init(){
        // 获取类加载器,类加载器是从类路径下,去加载资源,所谓的类路径就是target/classes/目录之下。
        // 我们的程序一编译,所有的程序都编译到classes下,包括配置文件。
        try(
                //得到字节流
                InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                //从字节流中读文字不太方便,把字节流转成字符流,把字符流改成缓冲流会效率更高
                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        ){
            String keyword;  //用这个keyword去读数据
            while((keyword = reader.readLine()) != null){//一行一行的读
                //添加到前缀树
                this.addKeyWord(keyword);
            }
        }catch (IOException e){
            logger.error("获取铭感词文件失败:"+e.getMessage());
        }
    }
    //把一个敏感词添加到前缀树中去。
    public void addKeyWord(String keyword){
        TrieNode tempNode = rootNode;
        for(int i = 0 ; i< keyword.length() ; ++i){
            char key = keyword.charAt(i);
            //找子节点
            TrieNode subNode = tempNode.getSubNode(key);
            if(subNode == null){//如果没有这个子节点
                //初始化子节点;
                subNode = new TrieNode();
                tempNode.addSubNode(key,subNode);
            }
            //指向子节点,进入下一次循环
            tempNode = subNode;
            if(i == keyword.length() -1){
                tempNode.setKeyWordEnd(true);
            }
        }
    }

    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return    过滤后的文本
     */
    public String filter(String text){
        if(StringUtils.isBlank(text)){
            return null;
        }
        //指针1 ,指向树
        TrieNode tempNode = rootNode;
        //指针2 指向字符串的慢指针,一直往前走
        int begin = 0;
        //指针3 指向字符串的快指针,往后走检查,如果不是就归位。
        int position = 0;
        //结果
        StringBuilder result = new StringBuilder();
        while(position < text.length()){
            char c = text.charAt(position);
            //跳过符号
            if(isSymbol(c)){
                //若指针处于根节点。则将此符号计入结果,让指针向下走一步。
                if(tempNode == rootNode){
                    result.append(c);
                    begin++;
                }
                //无论结构在开头或中间指针3都向下走一步。
                position++;
                continue;
            }
            tempNode = tempNode.getSubNode(c);
            if(tempNode == null){
//                以begin为开头的字符串不存在敏感词
                result.append(text.charAt(begin));
                //进入下一个位置
                position = ++begin;
                // 重新指向根节点。
                tempNode  = rootNode;
            }else if(tempNode.isKeyWordEnd()){
                //发现了敏感词,将begin-posttion中的字符串替换
                result.append(REPLACEMENT);
//                进入下一个位置。
                begin = ++ position;
                tempNode = rootNode;
            }else{
                //检查下一个字符
                position++;
            }
        }
        //将最后一批字符计入结构
        result.append(text.substring(begin));
        return result.toString();
    }

    //判断是否为符号,是的话返回true,不是的话返回false
    public boolean isSymbol(Character c){
        //!CharUtils.isAsciiAlphanumeric(c)判断合法字符
        //c < 0x2E80 || c > 0x9fff 东亚文字的范围是0x2E80到0x9fff
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9fff);
    }

    //定义一个内部类,作为前缀树的结构
    private static class TrieNode{
        //关键词结束的标识
        private boolean isKeyWordEnd = false;
        //子节点(key 代表下级的节点字符, value是下级节点)
        private Map<Character, TrieNode> subNode = new HashMap<>();

        public boolean isKeyWordEnd() {
            return isKeyWordEnd;
        }

        public void setKeyWordEnd(boolean keyWordEnd) {
            isKeyWordEnd = keyWordEnd;
        }

        //添加子节点
        public void addSubNode(Character key,TrieNode subNode){
            this.subNode.put(key,subNode);
        }
        //获取子节点
        public TrieNode getSubNode(Character key){
            return subNode.get(key);
        }
    }
}

发邮件功能

参考博客

代码直接拷贝用即可

  1. utils包下新建一个MailUtil工具类。
@Component
public class MailUtil {
    @Value("${spring.mail.username}")
    String from;

    @Autowired
    JavaMailSender mailSender;
    //简单邮件
    public void sendSimpleMail(String to, String subject, String content){
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from); //发件人
        message.setTo(to);//收件人
        message.setSubject(subject); //标题
        message.setText(content);   //文件内容

        try {
            mailSender.send(message);
            System.out.println("简单邮件发送成功!");
        } catch (Exception e){
            System.out.println("发送简单邮件时发生异常!"+e);
        }
    }
    //html格式邮件
    public void sendHtmlMail(String to, String subject, String content){
        MimeMessage message = mailSender.createMimeMessage();
        try {
            //true表示需要创建一个multipart message
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);

            mailSender.send(message);
            System.out.println("html邮件发送成功!");
        } catch (MessagingException e) {
            System.out.println("发送html邮件时发生异常!"+e);
        }
    }

    //带附件的邮件
    public void sendAttachmentsMail(String to, String subject, String content, String filePath){
        MimeMessage message = mailSender.createMimeMessage();

        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);

            FileSystemResource file = new FileSystemResource(new File(filePath));
            String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
            helper.addAttachment(fileName, file);

            mailSender.send(message);
            System.out.println("带附件的邮件已经发送。");
        } catch (MessagingException e) {
            System.out.println("发送带附件的邮件时发生异常!" + e);
        }
    }

    //带静态资源的邮件
    public void sendInlineResourceMail(String to, String subject, String content, String rscPath, String rscId){
        MimeMessage message = mailSender.createMimeMessage();

        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);

            FileSystemResource res = new FileSystemResource(new File(rscPath));
            helper.addInline(rscId, res);

            mailSender.send(message);
            System.out.println("嵌入静态资源的邮件已经发送。");
        } catch (MessagingException e) {
            System.out.println("发送嵌入静态资源的邮件时发生异常!" + e);
        }
    }
}

我的思路是在controller直接发生邮件, 但此时父评论的信息并没有(它只在评论列表获取的service接口实现类才存在)。

所以为了获取父评论信息,直接mvc3层都新增一段新代码。

  1. 配置application.yml

    #邮件配置(在spring层级下的)
      mail:
        host: smtp.qq.com
        username: 邮箱
        password: 授权码
        default-encoding: utf-8
        port: 25
    
  2. dao包下的CommentDao接口,添加如下接口:

    /*单独查询一条评论,查出父评论名(用于在controller发送邮件)-虽然在service中才会赋值父评论名,我们想提前用*/
    Comment findByParentId(@Param("id")Long id);
  1. mapper目录下CommentDao.xml文件中,添加如下sql:
    <!--查询父评论(为了昵称),在controller提前用来发邮件-->
    <select id="findByParentId" resultType="Comment">
        select *
        from t_comment where id=#{id};
    </select>
  1. service包下的CommentService添加接口:
    Comment findByParentId(Long id);

Impl包下的CommentServiceImpl接口实现类,添加:

    @Override
    public Comment findByParentId(Long id) {
        return commentDao.findByParentId(id);
    }

控制器

controller包下的CommentController类中,注入上面这两个工具类,然后修改新增评论接口如下:

//新增评论(有session 就是 管理员(后台登录了),游客就没有,(发评论时就没有传session)所以也删除不了)
    @PostMapping("/comments")
    public String post(Comment comment, HttpSession session,Model model){
        Long blogId = comment.getBlogId();
        User user =(User)session.getAttribute("user");
        if(user!=null){
            comment.setAvatar(user.getAvatar());
            comment.setAdminComment(true);
        }else {
            comment.setAvatar(avatar);
        }

        comment.setContent(sensitiveFilterUtil.filter(comment.getContent()));

        //前端 传来的是 parentComment.id
        if(comment.getParentComment().getId() !=null ){
            //是评论
            comment.setParentCommentId(comment.getParentComment().getId()); //到impl中 的展示才设置parentNickname

            //是子评论
            if(comment.getParentComment().getId()!=-1){
                Comment parentComment = commentService.findByParentId(comment.getParentComment().getId());//获得父评论(目的为了它的昵称)
                //发邮件回复给父评论
                String rpl="亲爱的【"+parentComment.getNickname()+"】,你在【HXJ的博客】的评论:"+parentComment.getContent()+".收到了来自【"+
                        comment.getNickname()+"】的回复! 内容如下:\n\n";
                mailUtil.sendSimpleMail(parentComment.getEmail(), subject, rpl+comment.getContent());
            }

        }
        commentService.savaComment(comment);//新增评论

        List<Comment> comments = commentService.listCommentByBlogId(blogId);//得到最新评论
        //System.out.println(comments);
        model.addAttribute("comments", comments);

        return "blog :: commentList";
    }


至此实现了博客详情内容,困难点是评论(笔者把oneStar原本的前端全部改为局部刷新,强迫症的我效果舒服一点。而且js改成如果session有用户信息,直接会在评论区的输入框中填入用户信息(使用了js内联操作,现学现卖的),不过这个人只能是我自己了。)

浏览后的效果示例图如下:

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值