微服务[学成在线] day08:FastDFS 实现课程图片管理

微服务[学成在线] day08:FastDFS 实现课程图片管理

😎 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day08 的内容

  •  FastDFS 的 安装与基本使用流程。
  •  基于 Java Api 来操作 FastDFS
  •  结合 FastDFS 实现对课程图片的 CRUD
  •  图片删除的场景使用 Promise 解决一些问题

目录

内容会比较多,小伙伴们可以根据目录进行按需查阅。

一、FastDFS 探究

0x01 什么是fastDFS ?

1、介绍

FastDFS是用c语言编写的一款开源的分布式文件系统,它是由淘宝资深架构师余庆编写并开源。FastDFS专为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

为什么要使用 fastDFS 呢?

上边介绍的 NFSGFS 都是通用的分布式文件系统,通用的分布式文件系统的优点的是开发体验好,但是系统复杂性高、性能一般,而专用的分布式文件系统虽然开发体验性差,但是系统复杂性低并且性能高。fastDFS 非常适合存储图片等那些小文件,fastDFS 不对文件进行分块,所以它就没有分块合并的开销,fastDFS 网络通信采用 socket,通信速度很快。

2、工作原理

FastDFS 架构包括 Tracker serverStorageserver。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

1)Tracker

Tracker Server作用是负载均衡和调度,通过Tracker server在文件上传时可以根据一些策略找到Storage server提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

FastDFS 集群中的 Tracker server 可以有多台,Tracker server 之间是相互平等关系同时提供服务,Tracker server 不存在单点故障。客户端请求 Tracker server 采用轮询方式,如果请求的 tracker 无法提供服务则换另一个 tracker

2)Storage

Storage Server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是使用操作系统的文件系统来管理文件。可以将 storage 称为存储服务器。

Storage 集群采用了分组存储方式。storage 集群由一个或多个组构成,集群存储总容量为集群中所有组的存储容量之和。一个组由一台或多台存储服务器组成,组内的 Storage server 之间是平等关系,不同组的 Storage server 之间不会相互通信,同组内的 Storage server 之间会相互连接进行文件同步,从而保证同组内每个storage上的文件完全一致的。一个组的存储容量为该组内的存储服务器容量最小的那个,由此可见组内存储服务器的软硬件配置最好是一致的。

采用分组存储方式的好处是灵活、可控性较强。比如上传文件时,可以由客户端直接指定上传到的组也可以由 tracker 进行调度选择。一个分组的存储服务器访问压力较大时,可以在该组增加存储服务器来扩充服务能力(纵向扩容)。当系统容量不足时,可以增加组来扩充存储容量(横向扩容)。

3)Storage状态收集

Storage server 会连接集群中所有的 Tracker server,定时向他们报告自己的状态,包括磁盘剩余空间、文件同步状况、文件上传下载次数等统计信息。

3、文件上传流程

客户端上传文件后存储服务器将 文件ID 返回给客户端,此 文件ID 用于以后访问该文件的索引信息。文件索引信息,包括:组名,虚拟磁盘路径,数据两级目录,文件名。

  • 组名:文件上传后所在的 storage组名称,在文件上传成功后有 storage服务器返回,需要客户端自行保存。
  • 虚拟磁盘路径: storage配置的虚拟路径,与磁盘选项 store_path* 对应。如果配置了store_path0则是M00,如果配置了store_path1则是M01,以此类推。
  • 数据两级目录: storage服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
  • 文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

4、文件下载流程

tracker 根据请求的文件路径即文件ID 来快速定义文件。

比如请求下边的文件:

1.通过组名tracker能够很快的定位到客户端需要访问的存储服务器组是group1,并选择合适的存储服务器提供客户端访问。

2.存储服务器根据“文件存储虚拟磁盘路径”和“数据文件两级目录”可以很快定位到文件所在目录,并根据文件名找到客户端需要访问的文件。

0x02 FastDFS入门

1、环境搭建

安装 FastDFS 流程 参考: https://www.cnblogs.com/yufeng218/p/8111961.html

2、上传、下载、查询 测试

在工程下创建 test-fastdfs 模块

1)上传测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestFastDFS {
    /**
     * 测试文件上传
     */
    @Test
    public void testFileUpload(){
        try {
            //加载配置文件
            ClientGlobal.initByProperties("config/fastdfs-client.properties");
            //连接 Tracker
            TrackerClient trackerClient = new TrackerClient();
            TrackerServer trackerServer = trackerClient.getConnection();
            //获取 Storage
            StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
            //创建 Storage Client
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storeStorage);
            //向 Storage 服务器上传文件,拿到文件id
            String filePath = "d:/test1.html";
            String fileId = storageClient1.upload_file1(filePath, "html", null);
            System.out.println("上传成功:" + fileId);

        } catch (Exception ex){
            ex.printStackTrace();
        }
    }
}

在上述的代码中,我们通过访问 Trackerd:/test1.html 文件上传到了 Storage 服务器中,上传完成后得到了文件的完整的储存路径,如下:

group1/M00/00/00/CgEBmF6QKa-Abjv7AAAGqbcDljA60.html

我们可以在 Storage 中的物理 fdfs_storage/data 路径找到该文件

2)文件查询

/**
 * 文件信息查询
 */
@Test
public void TestFileInfoQuery(){
    try {
        //加载配置文件
        ClientGlobal.initByProperties("config/fastdfs-client.properties");
        //连接 Tracker
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        //获取 Storage
        StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
        //创建 Storage Client
        StorageClient1 storageClient1 = new StorageClient1(trackerServer, storeStorage);

        String fileId = "group1/M00/00/00/CgEBmF6QKa-Abjv7AAAGqbcDljA60.html";
        FileInfo fileInfo = storageClient1.query_file_info1(fileId);
        System.out.println("文件信息: "+ fileInfo);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

运行结果

3)文件下载

/**
 * 文件下载
 */
@Test
public void TestFileDownload(){
    try {
        //加载配置文件
        ClientGlobal.initByProperties("config/fastdfs-client.properties");
        //连接 Tracker
        TrackerClient trackerClient = new TrackerClient();
        TrackerServer trackerServer = trackerClient.getConnection();
        //获取 Storage
        StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
        //创建 Storage Client
        StorageClient1 storageClient1 = new StorageClient1(trackerServer, storeStorage);

        String fileId = "group1/M00/00/00/CgEBmF6QKa-Abjv7AAAGqbcDljA60.html";
        String saveToPath = "d:/test.html";
        //下载文件
        byte[] bytes = storageClient1.download_file1(fileId);
        FileOutputStream fileOutputStream = new FileOutputStream(new File(saveToPath));
        fileOutputStream.write(bytes);
        System.out.println("下载成功! " + saveToPath);
    } catch (Exception ex){
        ex.printStackTrace();
    }
}

运行结果

3、搭建图片虚拟主机

1) 在storage上安装Nginx

storage server 上安装 nginx 的目的是对外通过 http 访问 storage server 上的文 件。使用 nginx 的模块 FastDFS-nginx-module 的作用是通过 http 方式访问 storage 中 的文件,当 storage 本机没有要找的文件时向源storage 主机代理请求文件。

在storage上安装nginx(安装 FastDFS-nginx-module 模块)

参考:FastDFS安装教程.pdf 进行安装

安装完成启动 storage上的 nginx

/usr/local/nginx/sbin/nginx ‐c /usr/local/nginx/conf/nginx‐fdfs.conf

2)配置 Nginx 的图片服务的虚拟主机

图片服务虚拟主机的作用是负载均衡,将图片请求转发到 storage server 上。

1、 通过图片服务虚拟主机请求图片流程图

2、在 nginx 图片代理服务上配置图片服务器虚拟主机

修改本地的 hosts 文件,将 img.xuecheng.com 映射到本地IP 127.0.0.1

windows 的 hosts 文件路径为:C:\Windows\System32\drivers\etc

127.0.0.1 img.xuecheng.com

增加 nginx 虚拟机主机

	#图片服务
	upstream img_server_pool{
		#server 192.168.101.64:80 weight=10;
		server 10.1.1.152:80 weight=10;
	} 
	#学成网图片服务
	server {
		listen       80;    
		server_name img.xuecheng.com;    

		#个人中心    
		location /group1 {      
			proxy_pass http://img_server_pool;          
		}     
		location /group2 {      
			proxy_pass http://img_server_pool;          
		}     
	}

3、测试访问

http://img.xuecheng.com/group1/M00/00/00/CgEBmF6QKa-Abjv7AAAGqbcDljA60.html

成功映射到 storage 服务上

4、总结

通过本次课程的学习您要达到以下目标:

1)了解分布式文件系统的概念及应用场景

分布式文件系统是通过网络将单机上的文件系统组成一个网络文件系统。

分布式文件系统主要应用在大型互联网项目中,实现图片存储、音视频存储等服务。

分布式文件系统的优点:可以快速扩容存储,提高文件访问速度。

2)理解fastDFS的工作原理

fastDFS 由 trackerstorage 组成,它们都可以部署集群。

tracker 负责调度,storage 负责存储。

3)掌握 fastDFS 存取文件方法

客户端与 fastDFS 采用 socket 协议通信,可以采用官方提供的 java版本的 fastDSF-client快速开发。

4)能够动手搭建一个 fastDSF 文件服务器

二、上传课程图片

0x01 需求分析

在很多系统都有上传图片/上传文件的需求,比如:上传课程图片、上传课程资料、上传用户头像等,为了提供系统的可重用性专门设立文件系统服务承担图片/文件的管理,文件系统服务实现对文件的上传、删除、查询等功能进行管理。

各个子系统不再开发上传文件的请求,各个子系统通过文件系统服务进行文件的上传、删除等操作。文件系统服务最终会将文件存储到 fastDSF 文件系统中。

下图是各各子系统与文件系统服务之间的关系:

下图是课程管理中上传图片处理流程:

执行流程如下:

1、管理员进入教学管理前端,点击上传图片

2、图片上传至文件系统服务,文件系统请求 fastDFS 上传文件

3、文件系统将文件入库,存储到文件系统服务数据库中。

4、文件系统服务向前端返回文件上传结果,如果成功则包括文件的 Url 路径。

5、课程管理前端请求 课程管理 进行保存课程图片信息到课程 数据库。

6、课程管理服务将课程图片保存在 课程数据库。

0x02 创建工程

导入xc-service-base-filesystem.zip工程。

1)工程目录结构如下

image-20200410193248194

1)pom.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>xc-framework-parent</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../xc-framework-parent/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>xc-service-base-filesystem</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-model</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xc-framework-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>net.oschina.zcx7878</groupId>
            <artifactId>fastdfs-client-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
    </dependencies>

</project>

2)配置文件

server:
  port: 22100
spring:
  application:
    name: xc-service-base-filesystem
#mongo配置
  data:
    mongodb:
      database: xc_fs
      uri: mongodb://root:123123@127.0.0.1:27017
#SpringMVC上传文件配置
  servlet:
    multipart:
      #默认支持文件上传.
      enabled: true
      #支持文件写入磁盘.
      file-size-threshold: 0
      # 上传文件的临时目录
      location:
      # 最大支持文件大小
      max-file-size: 1MB
      # 最大支持请求大小
      max-request-size: 30MB
xuecheng:
  fastdfs:
    connect_timeout_in_seconds: 5
    network_timeout_in_seconds: 30
    charset: UTF-8
    tracker_servers: 192.168.101.65:22122 #多个 trackerServer中间以逗号分隔

0x03 构建接口

系统的文件信息(图片、文档等小文件的信息)在mongodb中存储,下边是文件信息的模型类。

1、模型类

1)模型类如下

@Data
@ToString
@Document(collection = "filesystem")
public class FileSystem {
    @Id
    private String fileId;
    //文件请求路径
    private String filePath;
    //文件大小
    private long fileSize;
    //文件名称
    private String fileName;
    //文件类型
    private String fileType;
    //图片宽度
    private int fileWidth;
    //图片高度
    private int fileHeight;
    //用户id,用于授权暂时不用
    private String userId;
    //业务key
    private String businesskey;
    //业务标签
    private String filetag;
    //文件元信息
    private Map metadata;
}

说明:

  • fileId:fastDFS 返回的文件 ID。

  • filePath:请求fastDFS浏览文件 URL。

  • filetag:文件标签,由于文件系统服务是公共服务,文件系统服务会为使用文件系统服务的子系统分配文件标签,用于标识此文件来自哪个系统。

  • businesskey:文件系统服务为其它子系统提供的一个业务标识字段,各子系统根据自己的需求去使用,比如:课程管理会在此字段中存储课程 id 用于标识该图片属于哪个课程。

  • metadata:文件相关的元信息。

2)数据库 collection 集合

mongodb 创建数据库xc_fs(文件系统数据库),并创建集合 filesystem

2、API 接口

@Api(value = "文件管理接口", description = "文件管理接口,提供对文件的CRUD")
public interface FileSystemControllerApi {

    @ApiOperation("上传文件接口")
    public UploadFileResult uploadFile(MultipartFile multipartFile,
                                       String fileTage,
                                       String businessKey,
                                       String metaData);

}

3、Dao 层

dao 层继承 Spring Data Mongodb 的 API

package com.xuecheng.filesystem.dao;

import com.xuecheng.framework.domain.filesystem.FileSystem;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface FileSystemRepository extends MongoRepository<FileSystem,String> {

}

4、Service 层

package com.xuecheng.filesystem.service;

import com.alibaba.fastjson.JSON;
import com.xuecheng.filesystem.dao.FileSystemRepository;
import com.xuecheng.framework.domain.filesystem.FileSystem;
import com.xuecheng.framework.domain.filesystem.response.FileSystemCode;
import com.xuecheng.framework.domain.filesystem.response.UploadFileResult;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import org.apache.commons.lang3.StringUtils;
import org.csource.common.MyException;
import org.csource.fastdfs.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Map;

@Service
public class FileSystemService {
    @Value("${xuecheng.fastdfs.tracker_servers}")
    String tracker_servers;
    @Value("${xuecheng.fastdfs.connect_timeout_in_seconds}")
    int connect_timeout_in_seconds;
    @Value("${xuecheng.fastdfs.network_timeout_in_seconds}")
    int network_timeout_in_seconds;
    @Value("${xuecheng.fastdfs.charset}")
    String charset;

    @Autowired
    FileSystemRepository fileSystemRepository;

    /**
     * 上传文件到FastDFS
     * @param multipartFile 文件数据
     * @param fileTag 文件标签
     * @param businessKey 业务key
     * @param metaData 文件元数据
     * @return
     */
    public UploadFileResult uploadFile(MultipartFile multipartFile,
                                       String fileTag,
                                       String businessKey,
                                       String metaData){
        //验证提交的文件是否为空
        if(multipartFile == null){
            ExceptionCast.cast(FileSystemCode.FS_UPLOADFILE_FILEISNULL);
        }

        //第一步:将文件上传到 FastDFS中
        String fileId = this.fdfsUpload(multipartFile);
        if(StringUtils.isEmpty(fileId)){    //上传文件为空时抛出异常
            ExceptionCast.cast(FileSystemCode.FS_UPLOADFILE_SERVERFAIL);
        }

        //第二步:将文件储存到mongoDB内

        //设置相关的文件信息
        FileSystem fileSystem = new FileSystem();
        fileSystem.setFileId(fileId);
        fileSystem.setFileName(multipartFile.getOriginalFilename());
        fileSystem.setBusinesskey(businessKey);
        fileSystem.setFilePath(fileId); //FastDFS的fileId就是实际的物理路径
        fileSystem.setFileSize(multipartFile.getSize());
        fileSystem.setFiletag(fileTag);
        fileSystem.setFileType(multipartFile.getContentType());
        //文件元数据需要转换成Map对象
        if(StringUtils.isNotEmpty(metaData)){
            Map metaDataMap = JSON.parseObject(metaData, Map.class);
            fileSystem.setMetadata(metaDataMap);
        }
        FileSystem save = fileSystemRepository.save(fileSystem);
        return new UploadFileResult(CommonCode.SUCCESS,save);
    }

    //上传文件到FastDFS
    private String fdfsUpload(MultipartFile multipartFile){
        //初始化FastDFS的环境
        try {
            initFdfsConfig();
            TrackerClient trackerClient = new TrackerClient();
            TrackerServer trackerServer = trackerClient.getConnection();
            //得到 Storage 连接信息
            StorageServer storeStorage = trackerClient.getStoreStorage(trackerServer);
            //获取 Storage 客户端
            StorageClient1 storageClient1 = new StorageClient1(trackerServer, storeStorage);

            //上传文件
            byte[] fileBytes = multipartFile.getBytes();   // 获取文件信息
            String originalFilename = multipartFile.getOriginalFilename();
            // 获取文件拓展名
            String extStr = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
            //上传文件
            String fileId = storageClient1.upload_file1(fileBytes, extStr, null);
            return fileId;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //初始化FastDFS环境
    private void initFdfsConfig(){
        try {
              ClientGlobal.initByTrackers(tracker_servers);
              ClientGlobal.setG_connect_timeout(connect_timeout_in_seconds);
              ClientGlobal.setG_network_timeout(network_timeout_in_seconds);
              ClientGlobal.setG_charset(charset);
        } catch (Exception e) {
            e.printStackTrace();
            //抛出异常
            ExceptionCast.cast(FileSystemCode.FS_INITFDFS_ERROR);
        }
    }
}

5、Controller 层

package com.xuecheng.filesystem.controller;

@RestController
@RequestMapping("/filesystem")
public class FileSystemController implements FileSystemControllerApi {
    @Autowired
    FileSystemService fileSystemService;
    @Override
    @PostMapping("/upload")
    public UploadFileResult uploadFile(MultipartFile multipartFile, String fileTag, String businessKey, String metaData) {
        return fileSystemService.uploadFile(multipartFile,fileTag,businessKey,metaData);
    }
}

6、测试

在 Swagger UI 生成的接口文档内进行测试 http://localhost:22100/swagger-ui.html

0x04 前端配置

1、需求

上传图片界面如下图:

点击“加号”上传图片,图片上传成功自动显示;点击“删除”将删除图片。

2、页面

使用 Element-UIUpload上传组件实现上边的效果。

1) template

<el‐upload
  action="/filesystem/upload"
  list‐type="picture‐card"
  name="multipartFile" 
  :before‐upload="setbusinesskey"
  :on‐success="handleSuccess"
  :file‐list="fileList"
  :limit="picmax"
  :on‐exceed="rejectupload"
  :data="uploadval">
  <i class="el‐icon‐plus"></i>
</el‐upload>

el-upload参数说明:

  • action:必选参数,上传的地址
  • list-type:文件列表的类型(text/picture/picture-card)
  • name: 提交的文件参数名称,这里要跟后端接收的对应上
  • before-upload:上传前执行钩子方法 ,function(file)
  • on-success:上传成功 执行的钩子方法 ,function(response, file, fileList)
  • on-error:上传失败的钩子方法,function(err, file, fileList)
  • on-remove:文件删除的钩子方法,function(file, fileList)
  • file-list:文件列表,此列表为上传成功 的文件
  • limit:最大允许上传个数
  • on-exceed:文件超出个数限制时的钩子,方法为:function(files, fileList)
  • data:提交上传的额外参数,需要封装为json对象,最终提交给服务端为key/value串

2) 数据模型

<script>
  import * as sysConfig from '@/../config/sysConfig';
  import * as courseApi from '../../api/course';
  import utilApi from '../../../../common/utils';
  import * as systemApi from '../../../../base/api/system';
  export default {
    data() {
      return {
        picmax:1,
        courseid:'',
        dialogImageUrl: '',
        dialogVisible: false,
        fileList:[],
        uploadval:{filetag:"course"},
        imgUrl:sysConfig.imgUrl
      }
    },
    methods: {
      //超出文件上传个数提示信息
      rejectupload(){
        this.$message.error("最多上传"+this.picmax+"个图片");
      },
      //在上传前设置上传请求的数据
      setuploaddata(){
      },
      //删除图片
      handleRemove(file, fileList) {
        console.log(file)
        alert('删除')
      },
      //上传成功的钩子方法
      handleSuccess(response, file, fileList){
        console.log(response)
        alert('上传成功')
      },
      //上传失败执行的钩子方法
      handleError(err, file, fileList){
        this.$message.error('上传失败');
        //清空文件队列
        this.fileList = []
      }
    },
    mounted(){
      //课程id
      this.courseid = this.$route.params.courseid;
    }
  }
</script>

3、测试

1、点击“加号”测试上传图片

三、保存课程图片

0x01 需求分析

图片上传到文件系统后,其它子系统如果想使用图片可以引用图片的地址,课程管理模块使用图片的方式是将图片地址保存到课程数据库中。

业务流程如下:

1、上传图片到文件系统服务

2、保存图片地址到课程管理服务在课程管理服务创建保存课程与图片对应关系的表 course_pic

3、在 course_pic 保存图片成功后方可查询课程图片信息。

通过查询 course_pic 表数据则查询到某课程的图片信息。

0x02 构建接口

1、API 接口

com.xuecheng.api.course 包下创建该接口

@ApiOperation("保存课程图片信息")
public ResponseResult saveCoursePic(String courseId, String pic);

2、Dao 层

由于是对 mysql 的单表操作,这里我们通过继承 spring data jpa 提供的接口来实现 dao

package com.xuecheng.manage_course.dao;

import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CoursePic;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Administrator.
 */
public interface CoursePicRepository extends JpaRepository<CoursePic,String> {

}

3、Service 层

CourseService 下新增 saveCoursePic 方法

/**
 * 保存课程图片信息到数据库
 * @param courseId 课程id
 * @param pic 图片id
 * @return
 */

@Transactional  //Mysql操作需要添加到Spring事务
public ResponseResult saveCoursePic(String courseId, String pic) {
    CoursePic coursePic = null;
    //判断该课程id是否已经存在图片
    Optional<CoursePic> byId = coursePicRepository.findById(courseId);
    if(byId.isPresent()){
        coursePic = byId.get();
    }
    //不存在则重新创建一个课程图片对象并保存信息
    if(coursePic == null){
        coursePic = new CoursePic();
    }

    coursePic.setCourseid(courseId);
    coursePic.setPic(pic);
    CoursePic save = coursePicRepository.save(coursePic);
    return new ResponseResult(CommonCode.SUCCESS);
}

4、Controller 层

CourseContorller 下新增 saveCoursePic 方法

/**
 * 保存课程信息与图片的对应关系
 * @param courseId 课程id
 * @param pic 图片文件id
 * @return
 */
@Override
@PostMapping("/coursepic/add")
public ResponseResult saveCoursePic(@RequestParam("courseId") String courseId, @RequestParam("pic") String pic) {
    return courseService.saveCoursePic(courseId,pic);
}

0x03 前端配置

1、需求

前端需要在上传图片成功后保存课程图片信息。

2、页面

在 course.js 下创建 ajax 请求的 api

//保存课程图片地址到课程数据 库
export const addCoursePic= (courseId,pic) => {
  return http.requestPost(apiUrl+'/course/coursepic/add?courseId='+courseId+"&pic="+pic)
}

el-upload 组件在上传成功后,会调用上传成功的钩子方法,配置如 :on-success="handleSuccess"

钩子方法如下:

//上传成功的钩子方法
handleSuccess(response, file, fileList){
    console.log(response)
    //调用课程管理的保存图片接口,将图片信息保存到课程管理数据库course_pic中
    //从response得到新的图片文件的地址
    if(response.success){
        let fileId = response.fileSystem.fileId;
        courseApi.addCoursePic(this.courseid,fileId).then(res=>{
            if(res.success){
                this.$message.success("图片上传成功")
            }else{
                this.$message.error("图片保存失败!")
            }
        })
    }
},

3、测试

上传成功

四、图片信息查询

0x01 需求分析

在进入到 课程图片 页面之前,根据当前课程的id,找到该课程对应的课程图片。

0x02 构建接口

1、API 接口

@ApiOperation("获得课程图片信息")
public CoursePic getCoursePic(String courseId);

2、Dao 层

CoursePicRepository ,在编写保存功能时已构建。

3、Service 层

/**
 * 根据课程id获得课程的图片信息
 * @param courseId
 * @return
 */
@Transactional //Mysql操作需要添加到Spring事务
public CoursePic getCoursePic(String courseId) {
    Optional<CoursePic> byId = coursePicRepository.findById(courseId);
    if(byId.isPresent()){
        CoursePic coursePic = byId.get();
        return coursePic;
    }
    return null;
}

4、Controller 层

/**
 * 根据课程id获取该课程的课程图片信息
 * @param courseId
 * @return 由于这里每个课程只有一个图片,所以只返回一个 CoursePic 对象
 */
@Override
@GetMapping("/coursepic/get/{courseId}")
public CoursePic getCoursePic(@PathVariable("courseId") String courseId) {
    return courseService.getCoursePic(courseId);
}

5、测试

0x03 前端配置

1、需求

在课程图片页面的 mounted 钩子方法 中查询课程图片信息,并将图片地址赋值给数据对象

2、页面

API 接口

//查询课程图片
export const findCoursePicList = courseId => {
  return http.requestQuickGet(apiUrl+'/course/coursepic/get/'+courseId)
}

钩子函数

mounted(){
  //课程id
  this.courseid = this.$route.params.courseid;
  //查询课程
  courseApi.findCoursePicList(this.courseid).then(res=>{
      if(res && res.pic){
          let imgUrl = this.imgUrl+res.pic;
          //将图片地址设置到
        this.fileList.push({name:'pic',url:imgUrl,fileId:res.pic})
      }
  })
}

3、测试

别忘了启动 nginx ,这里是通过 img.xuecheng.com 访问图片。

五、课程图片删除

0x01 需求分析

0x02 构建接口

1、API 接口

@ApiOperation("删除课程图片信息")
public ResponseResult deleteCoursePic(String courseId);

2、Dao 层

JPA 自带的 deleteById 方法没有返回值,所以这里我们需要在 Dao 里面再定义一个 deleteByCourseid 方法,删除后返回影响的行数。

package com.xuecheng.manage_course.dao;

import com.xuecheng.framework.domain.course.CourseBase;
import com.xuecheng.framework.domain.course.CoursePic;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Administrator.
 */
public interface CoursePicRepository extends JpaRepository<CoursePic,String> {
    /**
     * 根据课程id删除图片信息
     * @param courseId
     * @return 返回删除影响的行,小于1则表示删除失败
     */
    long deleteByCourseid(String courseId);
}

3、Service 层

/**
 * 删除课程图片信息
 * @param courseId
 * @return
 */
@Transactional //Mysql操作需要添加到Spring事务
public ResponseResult deleteCoursePic(String courseId) {
    long byCourseid = coursePicRepository.deleteByCourseid(courseId);
    if(byCourseid > 0){
        return new ResponseResult(CommonCode.SUCCESS);
    }
    return new ResponseResult(CommonCode.FAIL);
}

4、Controller 层

/**
 * 删除课程图片信息
 * @param courseId
 * @return
 */
@Override
@DeleteMapping("/coursepic/delete")
public ResponseResult deleteCoursePic(@RequestParam("courseId") String courseId) {
    return courseService.deleteCoursePic(courseId);
}

5、测试

0x03 前端配置

1、需求

2、页面

api 接口

//删除课程图片
export const deleteCoursePic= courseId => {
  return http.requestDelete(apiUrl+'/course/coursepic/delete?courseId='+courseId)
}

before-remove钩子方法

<el‐upload
  action="/filesystem/upload"
  list‐type="picture‐card"
  :before‐remove="handleRemove">
  <i class="el‐icon‐plus"></i>
</el‐upload>

在upload组件的 before-remove钩子方法 中实现删除动作。

//删除图片
handleRemove(file, fileList) {
  console.log(file)
  //调用服务端去删除课程图片信息,如果返回false,前端停止删除
  //异步调用
  return new Promise((resolve,rejct)=>{
    courseApi.deleteCoursePic(this.courseid).then(res=>{
      if(res.success){
          //成功
        resolve()
      }else{
        this.$message.error("删除失败");
          //失败
        rejct()
      }
    })
  })
},

before-remove 说明:删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject,则停止删除。

3、为什么要使用 promise?

handleRemove 方法调用删除图片的 api 方法,删除成功时 return true,删除失败时return false;

// 删除图片
handleRemove(file, fileList) {
    console.log(file)
    //        alert('删除')
    //        return true;
    //删除图片
    courseApi.deleteCoursePic('1').then((res) => {
        if(res.success){
            this.$message.success('删除成功');
            return true;
        }else{
            this.$message.error(res.message);
            return false;
        }
    });
},

在上边代码中将提交的课程 id 故意写错,按照我们预期应该是删除失败,而测试结果却是图片在页面上删除成功。

问题原因:

通过查询 deleteCoursePic 方法的底层代码,deleteCoursePic 最终返回一个 promise 对象。

Promise 是ES6提供的用于异步处理的对象,因为axios提交是异步提交,这里使用promise作为返回值

Promise的使用方法如下:

Promise对象在处理过程中有三种状态:

  • pending:进行中

  • resolved:操作成功

  • rejected: 操作失败

Promise的构建方法如下:

const promise = new Promise(function(resolve,reject){
     //...TODO...
    if(操作成功){
        resolve(value);
    }else{
        reject(error);
    }
})

上边的构造方法function(resolve,reject)执行流程如下:

1)方法执行一些业务逻辑。

2)如果操作成功将 Promise 的状态由 pending 变为 resolved,并将操作结果传出去

3)如果操作失败会将 promise 的状态由 pending 变为 rejected,并将失败结果传出去。
上边说的操作成功将操作结果传给谁了呢?操作失败将失败结果传给谁了呢?

我们可以通过 promise 的 thencatch 来指定:

promise.then(function (result) {
    console.log('操作成功:' + result);
});
promise.catch(function (reason) {
    console.log('操作失败:' + reason);
});

例子如下:

1、定义一个方法,返回 promise 对象

testpromise(i){
  return new Promise((resolve,reject)=>{
    if(i % 2==0){
      resolve('成功了')
    }else{
      reject('拒绝了')
    }
  })
}

2、调用此方法

向方法传入偶数、奇数进行测试。

this.testpromise(3).then(res=>{// 在then中对成功结果进行处理
    alert(res)
}).catch(res=>{//在catch中对操作失败结果进行处理
    alert(res)
})

3、最终将 handleRemove 方法修改如下

handleRemove 方法返回 promise 对象,当删除成功则 resolve,删除失败则 reject

// 删除图片
handleRemove(file, fileList) {
    console.log(file)
    return new Promise((resolve,reject)=>{
        //删除图片
 courseApi.deleteCoursePic(this.courseid).then((res) => {
            if(res.success){
                this.$message.success('删除成功');
                resolve()//通过
            }else{
                this.$message.error(res.message);
                reject()//拒绝
            }
        });
    })
}

😁 认识作者

作者:👦 LCyee ,一个向往体面生活的代码🐕

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

CSDN 博客:https://blog.csdn.net/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://codeyee.com/archives/xuecheng-day08-fastdfs.html