⭐ 作者简介:码上言
⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程
⭐专栏内容:零基础学Java、个人博客系统
👦 学习讨论群:530826149
项目部署视频
https://www.bilibili.com/video/BV1sg4y1A7Kv/?vd_source=dc7bf298d3c608d281c16239b3f5167b
文章目录
前言
我们接着上一篇的来写,现在就剩下文章发布的功能了,但这个是最重要的部分,大家有什么问题欢迎随时留言或者私信我,或加我好友都可以。目录的话我就接着上一篇的来写了,不再重新写目录了,大家可以两篇合起来一起来看。
3.4、编写添加文章页面弹出层
3.4.1、添加标签和分类查询接口
在/src/api
文件下找到category.js
接口,我们将刚才新添加的分类查询接口加入进来
export function getCategory(data) {
return request({
url: '/category/getCategory',
method: 'post',
data
})
}
在/src/api
文件下找到tag.js
接口,我们将刚才新添加的标签查询接口加入进来
export function getTag(data) {
return request({
url: '/tag/selectTag',
method: 'post',
data
})
}
3.4.2、添加文章分类功能
文章分类功能是在我们弹出框里,为了方便大家学习,我现将业务逻辑分析一下,然后再来写具体的代码,这样就能清晰的分析代码,进而读懂代码了。
文章分类业务分析:
首先我们写完文章之后,点击发布文章,然后跳出一个弹出框,需要我们填写文章的一些属性,我们先重点来说分类,然后标签就和这类似了。
上图是我点击发布文章之后弹出的,我们先设置一个添加分类的按钮,这时我们会触发查询分类的接口,将数据库中的所有分类全部查询出来。然后我们可以进行搜索或者自定义分类,使用了vue的el-autocomplete搜索框来实现。
- 搜索:当我们在输入框输入数据时,就会请求接口进行条件查询。
- 自定义:直接在输入框中输入完分类的名称之后,直接回车即可。
接下来我们用代码进行分析。
这里主要是分析弹出框里面的内容。
当你选择完分类之后,会在页面上展示出来分类的名称。
<el-tag type="success" v-show="article.categoryName" style="margin:0 1rem 0 0" :closable="true" @close="removeCategory">
{{ article.categoryName }}
</el-tag>
removeCategory()
方法的实现:
removeCategory() {
this.article.categoryName = null;
},
以下是选择分类的功能模块
el-popover
是ElementUI封装的一个弹窗组件,类似于el-tooltip,弹窗中也可以自定义内容等。autocomplete
是一个可带输入建议的输入框组件,具体官方文档:https://element.eleme.cn/#/zh-CN/component/input
<!-- 分类选项 -->
<el-popover placement="bottom-start" width="460" trigger="click" v-if="!article.categoryName">
<div class="popover-title">分类</div>
<!-- 搜索框 -->
<el-autocomplete
style="width:100%"
v-model="categoryName"
:fetch-suggestions="findCategories"
placeholder="请输入分类名搜索,如果自定义分类,输入完成之后直接回车即可!"
:trigger-on-focus="false"
@keyup.enter.native="saveCategory"
@select="handleFindCategories"
>
<template slot-scope="{ item }">
<div>{{ item.categoryName }}</div>
</template>
</el-autocomplete>
<!-- 分类数据展示 -->
<div class="popover-container">
<div class="category-item" v-for="item of categoryList" :key="item.id" @click="addCategory(item)">
{{ item.categoryName }}
</div>
</div>
<el-button type="success" plain slot="reference" size="small"> 添加分类 </el-button>
</el-popover>
其余的也没什么好说的,大家看一下应该都能看懂,之前也讲过一部分,比如分类数据展示,就是个list数据的展示等操作。
js的部分主要实现一些方法的操作,这里我们还引入了分类的接口。后边还会引入标签的接口。
import { addArticle, updateArticle, getArticleById } from '@/api/article'
import { getCategory } from '@/api/category'
data() {
return {
showDialog: false,
categoryName: "",
categoryList: [],
article: {
id: "",
title: "",
categoryId: "",
content: "",
categoryName: null
}
}
},
methods: {
// 打开文章信息填写框
openDialog() {
if (this.article.title.trim() == "") {
this.$message.error("文章标题为空,请填写文章标题");
return false;
}
if (this.article.content.trim() == "") {
this.$message.error("文章内容为空,请填写文章内容");
return false;
}
this.getCategoriesList();
this.showDialog = true;
},
//------分类的业务处理开始------
getCategoriesList() {
var categoryName = "";
getCategory({categoryName}).then(response => {
this.categoryList = response.data;
})
},
removeCategory() {
this.article.categoryName = null;
},
//搜索分类名称
findCategories(categoryName, cb) {
getCategory({categoryName}).then(response => {
cb(response.data);
})
},
saveCategory() {
if (this.categoryName.trim() != "") {
this.addCategory({
categoryName: this.categoryName
});
this.categoryName = "";
}
},
addCategory(item) {
this.article.categoryName = item.categoryName;
},
handleFindCategories(item) {
this.addCategory({
categoryName: item.categoryName
});
},
//------分类的业务处理结束------
handleSubmit() {
this.showDialog = true;
var body = this.article;
},
handleCancel() {
this.showDialog = false;
},
}
//css
.popover-title {
margin-bottom: 1rem;
text-align: center;
}
.category-item {
cursor: pointer;
padding: 0.6rem 0.5rem;
}
.category-item:hover {
background-color: #f0f9eb;
color: #67c23a;
}
具体的代码我会将放在这一节的最后展示,大家先进行学习,有错误了再去对照完整的代码看一下。
3.4.3、添加文章标签功能
标签的功能和分类的功能差不多,大家可以先自己参照分类的思路先写,然后再接着往下看我写的作为参考,这样自己就能再脑子里回顾一下写的思路,以后写项目遇到类似的功能就能非常清楚的想起思路。
这里我先将标签的主要代码列举出来,基本上和分类的一致,但是分类只有一个,标签有多个,前端限制的每一篇文章最多只有三个。
<!-- ----------文章标签开始---------- -->
<el-form-item label="文章标签">
<el-tag
v-for="(item, index) of article.tagNameList"
:key="index"
style="margin:0 1rem 0 0"
:closable="true"
@close="removeTag(item)"
>
{{ item }}
</el-tag>
<!-- 标签选项 -->
<el-popover
placement="bottom-start"
width="460"
trigger="click"
v-if="article.tagNameList.length < 3"
>
<div class="popover-title">标签</div>
<!-- 标签搜索框 -->
<el-autocomplete
style="width:100%"
v-model="tagName"
:fetch-suggestions="findTags"
placeholder="请输入标签名搜索,按回车可添加自定义标签"
:trigger-on-focus="false"
@keyup.enter.native="saveTag"
@select="handleFindTag"
>
<template slot-scope="{ item }">
<div>{{ item.tagName }}</div>
</template>
</el-autocomplete>
<!-- 标签数据展示 -->
<div class="popover-container">
<div style="margin-bottom:1rem">添加标签</div>
<el-tag
v-for="(item, index) of tagList"
:key="index"
:class="tagClass(item)"
@click.native="addTag(item)"
>
{{ item.tagName }}
</el-tag>
</div>
<el-button type="primary" plain slot="reference" size="small">
添加标签
</el-button>
</el-popover>
</el-form-item>
<!-- ----------文章标签结束---------- -->
JS部分:
import { getTag } from '@/api/tag'
//------标签的业务处理------
getTagsList() {
var tagName = "";
getTag({tagName}).then(response => {
this.tagList = response.data;
})
},
//搜索标签名称
findTags(tagName, cb) {
getTag({tagName}).then(response => {
cb(response.data);
})
},
handleFindTag(item) {
this.addTag({
tagName: item.tagName
});
},
saveTag() {
if (this.tagName.trim() != "") {
this.addTag({
tagName: this.tagName
});
this.tagName = "";
}
},
addTag(item) {
console.log("标签展示:",item);
if (this.article.tagNameList.indexOf(item.tagName) == -1) {
this.article.tagNameList.push(item.tagName);
}
},
removeTag(item) {
const index = this.article.tagNameList.indexOf(item);
this.article.tagNameList.splice(index, 1);
},
//------标签的业务处理结束------
//放到methods方法外层
computed: {
tagClass() {
return function(item) {
const index = this.article.tagNameList.indexOf(item.tagName);
return index != -1 ? "tag-item-select" : "tag-item";
};
}
}
这里我只说一个知识点:
问题:vue中@click和@click.native.prevent的区别是什么?
@click
是用在按钮上的语法@click.native
是给vue组件绑定事件时候,必须加上native ,否则会认为监听的是来自Item组件自定义的事件,prevent是用来阻止默认的事件。就相当于event.preventDefault()
,父组件想在子组件上监听自己的click的话,需要加上native修饰符。
3.4.4、文章摘要
文章摘要也就是对文章的大体描述功能,这个就是个表单,没有难点。
<el-form-item label="文章摘要">
<el-input type="textarea" :rows="2" placeholder="请输入内容" v-model="article.description" style="width:220px" />
</el-form-item>
return {
showDialog: false,
categoryName: "",
categoryList: [],
tagName: "",
tagList: [],
article: {
id: "",
title: "",
categoryId: "",
content: "",
categoryName: null,
tagNameList: [],
description: ""
}
}
3.4.5、文章封面上传
文章封面上传功能是一个新的知识点,我们后端要添加新的接口,上传图片的接口,这里就涉及到文件的上传,保存路径等操作,这也是在以后的工作中经常使用的文件操作,这个也是一个重点,希望大家认真对待学习。
我们先去写后端图片上传的功能,图片的存储等操作。希望大家能了解图片上传的业务逻辑思路。
1、后端功能
和之前的业务流程一样,我们先写一个图片上传的接口,打开ArticleService.java
/**
* 上传文件
*
* @param file
* @return
*/
String uploadFile(MultipartFile file);
这里说一下MultipartFile
对象,有些同学可能没有学过或不了解,我这里简单的说明一下。
使用MultipartFile这个类主要是来实现以表单的形式进行文件上传功能。首先MultipartFile是一个接口,并继承自InputStreamSource,且在InputStreamSource接口中封装了getInputStream方法,该方法的返回类型为InputStream类型,这也就是为什么MultipartFile文件可以转换为输入流。通过以下代码即可将MultipartFile格式的文件转换为输入流。
这个MultipartFile有一些常用的方法。
getName
getName方法获取的是前后端约定的传入文件的参数的名称,在SpringBoot后台中则是通过@Param("uploadFile")
注解定义的内容。getOriginalFileName
getOriginalFileName方法获取的是文件的完整名称,包括文件名称+文件拓展名。getContentType
getContentType方法获取的是文件的类型,注意是文件的类型,不是文件的拓展名。getSize
getSize方法用来获取文件的大小,单位是字节。getInputStream
getInputStream方法用来将文件转换成输入流的形式来传输文件,会抛出IOException异常。
还有一些其他的方法,大家可以自己去查找,以上这些我们会经常用到。
接口有了,我们再去写实现方法,打开ArticleServiceImpl.java
实现类。
通过上边对MultipartFile的解释,下面我们就使用到了。
@Override
public String uploadFile(MultipartFile file) {
try {
// 获取文件md5值
String md5 = FileUtils.getMd5(file.getInputStream());
// 获取文件扩展名
String extName = FileUtils.getExtName(file.getOriginalFilename());
// 重新生成文件名
String fileName = md5 + extName;
// 判断文件是否已存在
if (!exists(ARTICLE + fileName)) {
// 不存在则继续上传
upload(ARTICLE, fileName, file.getInputStream());
}
// 返回文件访问路径
return getFileAccessUrl(ARTICLE + fileName);
} catch (Exception e) {
e.printStackTrace();
log.error("文件上传失败");
}
return null;
}
- 首先我们拿到上传的图片之后,我们先进行MD5然后拿到文件的MD5值进行图片文件名重新命名,为的就是防止文件名重复,被覆盖,就会导致图片数据丢失。
- 然后通过getOriginalFilename获取图片的后缀格式。
- 拼接起来组成新的图片名称。
- 为了防止丢失数据,再次进行图片名称判断,有就直接返回地址,没有则继续上传图片或文件。
以上会用到几个方法和工具类如下:
FileUtils工具类:
package com.blog.personalblog.util;
import cn.hutool.core.util.StrUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.binary.Hex;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
/**
* 文件md5工具类
*
*/
@Log4j2
public class FileUtils {
/**
* 获取文件md5值
*
* @param inputStream 文件输入流
* @return {@link String} 文件md5值
*/
public static String getMd5(InputStream inputStream) {
try {
MessageDigest md5 = MessageDigest.getInstance("md5");
byte[] buffer = new byte[8192];
int length;
while ((length = inputStream.read(buffer)) != -1) {
md5.update(buffer, 0, length);
}
return new String(Hex.encodeHex(md5.digest()));
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 得到文件扩展名
*
* @param fileName 文件名称
* @return {@link String} 文件后缀
*/
public static String getExtName(String fileName) {
if (StrUtil.isBlank(fileName)) {
return "";
}
return fileName.substring(fileName.lastIndexOf("."));
}
}
exists方法:
本地路径需要在配置文件中配置,打开application.yml配置文件,然后配置如下:
upload:
local:
path: /blog/uploadFile/
url: http://localhost:9090/blog
/**
* 本地路径
*/
@Value("${upload.local.path}")
private String localPath;
/**
* 访问url
*/
@Value("${upload.local.url}")
private String localUrl;
private static final String ARTICLE = "articles/";
/**
* 判断文件是否存在
*
* @param filePath 文件路径
* @return
*/
public Boolean exists(String filePath){
return new File(localPath + filePath).exists();
}
getFileAccessUrl方法:
/**
* 获取文件访问url
*
* @param filePath 文件路径
* @return
*/
public String getFileAccessUrl(String filePath) {
return localUrl + localPath + filePath;
}
upload方法:
private void upload(String path, String fileName, InputStream inputStream) throws IOException {
File directory = new File(localPath + path);
if (!directory.exists()) {
if (!directory.mkdirs()) {
log.error("创建目录失败");
}
}
// 写入文件
File file = new File(localPath + path + fileName);
if (file.createNewFile()) {
BufferedInputStream bis = new BufferedInputStream(inputStream);
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
byte[] bytes = new byte[1024];
int length;
while ((length = bis.read(bytes)) != -1) {
bos.write(bytes, 0, length);
}
bos.flush();
inputStream.close();
bis.close();
bos.close();
}
}
然后我们在去写controller层接口,打开ArticleController.java
,这个就不要记日志了,我们日志那边没有做文件的处理,所以会报错,这里暂时不需要记日志。
/**
* 上传网站logo封面
* @param file
* @return 返回logo地址
*/
@ApiOperation(value = "上传网站logo封面")
@PostMapping("upload")
public JsonResult<String> uploadImg(@RequestParam(value = "file") MultipartFile file) {
String s = articleService.uploadFile(file);
return JsonResult.success(s);
}
好了,上传图片的功能已经实现,我们接下来用postman测试一下。
这时我们请求接口会报500错误,我们再看一下后端有没有报错信息。果然也报错了,需要我们去设置一下文件上传的大小限制。
我们打开application.yml
,在spring下面添加一下配置:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB
然后重启项目,我们再次请求接口。
看到了吧,有数据返回,这个就是我们刚才上传图片的地址,我们去查看一下,这个目录会自动创建的,是你项目当前路径下的地方,假如你的项目在D盘,则这个图片地址就会在D盘下面。
看到了吧,我的项目就在D盘,所以这个文件就在D盘下。
接下来我们就要去实现前端的功能了,在这之前我们还要加一个图片拦截的操作,防止前端访问不到图片的操作。
新建一个配置类:MyInterceptorConfig.java
,我放在了config包
下。
package com.blog.personalblog.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 这个是访问图片拦截的
*
* @author: SuperMan
* @create: 2022-08-20
**/
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/blog/uploadFile/articles/**")//前端url访问的路径,若有访问前缀,在访问时添加即可,这里不需添加。
.addResourceLocations("file:/blog/uploadFile/articles/");//映射的服务器存放图片目录。
}
}
还有一个再写前端之前,我们将上传图片的接口过滤掉,不受登录的限制,还有个一图片预览的地址也要放开,否则请求图片地址会报302重定向错误这个是一个坑。
打开ShiroConfiguration.java
,再新增两个配置。
filterChainDefinitionMap.put("/blog/uploadFile/articles/**","anon");
filterChainDefinitionMap.put("/article/upload", "anon");
接下来打开前端项目,我们来写前端上传图片的页面。
<el-form-item label="上传封面">
<el-upload
class="upload-cover"
drag
action="null"
:http-request="importFile"
multiple
:before-upload="handleUploadBefore"
>
<i class="el-icon-upload" v-if="article.imageUrl == ''" />
<div class="el-upload__text" v-if="article.imageUrl == ''">
将文件拖到此处,或<em>点击上传</em>
</div>
<img
v-else
:src="article.imageUrl"
width="360px"
height="180px"
/>
</el-upload>
</el-form-item>
上边是上传图片的组件el-upload
,这里面有几个注意点,大家可以看一下element官方文档,这里给大家说一下,在开发的过程中一定要去看开发文档,里面有很多的组件设置的属性,还有具体的使用方法。
我原来想使用组件自带的action直接使用上传的地址,但是测试了一下会出现跨域的问题,我们这个项目是统一走的api那边的路由,所以我直接换成了自定义上传的方式,我感觉自定义上传是的代码会更加的清晰,自定义上传的地址就要使用到:http-request,这个文档也有说明。大家一定要看文档
我们需要写两个方法,一个是图片上传的方法,另一个是图片上传之前进行验证的操作。
以下是上传之前校验图片大小和格式
handleUploadBefore(file) {
const isJPGORPNG = file.type === "image/jpeg" || file.type === "image/png";
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isJPGORPNG) {
this.$message.error('上传图片只能是 JPG 或 PNG 格式!');
}
if (!isLt10M) {
this.$message.error('上传图片大小不能超过 10MB!');
}
return isJPGORPNG && isLt10M;
},
图片上传接口请求。
importFile(param){
let fd = new FormData();
fd.append("file", param.file); // 传文件
uploadImg(fd).then(res => {
if (res.data && res.data.code === 200) {
this.$message({
type: 'success',
message: '图片上传成功!'
})
}
this.article.imageUrl = res.data;
})
},
这个FormData
实现form表单数据的序列化,将数据以键值对 name/value 的形式传到后台,从而减少表单元素的拼接,提高工作效率。
向FormData中添加新的属性值使用append
,我们后端接收的参数就是file
,所以我们添加一个file即可。
然后我们将上传图片的接口加入到接口中。在article.js
接口中。
export function uploadImg(data) {
return request({
url: '/article/upload',
method: 'post',
data
})
}
别忘了在import中引入该接口。
好啦,这个上传的功能基本上实现了,我们测试一下,
好啦,文章上传功能就这些,下面我们来写文章的发布功能。
3.4.6、保存草稿功能
这里需要修改一下文章的数据库,有一些字段需要修改,多的字段暂时就不删除了,不然就改动的很多。例如这里的浏览量可以用redis
去实现,而不是存入数据库中实现。
DROP TABLE IF EXISTS `person_article`;
CREATE TABLE `person_article` (
`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`author` VARCHAR(128) NOT NULL COMMENT '作者',
`title` VARCHAR(255) NOT NULL COMMENT '文章标题',
`user_id` INT(11) NOT NULL COMMENT '用户id',
`category_id` INT(11) NULL COMMENT '分类id',
`content` LONGTEXT NOT NULL COMMENT '文章内容',
`views` BIGINT NOT NULL DEFAULT 0 COMMENT '文章浏览量',
`total_words` BIGINT NOT NULL DEFAULT 0 COMMENT '文章总字数',
`commentable_id` INT NULL COMMENT '评论id',
`art_status` TINYINT NOT NULL DEFAULT 1 COMMENT '发布,默认1, 1-发布, 2-仅我可见 3-草稿',
`description` VARCHAR(255) NULL COMMENT '描述',
`image_url` VARCHAR(255) NULL COMMENT '文章logo',
`create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
) ENGINE = InnoDB
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin
ROW_FORMAT = Dynamic
COMMENT '文章管理表';
还有文章和标签的关联表数据库也要修改一下,把表的id删除掉,这个字段多余的。
DROP TABLE IF EXISTS `person_article_tag`;
CREATE TABLE `person_article_tag` (
`tag_id` INT(11) NOT NULL COMMENT '标签id',
`article_id` INT(11) NOT NULL COMMENT '文章id'
) ENGINE = InnoDB
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_bin
ROW_FORMAT = Dynamic
COMMENT '文章和标签关联表';
相对应的实体类和xml这两个文件要把id删除掉,这里我就不展示了,只删除id即可。
保存草稿功能相对于保存文章比较简单,不需要进入到弹出框之前就结束了这个功能,先找到我们之前写的保存草稿页面,点击功能需要完善即可。这里就修改了一下点击的方法名称。
<el-button type="warning"
size="medium"
@click="saveDraft"
style="margin-left:10px"
v-if="article.id === '' || article.artStatus == 3"
>保存草稿</el-button>
接下来我们来实现saveDraft
方法的功能。
saveDraft() {
this.article.artStatus = 3;
if (this.article.title.trim() == "") {
this.$message.error("文章标题不能为空");
return false;
}
if (this.article.content.trim() == "") {
this.$message.error("文章内容不能为空");
return false;
}
var body = this.article;
addArticle(body).then(res => {
if(res.code === 200) {
this.$message({
type: 'success',
message: '保存草稿成功!'
});
} else {
this.$message({
type: 'error',
message: '保存草稿失败!'
});
}
})
},
从以上代码可以看出,我先校验了文章的标题和内容,然后调用了添加文章的接口进行文章保存。
这里还要修改一下文章保存的接口,我们之前设计的不太合理,现在需要再完善一下。
首先添加一个文章保存的对象:ArticleInsertBO.java
package com.blog.personalblog.bo;
import lombok.Data;
/**
* @author: SuperMan
* @create: 2022-10-02
*/
@Data
public class ArticleInsertBO {
/**
* 文章id
*/
private Integer id;
/**
* 文章标题
*/
private String title;
/**
* 分类id
*/
private Integer categoryId;
/**
* 文章内容
*/
private String content;
/**
* 发布,默认1, 1-发布, 2-仅我可见 3-草稿
*/
private Integer artStatus;
/**
* 描述
*/
private String description;
/**
* 文章logo
*/
private String imageUrl;
/**
* 分类名称
*/
private String categoryName;
/**
* 文章标签
*/
private List<String> tagNameList;
}
然后修改接口,这里我将修改个添加整合到一个接口中,可以减少重复的代码
/**
* 新建文章
* @param bo
* @return
*/
void insertOrUpdateArticle(ArticleInsertBO bo);
分类还要新加一个根据分类名称查询分类的接口以及实现方法,我这里就直接把代码列出来,相信大家对这操作也都比较熟练了。
/**
* 获取分类
* @param categoryName
* @return
*/
Category getCategoryByName(String categoryName);
@Override
public Category getCategoryByName(String categoryName) {
Category category = categoryMapper.getCategoryByName(categoryName);
return category;
}
CategoryMapper.java:
Category getCategoryByName(String categoryName);
<select id="getCategoryByName" resultMap="BaseResultMap">
select * from person_category where category_name = #{categoryName, jdbcType=VARCHAR}
</select>
再将文章的发布形式单独提出来,写成一个枚举类,方便以后维护。
package com.blog.personalblog.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author: SuperMan
* @create: 2022-10-10
**/
@Getter
@AllArgsConstructor
public enum ArticleArtStatusEnum {
/**
* 发布
*/
PUBLISH(1, "发布"),
/**
* 仅我可见
*/
ONLYME(2, "仅我可见"),
/**
* 草稿
*/
DRAFT(3, "草稿");
/**
* 状态
*/
private final Integer status;
/**
* 描述
*/
private final String desc;
}
然后我们来写文章保存或修改的实现类。
这一块的逻辑相比较之前的有了一些变化,希望大家好好看一下这个逻辑,如果看不明白可以留言,我这里不再讲述了。
@Resource
private CategoryService categoryService;
@Resource
private UserService userService;
@Override
public void insertOrUpdateArticle(ArticleInsertBO bo) {
//分类添加
Category category = saveCategory(bo);
Article article = BeanUtil.copyProperties(bo, Article.class);
if (category != null) {
article.setCategoryId(category.getCategoryId());
}
String username = (String) SecurityUtils.getSubject().getPrincipal();
User user = userService.getUserByUserName(username);
article.setUserId(user.getId());
article.setAuthor(user.getUserName());
article.setViews(0L);
article.setTotalWords(0L);
if (bo.getId() != null) {
articleMapper.updateArticle(article);
} else {
articleMapper.createArticle(article);
}
articleMap.put(article.getId(), article);
//添加文章标签
saveTags(bo, article.getId());
//添加文章发送邮箱提醒
try {
String content = "【{0}】您好:\n" +
"您已成功发布了标题为: {1} 的文章 \n" +
"请注意查收!\n";
MailInfo build = MailInfo.builder()
.receiveMail(user.getEmail())
.content(MessageFormat.format(content, user.getUserName(), article.getTitle()))
.title("文章发布")
.build();
SendMailConfig.sendMail(build);
} catch (Exception e) {
log.error("邮件发送失败{}", e.getMessage());
}
}
private Category saveCategory(ArticleInsertBO bo) {
if (StrUtil.isEmpty(bo.getCategoryName())) {
return null;
}
Category category = categoryService.getCategoryByName(bo.getCategoryName());
if (category == null && !ArticleArtStatusEnum.DRAFT.getStatus().equals(bo.getArtStatus())) {
category.setCategoryName(bo.getCategoryName());
categoryService.saveCategory(category);
}
return category;
}
private void saveTags(ArticleInsertBO bo, Integer articleId) {
//首先判断是不是更新文章
if (bo.getId() == null) {
articleTagService.deleteTag(bo.getId());
}
//添加文章标签
List<String> tagNameList = bo.getTagNameList();
List<Integer> tagIdList = new ArrayList<>();
if (CollUtil.isNotEmpty(tagNameList)) {
//先查看添加的标签数据库里有没有
for (String tagName : tagNameList) {
Tag one = tagService.findByTagName(tagName);
if (one == null) {
Tag tag = new Tag();
tag.setTagName(tagName);
tagService.saveTag(tag);
tagIdList.add(tag.getId());
} else {
tagIdList.add(one.getId());
}
}
}
articleTagService.deleteTag(articleId);
if (tagIdList != null) {
List<ArticleTag> articleTagList = tagIdList.stream().map(tagId -> ArticleTag.builder()
.tagId(tagId)
.articleId(articleId)
.build()).collect(Collectors.toList());
articleTagService.insertBatch(articleTagList);
}
}
好啦,后端修改完成了,别忘了文章的数据库表更新成最新修改的。接下来我们运行项目,启动后端项目,然后我们测试一下数据有没有成功。
显示操作成功了,说明接口是通的,然后我们再去看一下数据库有没有这条数据。
有数据,那我们的保存草稿功能就完成了。
3.4.7、文章的发布状态
文章的发布状态有三种:
- 公开
- 自己可见
- 草稿
<el-form-item label="发布形式">
<el-radio-group v-model="article.artStatus">
<el-radio :label="1">全部可见</el-radio>
<el-radio :label="2">仅我可见</el-radio>
</el-radio-group>
</el-form-item>
3.4.8、文章发布
前面已经将铺垫都准备好了,接下来我们还要实现一个发布的功能,这个和保存草稿的功能基本类似。
handleSubmit() {
this.showDialog = true;
if (this.article.title.trim() == "") {
this.$message.error("文章标题不能为空");
return false;
}
if (this.article.content.trim() == "") {
this.$message.error("文章内容不能为空");
return false;
}
if (this.article.categoryName == null) {
this.$message.error("文章分类不能为空");
return false;
}
if (this.article.tagNameList.length == 0) {
this.$message.error("文章标签不能为空");
return false;
}
var body = this.article;
addArticle(body).then(res => {
if(res.code === 200) {
this.$message({
type: 'success',
message: '文章发表成功!'
});
} else {
this.$message({
type: 'error',
message: '文章发表失败!'
});
}
})
},
写完之后,我们再测试一下正式发布文章。
看到数据库有数据了,我们的文章发布功能就全部完成了。
这里我只将添加文章的页面全部代码列出来,其余的小东西比较多,我统一上传到了gitee上,大家可以下载下来去查看代码。
后端gitee地址:https://gitee.com/xyhwh/personal_blog
前端gitee地址:https://gitee.com/xyhwh/personal_vue