Spring Boot + vue-element 开发个人博客项目实战教程(二十三、文章管理页面开发(2))

简介: Spring Boot + vue-element 开发个人博客项目实战教程(二十三、文章管理页面开发(2))

⭐ 作者简介:码上言



⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程



⭐专栏内容:零基础学Java个人博客系统

项目部署视频

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搜索框来实现。


搜索:当我们在输入框输入数据时,就会请求接口进行条件查询。

自定义:直接在输入框中输入完分类的名称之后,直接回车即可。

接下来我们用代码进行分析。

这里主要是分析弹出框里面的内容。

当你选择完分类之后,会在页面上展示出来分类的名称。

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;
    }

1.首先我们拿到上传的图片之后,我们先进行MD5然后拿到文件的MD5值进行图片文件名重新命名,为的就是防止文件名重复,被覆盖,就会导致图片数据丢失。

2.然后通过getOriginalFilename获取图片的后缀格式。

3.拼接起来组成新的图片名称。

4.为了防止丢失数据,再次进行图片名称判断,有就直接返回地址,没有则继续上传图片或文件。

以上会用到几个方法和工具类如下:

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、文章的发布状态

文章的发布状态有三种:

  1. 公开
  2. 自己可见
  3. 草稿
<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


目录
相关文章
|
1天前
|
监控 安全 NoSQL
采用java+springboot+vue.js+uniapp开发的一整套云MES系统源码 MES制造管理系统源码
MES系统是一套具备实时管理能力,建立一个全面的、集成的、稳定的制造物流质量控制体系;对生产线、工艺、人员、品质、效率等多方位的监控、分析、改进,满足精细化、透明化、自动化、实时化、数据化、一体化管理,实现企业柔性化制造管理。
15 3
|
4天前
|
开发框架 监控 Java
深入探索Spring Boot的监控、管理和测试功能及实战应用
【5月更文挑战第14天】Spring Boot是一个快速开发框架,提供了一系列的功能模块,包括监控、管理和测试等。本文将深入探讨Spring Boot中监控、管理和测试功能的原理与应用,并提供实际应用场景的示例。
16 2
|
4天前
|
Java 关系型数据库 MySQL
保姆级教程——将springboot项目部署到阿里云服务器包含环境配置(小白包会)
本文档详细介绍了将SpringBoot项目部署到阿里云服务器的步骤。首先,通过Xshell连接服务器,使用公网IP地址。接着,下载JDK的Linux版本,使用XFTP上传并解压,配置环境变量。然后,安装MySQL 5.7,包括下载YUM源、安装、启动服务以及修改root密码和开启远程访问。最后,将SpringBoot项目打包成jar,上传至服务器,使用`java -jar`命令运行,通过`nohup`确保服务持续运行。配置安全组以允许远程访问。
|
4天前
|
传感器 人工智能 前端开发
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
智慧校园电子班牌,坐落于班级的门口,适合于各类型学校的场景应用,班级学校日常内容更新可由班级自行管理,也可由学校统一管理。让我们一起看看,电子班牌有哪些功能呢?
106 4
JAVA语言VUE2+Spring boot+MySQL开发的智慧校园系统源码(电子班牌可人脸识别)Saas 模式
|
4天前
|
Java 数据库连接 数据库
Springboot整合mybatisPlus开发
MyBatis-Plus是一个MyBatis的增强工具,旨在简化开发和提高效率。它在不修改原有MyBatis的基础上提供额外功能。要将MyBatis-Plus集成到SpringBoot项目中,首先通过Maven添加mybatis-plus-boot-starter和相应数据库驱动依赖,然后配置application.yml中的数据库连接信息,并指定Mapper类的扫描路径。Mapper接口可继承BaseMapper实现基本的CRUD操作。
|
4天前
|
前端开发 JavaScript Java
Springboot框架整合jsp开发【干货满满】
该文介绍了如何在Spring Boot中集成JSP,需包含`spring-boot-starter-web`、`tomcat-embed-jasper`和`jstl`三个依赖。配置Spring Boot寻找JSP的位置,设置`spring.mvc.view.prefix`为`/WEB-INF/jsp/`,`spring.mvc.view.suffix`为`.jsp`。JSP文件应放在`src/main/webapp/WEB-INF/jsp/`下。
|
4天前
|
Web App开发 前端开发 Java
SpringBoot配置HTTPS及开发调试
在实际开发过程中,如果后端需要启用https访问,通常项目启动后配置nginx代理再配置https,前端调用时高版本的chrome还会因为证书未信任导致调用失败,通过摸索整理一套开发调试下的https方案,特此分享
21 0
SpringBoot配置HTTPS及开发调试
|
4天前
|
IDE Java 开发工具
Spring Boot DevTools:加速开发的热部署工具
【4月更文挑战第28天】在Spring Boot的开发过程中,快速反馈和效率至关重要。Spring Boot DevTools是一个为开发者设计的模块,支持热部署(hot swapping),能够实现应用的快速重启和自动重载,极大地提高了开发效率。本篇博客将介绍Spring Boot DevTools的核心概念,并通过具体的实战示例展示如何在开发过程中利用这一工具。
24 0
|
4天前
|
Java 应用服务中间件 Maven
Spring Boot项目打war包(idea:多种方式)
Spring Boot项目打war包(idea:多种方式)
16 1
|
4天前
|
Java Linux
Springboot 解决linux服务器下获取不到项目Resources下资源
Springboot 解决linux服务器下获取不到项目Resources下资源