博客地址 :ONESTARの客栈
源码领取方式一:
源码领取方式二:
前端页面源码地址: github.com/oneStarLR/m…
以jpa为持久层源码地址: github.com/oneStarLR/m…
以mybatis为持久层源码地址: github.com/oneStarLR/m…
欢迎给star以鼓励(^_−)☆
博客详情页面包括文章内容和评论部分,本文将从文章内容显示和评论功能来讲述SpringBoot搭建个人博客的详情页面显示,由于博客详情和分类功能都是独立的,这里将评论单独用一个类来编写接口,查询博客详情就直接放在首页的控制器进行处理
分析:
问:博客详情页面是包含文章内容和评论部分,要如何处理
答:在跳转博客详情页面的时候,可以返回连个model,一个是文章详情内容,一个是评论列表
问:文章详情内容如何处理,需要哪些接口?评论如何处理,又需要哪些接口?
答:文章详情内容定义getDetailedBlog博客详情接口,需要定义一个查询实体类来封装一下查询内容,并把分类信息也包含进来;评论功能则需要定义listCommentByBlogId查询评论列表接口、saveComment新增保存接口、deleteComment删除接口
分析:
问:在博客详情页面中,文章显示格式要如何处理,文章访问数量如何处理,评论数量又该如何处理?
答:这些都在getDetailedBlog接口实现类中实现
博客详情除了显示博客信息外,还需要显示分类信息,所以还要创建分类名称属性,在queryvo包下创建DetailedBlog博客详情实体类,代码如下(省去get、set、toString方法):
package com.star.queryvo; import java.util.Date; /** * @Description: 博客详情实体类 * @Date: Created in 10:10 2020/6/19 * @Author: ONESTAR * @QQ群: 530311074 * @URL: https://onestar.newstar.net.cn/ */ public class DetailedBlog { //博客信息 private Long id; private String firstPicture; private String flag; private String title; private String content; private Integer views; private Integer commentCount; private Date updateTime; private boolean commentabled; private boolean shareStatement; private boolean appreciation; private String nickname; private String avatar; //分类名称 private String typeName; } 复制代码
在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> 复制代码
在util工具包下添加MarkdownUtils工具类:
package com.star.util; import org.commonmark.Extension; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.ext.heading.anchor.HeadingAnchorExtension; import org.commonmark.node.Link; import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.AttributeProvider; import org.commonmark.renderer.html.AttributeProviderContext; import org.commonmark.renderer.html.AttributeProviderFactory; import org.commonmark.renderer.html.HtmlRenderer; import java.util.*; /** * @Description: Markdown编辑器 * @Author: ONESTAR * @Date: Created in 13:24 2020/4/5 * @QQ群: 530311074 * @URL: https://onestar.newstar.net.cn/ */ 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)); } } 复制代码
文章内容显示是从首页点击文章标题或图片,然后跳转到博客详情页面的,所以这里就将代码放在博客业务这一块
在BlogDao接口中添加查询博客详情、文章访问更新、查询评论数量接口
//查询博客详情 DetailedBlog getDetailedBlog(Long id); //文章访问更新 int updateViews(Long id); //根据博客id查询出评论数量 int getCommentCountById(Long id); 复制代码
根据持久层接口,编写如下SQL:查询博客详情、文章访问更新、查询评论数量,这里需要对博客详情进行封装
<resultMap id="detailedBlog" type="com.star.queryvo.DetailedBlog"> <id property="id" column="bid"/> <result property="firstPicture" column="first_picture"/> <result property="flag" column="flag"/> <result property="title" column="title"/> <result property="content" column="content"/> <result property="typeName" column="name"/> <result property="views" column="views"/> <result property="commentCount" column="comment_count"/> <result property="updateTime" column="update_time"/> <result property="commentabled" column="commentabled"/> <result property="shareStatement" column="share_statement"/> <result property="appreciation" column="appreciation"/> <result property="nickname" column="nickname"/> <result property="avatar" column="avatar"/> </resultMap> <!--博客详情查询--> <select id="getDetailedBlog" resultMap="detailedBlog"> select b.id bid,b.first_picture,b.flag,b.title,b.content,b.views,b.comment_count,b.update_time,b.commentabled,b.share_statement,b.appreciation, u.nickname,u.avatar,t.name from myblog.t_blog b,myblog.t_user u, myblog.t_type t where b.user_id = u.id and b.type_id = t.id and b.id = #{id} </select> <!--文章访问自增--> <update id="updateViews" parameterType="com.star.entity.Blog"> update myblog.t_blog b set b.views = b.views+1 where b.id = #{id} </update> <!--查询出文章评论数量并更新--> <update id="getCommentCountById" parameterType="com.star.entity.Blog"> update myblog.t_blog b set b.comment_count = ( select count(*) from myblog.t_comment c where c.blog_id = #{id} and b.id = #{id} ) WHERE b.id = #{id} </update> 复制代码
在BlogService接口中添加查询博客详情方法:
//查询博客详情 DetailedBlog getDetailedBlog(Long id); 复制代码
次接口实现主要是设置文章显示格式,文章访问自增和文章评论的统计,在BlogServiceImpl类中添加实现方法,如下:
@Override public DetailedBlog getDetailedBlog(Long id) { DetailedBlog detailedBlog = blogDao.getDetailedBlog(id); if (detailedBlog == null) { throw new NotFoundException("该博客不存在"); } String content = detailedBlog.getContent(); detailedBlog.setContent(MarkdownUtils.markdownToHtmlExtensions(content)); //文章访问数量自增 blogDao.updateViews(id); //文章评论数量更新 blogDao.getCommentCountById(id); return detailedBlog; } 复制代码
在IndexController类中添加方法,调用业务层接口:
//跳转博客详情页面 @GetMapping("/blog/{id}") public String blog(@PathVariable Long id, Model model) { DetailedBlog detailedBlog = blogService.getDetailedBlog(id); model.addAttribute("blog", detailedBlog); return "blog"; } 复制代码
由于评论稍微复杂些,这里将评论单独放一个业务层
分析:
问:评论业务层需要哪些接口?
答:评论直接在前端页面上进行操作,没有后台管理,只是区分的管理员和普通用户,管理员可以对评论进行删除,因此需要查询评论列表(listCommentByBlogId)、添加保存评论(saveComment)、删除评论(deleteComment)接口
问:业务层这些接口够了,但持久层光这些够了吗?需要哪些SQL,需要哪些持久层接口呢?
答:持久层接口肯定是不够的,主要是查询评论列表的时候,需要将评论和回复加以区分,根据评论功能来看,有父评论、子评论(回复),并且父子评论在前端显示的位置有不同,这里细说一下查询:
所以查询评论信息需要:查询父级评论(findByBlogIdParentIdNull)、查询一级回复(findByBlogIdParentIdNotNull)、查询二级回复(findByBlogIdAndReplayId)
在dao包下创建CommentDao接口,添加如下接口:
package com.star.dao; import com.star.entity.Comment; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; /** * @Description: 评论持久层接口 * @Date: Created in 9:21 2020/6/23 * @Author: ONESTAR * @QQ群: 530311074 * @URL: https://onestar.newstar.net.cn/ */ @Mapper @Repository public interface CommentDao { //查询父级评论 List<Comment> findByBlogIdParentIdNull(@Param("blogId") Long blogId, @Param("blogParentId") Long blogParentId); //查询一级回复 List<Comment> findByBlogIdParentIdNotNull(@Param("blogId") Long blogId, @Param("id") Long id); //查询二级回复 List<Comment> findByBlogIdAndReplayId(@Param("blogId") Long blogId,@Param("childId") Long childId); //添加一个评论 int saveComment(Comment comment); //删除评论 void deleteComment(Long id); } 复制代码
在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.star.dao.CommentDao"> <!--添加评论--> <insert id="saveComment" parameterType="com.star.entity.Comment"> insert into myblog.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> <!--查询父级评论--> <select id="findByBlogIdParentIdNull" resultType="com.star.entity.Comment"> select * from myblog.t_comment c where c.blog_id = #{blogId} and c.parent_comment_id = #{blogParentId} order by c.create_time desc </select> <!--查询一级子评论--> <select id="findByBlogIdParentIdNotNull" resultType="com.star.entity.Comment"> select * from myblog.t_comment c where c.blog_id = #{blogId} and c.parent_comment_id = #{id} order by c.create_time desc </select> <!--查询二级子评论--> <select id="findByBlogIdAndReplayId" resultType="com.star.entity.Comment"> select * from myblog.t_comment c where c.blog_id = #{blogId} and c.parent_comment_id = #{childId} order by c.create_time desc </select> <!--删除评论--> <delete id="deleteComment" > delete from myblog.t_comment where id = #{id} </delete> </mapper> 复制代码
讲解:
添加删除:直接使用insert和delete即可进行添加和删除
查询:
在service包下创建CommentService接口,如下:
package com.star.service; import com.star.entity.Comment; import java.util.List; /** * @Description: 评论业务层接口 * @Date: Created in 10:22 2020/6/23 * @Author: ONESTAR * @QQ群: 530311074 * @URL: https://onestar.newstar.net.cn/ */ public interface CommentService { //根据博客id查询评论信息 List<Comment> listCommentByBlogId(Long blogId); //添加保存评论 int saveComment(Comment comment); //删除评论 void deleteComment(Comment comment,Long id); } 复制代码
在Impl包下创建接口实现类:CommentServiceImpl,功能都在这个接口中实现
package com.star.service.Impl; import com.star.dao.BlogDao; import com.star.dao.CommentDao; import com.star.entity.Comment; import com.star.service.CommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * @Description: 博客评论业务层接口实现类 * @Date: Created in 10:23 2020/6/23 * @Author: ONESTAR * @QQ群: 530311074 * @URL: https://onestar.newstar.net.cn/ */ @Service public class CommentServiceImpl implements CommentService { @Autowired private CommentDao commentDao; @Autowired private BlogDao blogDao; //存放迭代找出的所有子代的集合 private List<Comment> tempReplys = new ArrayList<>(); /** * @Description: 查询评论 * @Auther: ONESTAR * @Date: 10:42 2020/6/23 * @Param: blogId:博客id * @Return: 评论消息 */ @Override public List<Comment> listCommentByBlogId(Long blogId) { //查询出父节点 List<Comment> comments = commentDao.findByBlogIdParentIdNull(blogId, Long.parseLong("-1")); for(Comment comment : comments){ Long id = comment.getId(); String parentNickname1 = comment.getNickname(); List<Comment> childComments = commentDao.findByBlogIdParentIdNotNull(blogId,id); //查询出子评论 combineChildren(blogId, childComments, parentNickname1); comment.setReplyComments(tempReplys); tempReplys = new ArrayList<>(); } return comments; } /** * @Description: 查询出子评论 * @Auther: ONESTAR * @Date: 10:43 2020/6/23 * @Param: childComments:所有子评论 * @Param: parentNickname1:父评论姓名 * @Return: */ private void combineChildren(Long blogId, List<Comment> childComments, String parentNickname1) { //判断是否有一级子评论 if(childComments.size() > 0){ //循环找出子评论的id for(Comment childComment : childComments){ String parentNickname = childComment.getNickname(); childComment.setParentNickname(parentNickname1); tempReplys.add(childComment); Long childId = childComment.getId(); //查询出子二级评论 recursively(blogId, childId, parentNickname); } } } /** * @Description: 循环迭代找出子集回复 * @Auther: ONESTAR * @Date: 10:44 2020/6/23 * @Param: chileId:子评论id * @Param: parentNickname1:子评论姓名 * @Return: */ private void recursively(Long blogId, Long childId, String parentNickname1) { //根据子一级评论的id找到子二级评论 List<Comment> replayComments = commentDao.findByBlogIdAndReplayId(blogId,childId); if(replayComments.size() > 0){ for(Comment replayComment : replayComments){ String parentNickname = replayComment.getNickname(); replayComment.setParentNickname(parentNickname1); Long replayId = replayComment.getId(); tempReplys.add(replayComment); recursively(blogId,replayId,parentNickname); } } } //新增评论 @Override public int saveComment(Comment comment) { comment.setCreateTime(new Date()); int comments = commentDao.saveComment(comment); //文章评论计数 blogDao.getCommentCountById(comment.getBlogId()); return comments; } //删除评论 @Override public void deleteComment(Comment comment,Long id) { commentDao.deleteComment(id); blogDao.getCommentCountById(comment.getBlogId()); } } 复制代码
comment.avatar: /images/avatar.png message.avatar: /images/avatar.png 复制代码
在controller包下创建CommentController类,如下:
package com.star.controller; import com.star.entity.Comment; import com.star.entity.User; import com.star.queryvo.DetailedBlog; import com.star.service.BlogService; import com.star.service.CommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.servlet.http.HttpSession; import java.util.List; /** * @Description: 评论控制器 * @Date: Created in 10:25 2020/6/23 * @Author: ONESTAR * @QQ群: 530311074 * @URL: https://onestar.newstar.net.cn/ */ @Controller public class CommentController { @Autowired private CommentService commentService; @Autowired private BlogService blogService; @Value("${comment.avatar}") private String avatar; //查询评论列表 @GetMapping("/comments/{blogId}") public String comments(@PathVariable Long blogId, Model model) { List<Comment> comments = commentService.listCommentByBlogId(blogId); model.addAttribute("comments", comments); return "blog :: commentList"; } //新增评论 @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); } if (comment.getParentComment().getId() != null) { comment.setParentCommentId(comment.getParentComment().getId()); } commentService.saveComment(comment); List<Comment> comments = commentService.listCommentByBlogId(blogId); model.addAttribute("comments", comments); return "blog :: commentList"; } //删除评论 @GetMapping("/comment/{blogId}/{id}/delete") public String delete(@PathVariable Long blogId, @PathVariable Long id, Comment comment, RedirectAttributes attributes, Model model){ commentService.deleteComment(comment,id); DetailedBlog detailedBlog = blogService.getDetailedBlog(blogId); List<Comment> comments = commentService.listCommentByBlogId(blogId); model.addAttribute("blog", detailedBlog); model.addAttribute("comments", comments); return "blog"; } } 复制代码
讲解:
查询评论列表:调用接口查询评论信息列表,局部刷新评论信息
新增评论:对评论进行判断,区分游客和管理员
删除评论:将博客id和评论id参数传入,判断删除的是哪一条评论,这里没有做迭代删除子评论,若删除了含有回复的评论,根据之前的查询来看,在前端回复也不会查询出来,但回复并没有删除,依然在数据库里面,删除的只是父评论
给出部分前端代码,仅供参考,了解更多可以查看整个项目源码: github.com/oneStarLR/m…
<input type="hidden" name="blogId" th:value="${blog.id}"> <input type="hidden" name="parentComment.id" value="-1"> <div class="field"> <textarea name="content" placeholder="请输入评论信息..."></textarea> </div> <div class="fields"> <div class="field m-mobile-wide m-margin-bottom-small"> <div class="ui left icon input"> <i class="user icon"></i> <input type="text" name="nickname" placeholder="姓名" th:value="${session.user}!=null ? ${session.user.nickname}"> </div> </div> <div class="field m-mobile-wide m-margin-bottom-small"> <div class="ui left icon input"> <i class="mail icon"></i> <input type="text" name="email" placeholder="邮箱" th:value="${session.user}!=null ? ${session.user.email}"> </div> </div> <div class="field m-margin-bottom-small m-mobile-wide"> <button id="commentpost-btn" type="button" class="ui teal button m-mobile-wide"><i class="edit icon"></i>发布</button> </div> </div> 复制代码
$('#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}]]*/"",{ "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); clearContent(); }); } 复制代码
<div id="comment-container" class="ui teal segment"> <div th:fragment="commentList"> <div class="ui threaded comments" style="max-width: 100%;"> <h3 class="ui dividing header">评论</h3> <div class="comment" th:each="comment : ${comments}"> <a class="avatar"> <img src="https://unsplash.it/100/100?image=1005" th:src="@{${comment.avatar}}"> </a> <div class="content"> <a class="author" > <span th:text="${comment.nickname}">Matt</span> <div class="ui mini basic teal left pointing label m-padded-mini" th:if="${comment.adminComment}">栈主</div> </a> <div class="metadata"> <span class="date" th:text="${#dates.format(comment.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span> </div> <div class="text" th:text="${comment.content}"> How artistic! </div> <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" href="#" th:href="@{/comment/{param1}/{param2}/delete(param1=${comment.blogId},param2=${comment.id})}" onclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a> <!--<a class="delete" href="#" th:href="@{/comment/{id}/delete(id=${comment.id})}" onclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a>--> </div> </div> <!--子集评论--> <div class="comments" th:if="${#arrays.length(comment.replyComments)}>0"> <div class="comment" th:each="reply : ${comment.replyComments}"> <a class="avatar"> <img src="https://unsplash.it/100/100?image=1005" th:src="@{${reply.avatar}}"> </a> <div class="content"> <a class="author" > <span th:text="${reply.nickname}">小红</span> <div class="ui mini basic teal left pointing label m-padded-mini" th:if="${reply.adminComment}">栈主</div> <span th:text="|@ ${reply.parentNickname}|" class="m-teal">@ 小白</span> </a> <div class="metadata"> <span class="date" th:text="${#dates.format(reply.createTime,'yyyy-MM-dd HH:mm')}">Today at 5:42PM</span> </div> <div class="text" th:text="${reply.content}"> How artistic! </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="#" th:href="@{/comment/{param1}/{param2}/delete(param1=${reply.blogId},param2=${reply.id})}" onclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a> <!--<a class="delete" href="#" th:href="@{/comment/{id}/delete(id=${reply.id})}" onclick="return confirm('确定要删除该评论吗?三思啊! 删了可就没了!')" th:if="${session.user}">删除</a>--> </div> </div> </div> </div> </div> </div> </div> </div> 复制代码
运行项目,访问 http://localhost:8080/, 点击一片文章,可以查看文章信息,并能进行评论,登录后可以对评论进行删除
至此,Springboot搭建个人博客详情页面显示开发完成,由于留言功能和评论功能基本一样,之前也写过一篇有关评论的博客 (SpringBoot和Mybatis实现评论楼中楼功能(一张表搞定)) ,所以这里就不讲留言功能的开发了,还有不懂的伙伴可以问我,也可以加群讨论,下一篇就直接讲述分类、时间轴、音乐盒、友人帐、照片墙关于我页面的显示,比较简单,直接在一篇文章中讲完
【点关注,不迷路,欢迎持续关注本站】