八、博客管理系统
- 创建新的SpringBoot项目,综合运用以上知识点,做一个文章管理的后台应用。
- 依赖:
- Spring Web
- Lombok
- Thymeleaf
- MyBatis Framework
- MySQL Driver
- Bean Validation
- hutool
- 需求:文章管理工作,发布新文章,编辑文章,查看文章内容等
8.1配置文件
1.组织配置文件
app-base.yml
article:
#最低阅读数
low-read: 10
#首页显示记录的数量
top-read: 20
db.properties(一定要用properties)
#配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=030522
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=10
#获取连接时,检测语句
spring.datasource.hikari.connection-test-query=select 1
spring.datasource.hikari.connection-timeout=20000
#其他属性
spring.datasource.hikari.data-source-properties.cachePrepStmts=true
spring.datasource.hikari.data-source-properties.dataSource.cachePrepStmtst=true
spring.datasource.hikari.data-source-properties.dataSource.prepStmtCacheSize=250
spring.datasource.hikari.data-source-properties.dataSource.prepStmtCacheSqlLimit=2048
spring.datasource.hikari.data-source-properties.dataSource.useServerPrepStmts=true
<img src=“C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230902103338099.png”
8.2视图文件
2.logo文件
- favicon.ico 放在 static/ 根目录下
3.创建模板页面
articleList.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon"/>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div style="margin-left: 200px">
<h3>阅读最多的20篇文章</h3>
<table border="1px" cellpadding="0px" cellspacing="0px">
<thead>
<th>选择</th>
<th>序号</th>
<th>标题</th>
<th>副标题</th>
<th>已读数</th>
<th>发布时间</th>
<th>最后修改时间</th>
<th>操作</th>
</thead>
<tbody>
<tr th:each="article , loopStatus : ${articleList}">
<td><input type="checkbox" th:value="${article.id}"></td>
<td th:text="${loopStatus.index + 1}"></td>
<td th:text="${article.title}"></td>
<td th:text="${article.summary}"></td>
<td th:text="${article.readCount}"></td>
<td th:text="${article.createTime}"></td>
<td th:text="${article.updateTime}"></td>
<td><a th:href="@{/article/get(id=${article.id})}">编辑文章</a> </td>
</tr>
<tr>
<td colspan="8">
<table width="100%">
<tr>
<td><button id="add" onclick="addArticle()">发布新文章</button></td>
<td><button id="del" onclick="deleteArticle()">删除文章</button></td>
<td><button id="view" onclick="overView()">文章概览</button></td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<form id="frm" th:action="@{/view/addArticle}" method="get"></form>
<form id="delFrm" th:action="@{/article/remove}" method="post">
<input type="hidden" name="ids" id="ids" value="">
</form>
</div>
<script>
function addArticle(){
$("#frm").submit();
}
function deleteArticle(){
var ids=[];
$("input[type='checkbox']:checked").each((index,item)=>{
ids.push(item.value);
})
if (ids.length==0){
alert("请选择文章");
return;
}
$("#ids").val(ids);
$("#delFrm").submit();
}
function overView(){
var ids = [];
$("input[type='checkbox']:checked").each( (index,item) =>{
ids.push(item.value);
})
if( ids.length != 1){
alert("请选择一个文章查看");
return;
}
$.get("../article/detail/overview",{id: ids[0]}, (data,status)=>{
alert(data);
})
}
</script>
</body>
</html>
addArticle.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon"/>
</head>
<body>
<div style="margin-left: 200px">
<h3>发布新的文章</h3>
<form th:action="@{/article/add}" method="post">
<table>
<tr>
<td>标题</td>
<td><input type="text" name="title"></td>
</tr>
<tr>
<td>副标题</td>
<td><input type="text" name="summary" size="50"></td>
</tr>
<tr>
<td>内容</td>
<td><textarea name="content" cols="60" rows="20"></textarea></td>
</tr>
</table>
<br/>
<input type="submit" value="发布新文章" style="margin-left: 200px">
</form>
</div>
</body>
</html>
editArticle.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon"/>
</head>
<body>
<div style="margin-left: 200px">
<h3>修改文章</h3>
<form th:action="@{/article/edit}" method="post">
<table>
<tr>
<td>标题</td>
<td><input type="text" name="title" th:value="${article.title}"></td>
</tr>
<tr>
<td>副标题</td>
<td><input type="text" name="summary" size="50" th:value="${article.summary}"></td>
</tr>
<tr>
<td>内容</td>
<td><textarea name="content" cols="60" rows="20" th:text="${article.content}"></textarea></td>
</tr>
</table>
<br/>
<input type="hidden" th:value="${article.id}" name="id">
<input type="submit" value="确认修改" style="margin-left: 200px">
</form>
</div>
</body>
</html>
bind.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon"/>
</head>
<body>
<h3>输入异常</h3>
<table border="1px">
<thead>
<th>字段</th>
<th>描述</th>
</thead>
<tbody>
<tr th:each="err:${errors}">
<td th:text="${err.field}"></td>
<td th:text="${err.defaultMessage}"></td>
</tr>
</tbody>
</table>
</body>
</html>
error.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="icon" th:href="@{/favicon.ico}" type="image/x-icon"/>
</head>
<body>
<h3>请求错误</h3>
<h3 th:text="${msg}"></h3>
</body>
</html>
8.3Java代码
4.java代码
model包
ArticleDTO
package com.hhb.blog.model.dto;
import lombok.Data;
@Data
public class ArticleDTO {
private Integer id;
private String title;
private String summary;
private String content;
}
ArticleAndDetailMap
package com.hhb.blog.model.map;
import lombok.Data;
@Data
public class ArticleAndDetailMap {
private Integer id;
private String title;
private String summary;
private String content;
}
ArticleParam
package com.hhb.blog.model.param;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class ArticleParam {
//使用JSR303注解
public static interface AddArticle{};
public static interface EditArticle{};
@NotNull(message = "修改时必须有id",groups = {EditArticle.class})
@Min(value = 1,message = "文章id大于{value}",groups = {EditArticle.class})
private Integer id;
@NotBlank(message = "请输入文章标题",groups = {AddArticle.class,EditArticle.class})
@Size(min = 2,max = 20,message = "文章标题在{min}-{max}",groups = {AddArticle.class,EditArticle.class})
private String title;
@NotBlank(message = "请输入文章副标题",groups = {AddArticle.class,EditArticle.class})
@Size(min = 10,max=30,message = "文章副标题在{min}-{max}",groups = {AddArticle.class,EditArticle.class})
private String summary;
@NotBlank(message = "请输入文章内容",groups = {AddArticle.class,EditArticle.class})
@Size(min = 20,max = 8000,message = "文章内容至少{min}字,最多{max}字",groups = {AddArticle.class,EditArticle.class})
private String content;
}
ArticleDeatilPO
package com.hhb.blog.model.po;
import lombok.Data;
@Data
public class ArticleDetailPO {
private Integer id;
private Integer articleId;
private String content;
}
ArticlePO
package com.hhb.blog.model.po;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ArticlePO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
ArticleVO
package com.hhb.blog.model.po;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ArticlePO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
mapper包
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
ArticleMapper
package com.hhb.blog.mapper;
import com.hhb.blog.model.dto.ArticleDTO;
import com.hhb.blog.model.map.ArticleAndDetailMap;
import com.hhb.blog.model.po.ArticleDetailPO;
import com.hhb.blog.model.po.ArticlePO;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface ArticleMapper {
//查询首页需要的文章列表
@Select("""
select id,user_id ,title,summary, read_count , create_time, update_time
from article
where read_count >= #{lowRead}
order by read_count desc
limit #{topRead}
""")
List<ArticlePO> topSortByReadCount(Integer lowRead, Integer topRead);
//添加文章
@Insert("""
insert into article(user_id, title, summary, read_count, create_time, update_time)
values(#{userId},#{title},#{summary},#{readCount},#{createTime},#{updateTime})
""")
//主键自增
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int insertArticle(ArticlePO articlePO);
//添加文章内容
@Insert("""
insert into article_detail(article_id, content)
values(#{articleId},#{content})
""")
int insertArticleDetail(ArticleDetailPO articleDetailPO);
//两表连接,根据主键查询文章
@Select("""
select article.id,title,summary,content
from article,article_detail
where article.id=article_detail.article_id and article_id=#{id}
""")
ArticleAndDetailMap selectArticleAndDetail(Integer id);
//修改文章属性
@Update("""
update article
set title=#{title},summary=#{summary},update_time=#{updateTime}
where id=#{id}
""")
int updateArticle(ArticlePO articlePO);
//更新文章内容
@Update("""
update article_detail set content=#{content} where article_id=#{articleId}
""")
int updateArticleDetail(ArticleDetailPO articleDetailPO);
//删除文章
@Delete("""
<script>
delete from article where id in
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</script>
""")
int deleteArticle(List<Integer> idList);
//删除文章内容
@Delete("""
<script>
delete from article_detail where article_id in
<foreach item="id" collection="idList" open="(" separator="," close=")">
#{id}
</foreach>
</script>
""")
int deleteDetail(List<Integer> idList);
//根据id查询内容
@Select("""
select id,article_id,content from article_detail
where article_id = #{articleId}
""")
ArticleDetailPO selectArticleDetailByArticleId(Integer articleId);
}
service包
ArticleService
package com.hhb.blog.service;
import com.hhb.blog.model.dto.ArticleDTO;
import com.hhb.blog.model.po.ArticlePO;
import java.util.List;
public interface ArticleService {
//获取首页文章列表
List<ArticlePO> queryTopArticle();
//发布文章(article,article_detail)
boolean addArticle(ArticleDTO articleDTO);
//根据主键查询文章
ArticleDTO queryByArticleId(Integer id);
//修改文章属性和内容
boolean modifyArticle(ArticleDTO articleDTO);
//删除文章
boolean removeArticle(List<Integer> idList);
//查询文章内容前20个字符
String queryTop20Content(Integer id);
}
ArticleServiceImpl
package com.hhb.blog.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.hhb.blog.mapper.ArticleMapper;
import com.hhb.blog.model.dto.ArticleDTO;
import com.hhb.blog.model.map.ArticleAndDetailMap;
import com.hhb.blog.model.po.ArticleDetailPO;
import com.hhb.blog.model.po.ArticlePO;
import com.hhb.blog.service.ArticleService;
import com.hhb.blog.settings.ArticleSettings;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Random;
@Service
@RequiredArgsConstructor
public class ArticleServiceImpl implements ArticleService {
private final ArticleMapper articleMapper;
private final ArticleSettings articleSettings;
//构造注入
/* public ArticleServiceImpl(ArticleMapper articleMapper) {
this.articleMapper = articleMapper;
}*/
@Override
public List<ArticlePO> queryTopArticle() {
Integer lowRead = articleSettings.getLowRead();
Integer topRead = articleSettings.getTopRead();
return articleMapper.topSortByReadCount(lowRead, topRead);
}
//发布文章
@Transactional(rollbackFor = Exception.class)
@Override
public boolean addArticle(ArticleDTO articleDTO) {
//文章
ArticlePO articlePO = new ArticlePO();
articlePO.setTitle(articleDTO.getTitle());
articlePO.setSummary(articleDTO.getSummary());
articlePO.setCreateTime(LocalDateTime.now());
articlePO.setUpdateTime(LocalDateTime.now());
articlePO.setReadCount(new Random().nextInt(1000));
articlePO.setUserId(new Random().nextInt(5000));
int addArticle = articleMapper.insertArticle(articlePO);
//文章内容
ArticleDetailPO articleDetailPO = new ArticleDetailPO();
articleDetailPO.setArticleId(articlePO.getId());
articleDetailPO.setContent(articleDTO.getContent());
int addDetail = articleMapper.insertArticleDetail(articleDetailPO);
return (addDetail + addArticle) == 2 ? true : false;
}
@Override
public ArticleDTO queryByArticleId(Integer id) {
//文章属性,内容
ArticleAndDetailMap mapper = articleMapper.selectArticleAndDetail(id);
//转为DTO,两种方式
/*ArticleDTO articleDTO = new ArticleDTO();
articleDTO.setTitle(mapper.getTitle());
articleDTO.setContent(mapper.getContent());
articleDTO.setSummary(mapper.getSummary());
articleDTO.setId(mapper.getId());*/
ArticleDTO articleDTO = BeanUtil.copyProperties(mapper, ArticleDTO.class);
return articleDTO;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean modifyArticle(ArticleDTO articleDTO) {
//修改文章属性
ArticlePO articlePO = new ArticlePO();
articlePO.setTitle(articleDTO.getTitle());
articlePO.setSummary(articleDTO.getSummary());
articlePO.setUpdateTime(LocalDateTime.now());
articlePO.setId(articleDTO.getId());
int article = articleMapper.updateArticle(articlePO);
//修改文章内容
ArticleDetailPO articleDetailPO = new ArticleDetailPO();
articleDetailPO.setArticleId(articleDTO.getId());
articleDetailPO.setContent(articleDTO.getContent());
int detail = articleMapper.updateArticleDetail(articleDetailPO);
return (article + detail) == 2 ? true : false;
}
//删除文章属性、内容
@Transactional(rollbackFor = Exception.class)
@Override
public boolean removeArticle(List<Integer> idList) {
int article = articleMapper.deleteArticle(idList);
int detail = articleMapper.deleteDetail(idList);
return article == detail ? true : false;
}
//查询文章内容前20个字符
@Override
public String queryTop20Content(Integer id) {
ArticleDetailPO articleDetailPO = articleMapper.selectArticleDetailByArticleId(id);
String content = "无内容";
if( articleDetailPO != null ){
content = articleDetailPO.getContent();
if(StringUtils.hasText(content)){
//content = content.substring(0, content.length() >= 20 ? 20 : content.length() );
content = content.substring(0, 20 );
}
}
return content;
}
}
controller包
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
ArticleController
package com.hhb.blog.controller;
import cn.hutool.core.bean.BeanUtil;
import com.hhb.blog.format.IdType;
import com.hhb.blog.handler.exp.IdTypeException;
import com.hhb.blog.model.dto.ArticleDTO;
import com.hhb.blog.model.param.ArticleParam;
import com.hhb.blog.model.po.ArticlePO;
import com.hhb.blog.model.vo.ArticleVO;
import com.hhb.blog.service.ArticleService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class ArticleController {
private final ArticleService articleService;
@GetMapping(value = {"/", "/article/hot"})
public String showHotArticle(Model model) {
List<ArticlePO> articlePOList = articleService.queryTopArticle();
//转为VO .hutool工具类
List<ArticleVO> articleVOList = BeanUtil.copyToList(articlePOList, ArticleVO.class);
//添加数据
model.addAttribute("articleList", articleVOList);
//视图
return "/blog/articleList";
}
//发布新文章
@PostMapping("/article/add")
//接收对象类型参数
public String addArticle(@Validated(ArticleParam.AddArticle.class) ArticleParam param) {
ArticleDTO articleDTO = new ArticleDTO();
articleDTO.setContent(param.getContent());
articleDTO.setSummary(param.getSummary());
articleDTO.setTitle(param.getTitle());
boolean add = articleService.addArticle(articleDTO);
return "redirect:/article/hot";
}
//查询文章内容
@GetMapping("/article/get")
public String queryById(Integer id, Model model) {
if (id != null && id > 0) {
ArticleDTO articleDTO = articleService.queryByArticleId(id);
//DTO-VO
ArticleVO articleVO = BeanUtil.copyProperties(articleDTO, ArticleVO.class);
//添加数据
model.addAttribute("article", articleVO);
//视图
return "/blog/editArticle";
} else {
return "/blog/error/error";
}
}
//更新文章
@PostMapping("/article/edit")
public String modifyArticle(@Validated(ArticleParam.EditArticle.class) ArticleParam param) {
/*ArticleDTO articleDTO = new ArticleDTO();
articleDTO.setId(param.getId());
articleDTO.setTitle(param.getTitle());
articleDTO.setSummary(param.getSummary());
articleDTO.setContent(param.getContent());*/
ArticleDTO articleDTO = BeanUtil.copyProperties(param, ArticleDTO.class);
boolean edit = articleService.modifyArticle(articleDTO);
return "redirect:/article/hot";
}
//删除文章
@PostMapping("/article/remove")
//public String removeArticle(Integer ids[])
public String removeArticle(@RequestParam("ids") IdType idType) {
if (idType == null) {
throw new IdTypeException("ID为空");
}
boolean delete = articleService.removeArticle(idType.getIdList());
return "redirect:/article/hot";
}
//预览文章
@GetMapping("/article/detail/overview")
@ResponseBody
public String queryDetail(Integer id) {
String top20Content = "无ID";
if (id != null) {
top20Content = articleService.queryTop20Content(id);
}
return top20Content;
}
}
异常处理包
GlobalExceptionHandler
package com.hhb.blog.handler;
import java.util.List;
import com.hhb.blog.handler.exp.IdTypeException;
import org.springframework.ui.Model;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
//处理JSR303
@ExceptionHandler({BindException.class})
public String handlerBindException(BindException bindException, Model model) {
BindingResult bindingResult = bindException.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
model.addAttribute("errors", fieldErrors);
return "/blog/error/bind";
}
@ExceptionHandler({IdTypeException.class})
public String handleIdTypeException(IdTypeException idTypeException, Model model) {
model.addAttribute("msg", idTypeException.getMessage());
return "/blog/error/error";
}
@ExceptionHandler({Exception.class})
public String handleDefaultException(Exception e, Model model) {
model.addAttribute("msg", "请稍后重试!");
return "/blog/error/error";
}
}
BlogRootException
package com.hhb.blog.handler.exp;
public class BlogRootException extends RuntimeException{
public BlogRootException() {
}
public BlogRootException(String message) {
super(message);
}
}
IdTypeException
package com.hhb.blog.handler.exp;
public class IdTypeException extends BlogRootException {
public IdTypeException() {
}
public IdTypeException(String message) {
super(message);
}
}
数据格式化包
IdType
package com.hhb.blog.format;
import lombok.Data;
import java.util.List;
@Data
public class IdType {
private List<Integer> idList;
}
IdTypeFormatter
package com.hhb.blog.format;
import org.springframework.format.Formatter;
import org.springframework.util.StringUtils;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class IdTypeFormatter implements Formatter<IdType> {
@Override
public IdType parse(String text, Locale locale) throws ParseException {
IdType idType = null;
if (StringUtils.hasText(text)) {
List<Integer> ids = new ArrayList<>();
for (String id : text.split(",")) {
ids.add(Integer.parseInt(id));
}
idType = new IdType();
idType.setIdList(ids);
}
return idType;
}
@Override
public String print(IdType object, Locale locale) {
return null;
}
}
设置包
ArticleSettings
package com.hhb.blog.settings;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "article")
public class ArticleSettings {
private Integer lowRead;
private Integer topRead;
}
MvcSettings
package com.hhb.blog.settings;
import com.hhb.blog.format.IdTypeFormatter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcSettings implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/view/addArticle").setViewName("/blog/addArticle");
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new IdTypeFormatter());
}
}
启动类
package com.hhb.blog;
import com.hhb.blog.settings.ArticleSettings;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement
@MapperScan(basePackages = "com.hhb.blog.mapper")
@EnableConfigurationProperties({ArticleSettings.class})
@SpringBootApplication
public class Springboot20BlogAdminApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot20BlogAdminApplication.class, args);
}
}