实训——个人博客
这次实训项目——个人博客,主要采用SpringBoot+semantic-ui
视频链接
@
一.页面编写
页面采用Semantic-ui框架编写
包括前台博客展示页面和后台博客管理页面两部分
- 前台博客展示:
![在这里插入图片描述]()
- 后台博客管理页面
![在这里插入图片描述]()
同时也引入了以下插件: - animate :引入动画
- editotmd : Markdown插件
- prism : 代码高亮
- qrcode : 网址二维码生成
- tocbot : 文章目录生成
部分截图:
首页

博客管理
博客发布

二.项目搭建
1.创建项目


2.配置项目
修改thymeleaf:
<properties>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>
application.yml中更改模式:
thymeleaf:
mode: HTML
配置数据库:
spring:
datasource:
url: jdbc:mysql://localhost:3306/blog?serverTimezone=UTC
username: root
password: guan37
jpa:
hibernate:
ddl-auto: update
show-sql: true
异常处理:
新建异常页面

package net.ty.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
/**
* @date 2021年06月16日 10:08
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest request, Exception e) throws Exception {
logger.error("Requst URL : {}, Exception : {}", request.getRequestURI(),e);
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}
ModelAndView mv = new ModelAndView();
mv.addObject("url",request.getRequestURL());
mv.addObject("exception", e);
mv.setViewName("error/error");
return mv;
}
}

3.页面设置
将页面引入thymeleaf模板同时博客页面的头部和尾部都是一样的,这些共同的部分设置为_framents模板
_framents.html
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title th:text="${title}">_fragments</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css">
<link rel="stylesheet" href="../static/css/typo.css" th:href="@{/static/css/typo.css}">
<link rel="stylesheet" href="../static/css/animate.css" th:href="@{/static/css/animate.css}">
<link rel="stylesheet" href="../static/lib/prism/prism.css" th:href="@{/static/lib/prism/prism.css}">
<link rel="stylesheet" href="./static/lib/prism/tocbot.css" th:href="@{/static/lib/tocbot/tocbot.css}">
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/static/css/me.css}">
<!-- 导航 -->
<nav th:fragment="menu(n)" class="ui inverted attached segment m-shadow-small m-padded-tb-mini">
<div class="ui container">
<div class="ui inverted secondary stackable menu">
<h2 class="ui teal header item">Blog</h2>
<a href="#" th:href="@{/}" class="m-item item m-mobile-hide" th:classappend="${n==1} ? 'active'"><i class="home icon"></i> 首页</a>
<a href="#" th:href="@{/types/-1}" class="m-item item m-mobile-hide" th:classappend="${n==2} ? 'active'"><i class="idea icon"></i> 分类</a>
<a href="#" th:href="@{/tags/-1}" class="m-item item m-mobile-hide" th:classappend="${n==3} ? 'active'"><i class="tags icon"></i> 标签</a>
<a href="#" th:href="@{/archives}" class="m-item item m-mobile-hide" th:classappend="${n==4} ? 'active'"><i class="clone icon"></i> 归档</a>
<a href="#" th:href="@{/about}" class="m-item item m-mobile-hide" th:classappend="${n==5} ? 'active'"><i class="info icon"></i> 关于</a>
<div class="right m-item item m-mobile-hide">
<form name="search" action="#" th:action="@{/search}" method="post" target="_blank">
<div class="ui icon inverted transparent input m-margin-tb-tiny">
<input type="text" name="query" placeholder="Search...." th:value="${query}">
<i onclick="document.forms['search'].submit()" class="search link icon"></i>
</div>
</form>
</div>
</div>
</div>
<a href="#" class="ui menu toggle black icon button m-top-right m-mobile-show">
<i class="sidebar icon"></i>
</a>
</nav>
<!-- 底部 -->
<footer th:fragment="footer" class="ui inverted vertical segment m-padded-tb-massive">
<div class="ui center aligned container">
<div class="ui inverted divided stackable grid">
<div class="three wide column">
<div class="ui inverted link list">
<div class="item">
<img src="../static/images/QR_csdn.png" th:src="@{/static/images/QR_csdn.png}" class="ui rounded image" alt="" style="width: 110px">
</div>
</div>
</div>
<div class="three wide column">
<h4 class="ui inverted header">最新博客</h4>
<div id="newblog-container">
<div class="ui inverted link list" th:fragment="newblogList">
<a href="#" th:href="@{/blog/{id}(id=${blog.id})}" target="_blank" class="item m-text-thin" th:each="blog : ${newblogs}" th:text="${blog.title}">用户故事(User Story)</a>
<!--/*-->
<a href="#" class="item m-text-thin">用户故事(User Story)</a>
<a href="#" class="item m-text-thin">用户故事(User Story)</a>
<!--*/-->
</div>
</div>
</div>
<div class="three wide column">
<h4 class="ui inverted header">联系我</h4>
<div class="ui inverted link list">
<a href="#" class="item" th:text="#{index.email}">Email:tyaojoy@foxmail.com</a>
<a href="#" class="item" th:text="#{index.qq}">QQ:211163529</a>
</div>
</div>
<div class="seven wide column">
<h4 class="ui inverted header">Blog</h4>
<div class="ui inverted link list">
<p class="m-text-thin m-text-spaced m-opacity-mini">这是我的个人博客、会分享关于编程、写作、思考相关的任何内容,希望可以给来到这儿的人有所帮助...</p>
</div>
</div>
</div>
<div class="ui inverted section divider"></div>
<p class="m-text-thin m-text-spaced m-opacity-tiny">Copyright © 2021 Blog Designed by 观山奇</p>
</div>
</footer>
<!-- script -->
<th:block th:fragment="script">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.js"></script>
<script src="../static/lib/prism/prism.js" th:src="@{/static/lib/prism/prism.js}"></script>
<script src="../static/lib/qrcode/qrcode.min.js" th:src="@{/static/lib/qrcode/qrcode.min.js}"></script>
<script src="../static/lib/tocbot/tocbot.min.js" th:src="@{/static/lib/tocbot/tocbot.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery.scrollto@2.1.3/jquery.scrollTo.min.js"></script>
<script>
$('#newblog-container').load(/*[[@{/footer/newblog}]]*/"/footer/newblog");
</script>
</th:block>
注意:
1.html标签需要加入xmlns:th="http://www.thymeleaf.org"
2.静态文件需要加入th:src="@{/路径/文件名}"
3.因为博客展示页面和后台管理页面需要引入的插件不一样,所有设置了两个模板
_fragment模板设置完成后,其他页面直接引用就可以了

4.实体类
根据老师设计的实体关系分析新建实体类生成数据库
新建实体类

jpa生成数据库

5.博客后台
5.1 登录&首页页面
页面处理都一样分为:dao层,service层(接口,实现),控制器,页面处理
dao:dao/UserRepository
public interface UserRepository extends JpaRepository<user, long=""> {
User findByUsernameAndPassword(String username, String password);
}
服务-接口:service/UserService
public interface UserService {
User checkUser(String username, String password);
}
服务-实现:service/UserServiceImpl 实现
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserRepository userRepository;
@Override
public User checkUser(String username, String password) {
User user = userRepository.findByUsernameAndPassword(username, MD5Utils.code(password));
return user;
}
控制器:web/admin/LoginController:
@Controller
@RequestMapping("/admin")
public class loginController {
@Autowired
private UserService userService;
@GetMapping
public String LoginPage() {
return "admin/login";
}
@PostMapping("/login")
public String login(
@RequestParam String username,
@RequestParam String password,
HttpSession session,
RedirectAttributes attributes) {
User user = userService.checkUser(username, password);
if (user != null) {
user.setPassword(null);
session.setAttribute("user",user);
return "admin/index";
} else {
attributes.addFlashAttribute("message","用户名密码错误");
return "redirect:/admin";
}
}
@GetMapping("/logout")
public String logout(HttpSession session) {
session.removeAttribute("user");
return "redirect:/admin";
}
}
修改页面:
添加表单验证:



MD5加密:
正常情况密码应该以密文形式存储:
util/MD5Utils:
public class MD5Utils {
/**
* MD5加密类
*/
public static String code(String str){
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte[]byteDigest = md.digest();
int i;
StringBuilder buf = new StringBuilder();
for (byte b : byteDigest) {
i = b;
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
//32位加密
return buf.toString();
// 16位的加密
//return buf.toString().substring(8, 24);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
System.out.println(code("123456"));
}
}
登录拦截:
为了防止用户未登录就进入后台管理页面,所以设置了登录拦截:
interceptor/LoginInterceptor:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getSession().getAttribute("user") == null) {
response.sendRedirect("/admin");
return false;
}
return true;
}
}
interceptor/WebConfig:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin") //防止无限循环重定向进入admin
.excludePathPatterns("/admin/login"); //表单提交不能被拦截
}
}

5.2 分类&标签管理页面
博客分类管理和标签管理基本一样,这里只展示标签
public interface TypeRepository extends JpaRepository<type,long> {
Type findByName(String name);
}
public interface TypeService {
Type saveType(Type type);
Type getType(Long id);
Type getTypeName(String name);
Page<type> listType(Pageable pageable);
List<type> listType();
List<type> listTypeTop(Integer size);
Type updateType(Long id, Type type);
void deleteType(Long id);
}
sever实现:需要注意的是findone方法已经失效需要采用getbyid方法
@Service
public class TypeServiceImpl implements TypeService {
@Autowired
private TypeRepository typeRepository;
@Transactional
@Override
public Type saveType(Type type) {
return typeRepository.save(type);
}
@Transactional
@Override
public Type getType(Long id) {
return typeRepository.getById(id);
}
@Override
public Type getTypeName(String name) {
return typeRepository.findByName(name);
}
@Transactional
@Override
public Page<type> listType(Pageable pageable) {
return typeRepository.findAll(pageable);
}
@Override
public List<type> listType() {
return typeRepository.findAll();
}
@Override
public List<type> listTypeTop(Integer size) {
Sort sort = Sort.by(Sort.Direction.DESC, "blogs.size");
Pageable pageable = PageRequest.of(0, size, sort);
return typeRepository.findTop(pageable);
}
@Transactional
@Override
public Type updateType(Long id, Type type) {
Type t = typeRepository.getById(id);
if (t == null) {
throw new NotFoundException("不存在该类型!");
}
BeanUtils.copyProperties(type,t);
return typeRepository.save(t);
}
@Transactional
@Override
public void deleteType(Long id) {
typeRepository.deleteById(id);
}
}
控制器:页面处理步骤大致相同,只是每个页面功能不同,这其中设计重复验证,以及分页处理
@Controller
@RequestMapping("/admin")
public class TypeController {
@Autowired
private TypeService typeService;
@GetMapping("/types")
public String list(@PageableDefault(size = 5,sort = {"id"}, direction = Sort.Direction.DESC)
Pageable pageable, Model model) {
model.addAttribute("page", typeService.listType(pageable));
typeService.listType(pageable);
return "admin/types";
}
@GetMapping("/types/input")
public String input(Model model) {
model.addAttribute("type",new Type());
return "admin/types-input";
}
@GetMapping("/types/{id}/input")
public String editInput(@PathVariable Long id, Model model) {
model.addAttribute("type",typeService.getType(id));
return "admin/types-input";
}
@PostMapping("/types")
public String post(@Valid Type type, BindingResult result , RedirectAttributes attributes) {
/*重复验证*/
Type t1 = typeService.getTypeName(type.getName());
if (t1 != null) {
result.rejectValue("name","nameError","不能添加重复分类");
}
/*非空验证*/
if (result.hasErrors()) {
return "admin/types-input";
}
Type t = typeService.saveType(type);
if (t == null) {
attributes.addFlashAttribute("message","新增失败");
} else {
attributes.addFlashAttribute("message","新增成功");
}
return "redirect:/admin/types";
}
@PostMapping("/types/{id}")
public String editPost(@Valid Type type, BindingResult result, @PathVariable Long id, RedirectAttributes attributes) {
/*重复验证*/
Type t1 = typeService.getTypeName(type.getName());
if (t1 != null) {
result.rejectValue("name","nameError","不能添加重复分类");
}
/*非空验证*/
if (result.hasErrors()) {
return "admin/types-input";
}
Type t = typeService.updateType(id,type);
if (t == null) {
attributes.addFlashAttribute("message","更新失败");
} else {
attributes.addFlashAttribute("message","更新成功");
}
return "redirect:/admin/types";
}
@GetMapping("/types/{id}/delete")
public String delete(@PathVariable Long id, RedirectAttributes attributes) {
typeService.deleteType(id);
attributes.addFlashAttribute("message","删除成功");
return "redirect:/admin/types";
}
}
页面处理 页面处理主要就是thymeleaf语法的使用,就不过多介绍,这是我找的一篇中文文档

5.3 博客管理页面
博客页面管理也与标签分类类似,只是多了查询相关内容


6.博客前台展示
6.1首页
首页需要处理的就是页面的跳转,因为他全篇都是需要跳转的链接.
同时还有数据的获取:标签,分类,文章……这些数据的获取在后面的页面展示也会使用到.
还有就是博客点击后,浏览数要+1.

6.2 博客展示
博客展示主要就是md-html的转换,然后还有评论
markdown转换为html
porm: 添加依赖
<!--引入atlassian(将markdown形式转成html格式的)-->
<dependency>
<groupid>com.atlassian.commonmark</groupid>
<artifactid>commonmark</artifactid>
<version>0.14.0</version>
</dependency>
<dependency>
<groupid>com.atlassian.commonmark</groupid>
<artifactid>commonmark-ext-gfm-tables</artifactid>
<version>0.14.0</version>
</dependency>
<dependency>
<groupid>com.atlassian.commonmark</groupid>
<artifactid>commonmark-ext-heading-anchor</artifactid>
<version>0.14.0</version>
</dependency>
markdownUtils
public class MarkdownUtils {
/**
* markdown格式转换成HTML格式
*/
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);
}
/**
* 增加扩展[标题锚点,表格生成]
*/
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");
}
}
}
}
接口实现:
@Transactional
@Override
public Blog getAndConvert(Long id) {
Blog blog = blogRepository.getById(id);
if (blog == null) {
throw new NotFoundException("该博客不存在");
}
Blog b = new Blog();
BeanUtils.copyProperties(blog,b);
String content = b.getContent();
b.setContent(MarkdownUtils.markdownToHtml(content));
blogRepository.updateViews(id);
return b;
}
评论
评论可能是这个项目最难的一部分,其中还设计到了很多数据结构知识
dao
public interface CommentRepository extends JpaRepository<comment, long=""> {
List<comment> findByBlogId(Long blogId, Sort sort);
List<comment> findByBlogIdAndParentCommentNull(Long blogId, Sort sort);
}
服务-接口
public interface CommentService {
List<comment> listCommentByBlogId(Long blogId);
Comment saveComment(Comment comment);
}
服务-实现:
@Service
public class CommentServiceImpl implements CommentService {
@Autowired
private CommentRepository commentRepository;
@Override
public List<comment> listCommentByBlogId(Long blogId) {
Sort sort = Sort.by("createTime");
List<comment> comments = commentRepository.findByBlogIdAndParentCommentNull(blogId,sort);
return eachComment(comments);
}
@Transactional
@Override
public Comment saveComment(Comment comment) {
Long parentCommentId = comment.getParentComment().getId();
if (parentCommentId != -1) {
comment.setParentComment(commentRepository.getById(parentCommentId));
} else {
comment.setParentComment(null);
}
comment.setCreateTime(new Date());
return commentRepository.save(comment);
}
/* 树 */
private List<comment> eachComment(List<comment> comments) {
List<comment> commentsView = new ArrayList<>();
for (Comment comment : comments) {
Comment c = new Comment();
BeanUtils.copyProperties(comment,c);
commentsView.add(c);
}
//合并评论的各层子代到第一级子代集合中
combineChildren(commentsView);
return commentsView;
}
private void combineChildren(List<comment> comments) {
for (Comment comment : comments) {
List<comment> replys1 = comment.getReplyComments();
for(Comment reply1 : replys1) {
//循环迭代,找出子代,存放在tempReplys中
recursively(reply1);
}
//修改顶级节点的reply集合为迭代处理后的集合
comment.setReplyComments(tempReplys);
//清除临时存放区
tempReplys = new ArrayList<>();
}
}
//存放迭代找出的所有子代的集合
private List<comment> tempReplys = new ArrayList<>();
private void recursively(Comment comment) {
tempReplys.add(comment);//顶节点添加到临时存放集合
if (comment.getReplyComments().size()>0) {
List<comment> replys = comment.getReplyComments();
for (Comment reply : replys) {
tempReplys.add(reply);
if (reply.getReplyComments().size()>0) {
recursively(reply);
}
}
}
}
}
页面处理:
<div class="ui bottom attached segment" th:if="${blog.commentabled}">
<!--评论列表-->
<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://picsum.photos/id/1/100/100" th:src="@{${comment.avatar}}">
</a>
<div class="content">
<a class="author">
<span th:text="${comment.nickname}">G37</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 are you
</div>
<div class="actions">
<a class="reply" data-commentid="1" data-commentnickname="G37" 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)}>0">
<div class="comment" th:each="reply : ${comment.replyComments}">
<a class="avatar">
<img src="https://picsum.photos/id/1/100/100" 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.parentComment.nickname}|" 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="G37" th:attr="data-commentid=${reply.id},data-commentnickname=${reply.nickname}" onclick="reply(this)">回复</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="comment-form" class="ui form">
<input type="hidden" name="blog.id" 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>
</div>
</div>


6.3 分类&标签

6.4 博客归档
因为添加的测试文档都是一年的,我修改了其中一遍为2020,已达到展示效果,实现只是新建了两个查询语句,能够实现按年份分组

6. 关于
关于页面只是之前的静态页面




浙公网安备 33010602011771号