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


目录
相关文章
|
9天前
|
JavaScript Java 关系型数据库
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
本文介绍了一个基于Spring Boot和Vue.js实现的在线考试系统。随着在线教育的发展,在线考试系统的重要性日益凸显。该系统不仅能提高教学效率,减轻教师负担,还为学生提供了灵活便捷的考试方式。技术栈包括Spring Boot、Vue.js、Element-UI等,支持多种角色登录,具备考试管理、题库管理、成绩查询等功能。系统采用前后端分离架构,具备高性能和扩展性,未来可进一步优化并引入AI技术提升智能化水平。
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
|
11天前
|
Java 关系型数据库 MySQL
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
本文介绍了一款基于Spring Boot和JSP技术的房屋租赁系统,旨在通过自动化和信息化手段提升房屋管理效率,优化租户体验。系统采用JDK 1.8、Maven 3.6、MySQL 8.0、JSP、Layui和Spring Boot 2.0等技术栈,实现了高效的房源管理和便捷的租户服务。通过该系统,房东可以轻松管理房源,租户可以快速找到合适的住所,双方都能享受数字化带来的便利。未来,系统将持续优化升级,提供更多完善的服务。
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
|
12天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
557 7
|
22天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
22天前
|
Java 数据库连接 Maven
Spring基础1——Spring(配置开发版),IOC和DI
spring介绍、入门案例、控制反转IOC、IOC容器、Bean、依赖注入DI
Spring基础1——Spring(配置开发版),IOC和DI
|
11天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的服装商城管理系统
基于Java+Springboot+Vue开发的服装商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的服装商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
33 2
基于Java+Springboot+Vue开发的服装商城管理系统
|
11天前
|
前端开发 JavaScript Java
SpringBoot项目部署打包好的React、Vue项目刷新报错404
本文讨论了在SpringBoot项目中部署React或Vue打包好的前端项目时,刷新页面导致404错误的问题,并提供了两种解决方案:一是在SpringBoot启动类中配置错误页面重定向到index.html,二是将前端路由改为hash模式以避免刷新问题。
53 1
|
8天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
基于Java+Springboot+Vue开发的大学竞赛报名管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的大学竞赛报名管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
23 3
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
|
9天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的蛋糕商城管理系统
基于Java+Springboot+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
21 3
基于Java+Springboot+Vue开发的蛋糕商城管理系统
|
9天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的美容预约管理系统
基于Java+Springboot+Vue开发的美容预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的美容预约管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
21 3
基于Java+Springboot+Vue开发的美容预约管理系统
下一篇
无影云桌面