目录
第八章 文章管理模块
创建新的Spring Boot项目,综合运用视频中的知识点,做一个文章管理的后台应用。 新的Spring Boot项目Lession20-BlogAdmin。Maven构建工具,包名称com.bjpowernode.blog JDK19,依赖:
- Spring Web
- Lombok
- Thymeleaf
- MyBatis Framework
- MySQL Driver
依赖还需要Bean Validation 需求:文章管理工作,发布新文章,编辑文章,查看文章内容等
8.1 配置文件
组织配置文件
app-base.yml
article:
#最低文章阅读数量
low-read: 10
#首页显示最多的文章数量
top-read: 20
db.yml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blog?serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
auto-commit: true
maximum-pool-size: 10
minimum-idle: 10
#获取连接时,检测语句
connection-test-query: select 1
connection-timeout: 20000
#其他属性
data-source-properties:
cachePrepStmts: true
dataSource.cachePrepStmtst: true
dataSource.prepStmtCacheSize: 250
dataSource.prepStmtCacheSqlLimit: 2048
dataSource.useServerPrepStmts: true
8.2 视图文件
logo文件
favicon.ico放在static/ 根目录下
创建模板页面
articleList.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文章列表</title>
<link rel="icon" 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>阅读最多的前10篇文章</h3>
<table border="1px" cellspacing="0px" cellpadding="2px">
<thead>
<th>选择</th>
<th>序号</th>
<th>标题</th>
<th>副标题</th>
<th>已读数量</th>
<th>发布时间</th>
<th>最后修改时间</th>
<th>编辑</th>
</thead>
<tbody>
<tr th:each="article,loopStats : ${articleList}">
<td><input type="checkbox" th:value="${article.id}"></td>
<td th:text="${loopStats.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="delete" onclick="deleteArticle()">删除文章</button></td>
<td><button id="read" onclick="readOverview()">文章概览</button></td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<form id="frm" th:action="@{/view/addArticle}" method="get">
</form>
<form id="delfrm" th:action="@{/article/removeArticle}" method="post">
<input type="hidden" id="idsDom" name="ids" value="" >
</form>
</div>
<script type="text/javascript">
function addArticle(){
$("#frm").submit();
}
function deleteArticle(){
var ids = [];
$("input[type='checkbox']:checked").each( (index,item)=>{
ids.push( item.value );
})
$("#idsDom").val(ids);
$("#delfrm").submit();
}
function readOverview(){
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>
bind.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="margin-left: 200px">
<div th:each="field:${errors}">
<div th:text="${field.field}"></div>
<div th:text="${field.defaultMessage}"></div>
</div>
</div>
</body>
</html>
error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="margin-left: 200px">
<div th:text="${error}"></div>
</div>
</body>
</html>
8.3 Java代码
java代码
model包:
ArticleVO.java
@Data
public class ArticleVO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private String content;
private Integer readCount;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
ArticleParam.java
package com.bjpowernode.blog.model.param;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.awt.PageAttributes.MediaType;
import lombok.Data;
import lombok.NonNull;
import org.hibernate.validator.constraints.Range;
@Datapublic class ArticleParam {
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 = 50,max = 8000,message = "文章至少五十个字,文章至多八千字",groups ={ AddArticle.class, EditArticle.class })
private String content;
}
ArticleDTO.java
@Data
public class ArticleDTO {
private Integer id;
private Integer userId;
private String title;
private String summary;
private Integer readCount;
private String content;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
po包
ArticlePO.java
@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;
}
ArticleDetailPO.java
@Data
public class ArticleDetailPO {
private Integer id;
private Integer articleId;
private String content;
}
mapper包
ArticleMapper.java
package com.bjpowernode.blog.mapper;
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}
""")
@Results(id = "ArticleBaseMap", value = {
@Result(id = true, column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "title", property = "title"),
@Result(column = "summary", property = "summary"),
@Result(column = "read_count", property = "readCount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "update_time", property = "updateTime"),
})
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 m.id as articleId,title,summary,content
from article m left join article_detail ad
on m.id = ad.article_id
where m.id=#{id}
""")
@Results({
@Result(id = true, column = "articleId", property = "id"),
@Result(column = "title", property = "title"),
@Result(column = "summary", property = "summary"),
@Result(column = "content", property = "content", jdbcType = JdbcType.LONGVARCHAR, javaType = String.class)
})
ArticleDTO selectArticleAndDetail(Integer id);
//更新文章title,summary
@Update("""
update article set title=#{title},summary=#{summary} where id=#{id}
""")
int updateArticle(ArticlePO articlePO);
@Update("""
update article_detail set content=#{content} where article_id=#{articleId}
""")
int updateArticleDetail(ArticleDetailPO articleDetailPO);
//<script>动态sql</script>
@Delete("""
<script>
delete from article where id in
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</script>
""")
int deleteArticle(List<Integer> ids);
@Delete("""
<script>
delete from article_detail where id in
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</script>
""")
int deleteArticleDetail(List<Integer> ids);
@Select("""
select id,article_id,content from article_detail
where article_id= #{id}
""")
ArticleDetailPO selectDetailByArticleId(Integer id);
}
ArticleService.java
public interface ArticleService {
List<ArticlePO> queryTopAritcle();
boolean addArticle(ArticleDTO article);
boolean modifyArticle(ArticleParam param);
int removeArticle(List<Integer> ids);
ArticleDTO queryByArticleId(Integer id);
String queryTop20Detail(Integer id);
}
ArticleServiceImpl.java
@RequiredArgsConstructor
@Service
public class ArticleServiceImpl implements ArticleService {
private final ArticleMapper articleMapper;
private final ArticleSettings articleSettings;
@Override
public List<ArticlePO> queryTopAritcle() {
Integer lowRead = articleSettings.getLowRead();
Integer topRead = articleSettings.getTopRead();
return articleMapper.topSortByReadCount(lowRead, topRead);
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean addArticle(ArticleDTO article) {
ArticlePO articlePO = new ArticlePO();
articlePO.setTitle(article.getTitle());
articlePO.setSummary(article.getSummary());
//从登陆信息中获取,现在给个默认
articlePO.setUserId(new Random().nextInt(1000));
articlePO.setReadCount(new Random().nextInt(50));
articlePO.setCreateTime(LocalDateTime.now());
articlePO.setUpdateTime(LocalDateTime.now());
articleMapper.insertArticle(articlePO);
ArticleDetailPO articleDetailPO = new ArticleDetailPO();
articleDetailPO.setArticleId(articlePO.getId());
articleDetailPO.setContent(article.getContent());
articleMapper.insertArticleDetail(articleDetailPO);
return true;
}
@Transactional(rollbackFor = Exception.class)
public boolean modifyArticle(ArticleParam param){
ArticlePO articlePO = new ArticlePO();
articlePO.setId(param.getId());
articlePO.setTitle(param.getTitle());
articlePO.setSummary(param.getSummary());
int editArticle = articleMapper.updateArticle(articlePO);
ArticleDetailPO detailPO = new ArticleDetailPO();
detailPO.setArticleId(param.getId());
detailPO.setContent(param.getContent());
int editDetail = articleMapper.updateArticleDetail(detailPO);
if( editArticle > 0 && editDetail > 0 ){
return true;
}
return false;
}
@Transactional(rollbackFor = Exception.class)
@Override
public int removeArticle(List<Integer> ids) {
int master = articleMapper.deleteArticle(ids);
int detail = articleMapper.deleteArticleDetail(ids);
return master;
}
@Override
public ArticleDTO queryByArticleId(Integer id) {
return articleMapper.selectArticleAndDetail(id);
}
@Override
public String queryTop20Detail(Integer id) {
ArticleDetailPO articleDetailPO = articleMapper.selectDetailByArticleId(id);
String content = articleDetailPO.getContent();
if(StringUtils.hasText(content)){
content = content.substring(0, content.length() >=20 ? 20 : content.length());
}
return content;
}
}
ArticleController.java
@RequiredArgsConstructor
@Controller
public class ArticleController {
private final ArticleService articleService;
@GetMapping( value = {"/", "/article/hot"})
public String showHotArticle(Model model){
List<ArticlePO> articlePOList = articleService.queryTopAritcle();
//转为VO
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 article = new ArticleDTO();
article.setTitle(param.getTitle());
article.setSummary(param.getSummary());
article.setContent(param.getContent());
boolean add = articleService.addArticle(article);
return "redirect:/article/hot";
}
//查询文章
@GetMapping("/article/get")
public String queryById(Integer id, Model model){
ArticleDTO articleDTO = articleService.queryByArticleId(id);
ArticleVO articleVO = BeanUtil.copyProperties(articleDTO, ArticleVO.class);
model.addAttribute("article",articleVO);
return "/blog/editArticle";
}
//修改文章
@PostMapping("/article/edit")
public String modifyArticle(@Validated(ArticleParam.EditArticle.class) ArticleParam param){
boolean edit = articleService.modifyArticle(param);
return "redirect:/article/hot";
}
//删除文章
@PostMapping("/article/removeArticle")
public String removeArticle(@RequestParam("ids") IdType idType){
System.out.println("ids="+idType);
if(idType.getIdList() == null){
throw new IdNullException("Id为null");
}
articleService.removeArticle(idType.getIdList());
return "redirect:/article/hot";
}
//查询文章开始的20个字
@GetMapping("/article/detail/overview")
@ResponseBody
public String queryDetail(Integer id){
String top20Content = articleService.queryTop20Detail(id);
return top20Content;
}
}
IdNullException.java
public class IdNullException extends BlogRootException{
public IdNullException() {
super();
}
public IdNullException(String message) {
super(message);
}
}
BlogRootException.java
public class BlogRootException extends RuntimeException{
public BlogRootException() {
super();
}
public BlogRootException(String message) {
super(message);
}
}
GlobalHandleException.java
@ControllerAdvice
public class GlobalHandleException {
@ExceptionHandler( BindException.class)
public String handlerBindException(BindException bindException, Model model){
BindingResult result = bindException.getBindingResult();
if( result.hasErrors()){
model.addAttribute("errors",result.getFieldErrors());
System.out.println("result.getFieldErrors()="+result.getFieldErrors().size());
}
return "/blog/error/bind";
}
@ExceptionHandler( Exception.class)
public String handlerDefaultException(Exception exception, Model model){
model.addAttribute("msg","请稍后重试!!!");
return "/blog/error/error";
}
}
IdType.java
@Data
public class IdType {
private List<Integer> idList;
}
IdTypeFormatter.java
public class IdTypeFormatter implements Formatter<IdType> {
@Override
public IdType parse(String text, Locale locale) throws ParseException {
IdType idType = new IdType();
if(StringUtils.hasText(text)){
List<Integer> ids = new ArrayList<>();
for (String id : text.split(",")) {
ids.add(Integer.parseInt(id));
}
idType.setIdList(ids);
}
return idType;
}
@Override
public String print(IdType object, Locale locale) {
return null;
}
}
ArticleSettings.java
@Data
@ConfigurationProperties(prefix = "article")
public class ArticleSettings {
private Integer lowRead;
private Integer topRead;
}
WebMvcSettings.java
@Configuration
public class WebMvcSettings implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new IdTypeFormatter());
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/view/addArticle").setViewName("/blog/addArticle");
}
}
启动类:
Lession19AdminApplication.java
@MapperScan(basePackages = { "com.bjpowernode.blog.mapper" })
@EnableConfigurationProperties( {ArticleSettings.class} )
@SpringBootApplicationpublic class Lession19AdminApplication {
public static void main(String[] args) {
SpringApplication.run(Lession19AdminApplication.class, args);
}
}