累成一条狗

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(三): 整合阿里云 OSS 服务 -- 上传、下载文件、图片

(1) 相关博文地址:

SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p/12930895.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(二):引入 element-ui 定义基本页面显示:https://www.cnblogs.com/l-y-h/p/12935300.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(三):引入 js-cookie、axios、mock 封装请求处理以及返回结果:https://www.cnblogs.com/l-y-h/p/12955001.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(四):引入 vuex 进行状态管理、引入 vue-i18n 进行国际化管理:https://www.cnblogs.com/l-y-h/p/12963576.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(五):引入 vue-router 进行路由管理、模块化封装 axios 请求、使用 iframe 标签嵌套页面:https://www.cnblogs.com/l-y-h/p/12973364.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(六):使用 vue-router 进行动态加载菜单:https://www.cnblogs.com/l-y-h/p/13052196.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(一): 搭建基本环境、整合 Swagger、MyBatisPlus、JSR303 以及国际化操作:https://www.cnblogs.com/l-y-h/p/13083375.html
SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后端篇(二): 整合 Redis(常用工具类、缓存)、整合邮件发送功能:https://www.cnblogs.com/l-y-h/p/13163653.html

(2)代码地址:

https://github.com/lyh-man/admin-vue-template.git

 

一、使用阿里云 OSS 服务

1、简介

  OSS 为 Object Storage Service,即对象存储服务。是阿里云提供的海量、安全、低成本、高可靠的云存储服务。

【官方使用文档:】
    https://help.aliyun.com/document_detail/31817.html

【快速上手 OSS 参考:】
    https://www.cnblogs.com/l-y-h/p/12805028.html

 

2、使用 -- 开通 OSS 服务、创建 AccessKey

(1)登录网站、开通 OSS 服务
  通 OSS 服务,用于存储文件。

【官网地址:】
    https://www.aliyun.com/

 

 

 

(2)创建 bucket,用于保存文件。
Step1:
  进入 OSS 控制台,点击创建 bucket,用于创建文件保存空间。

 

 

 

Step2:
  填写 bucket 相关信息。(视财力选择功能)
注:
  读写权限可以根据项目需要,酌情选择。
  私有指的是 读写操作 均需要 进行身份的验证(此项目中使用)。
  公共读指的是 写操作需要进行身份验证,读操作不需要(即通过 url 可以直接访问)。

 

 

 

Step3:
  配置跨域访问(放行 post、get 等请求)。

 

 

 

 

 

 

(3)创建 AccessKey,用于获取操作 OSS 的权限。
Step1:
  点击 Accesskey,会弹出一个页面,点击 开始使用子用户 AccessKey。

 

 

 

 

 

 

Step2:
  创建用户(admin-vue-template),并选择编程访问。

 

 

 

Step3:
  保存 AccessKey 相关信息,后续会使用。
  建议保存在自己知道的地方,页面关闭后无法再次获取,只有重新创建了(=_=)。

【用户登录名称】 
    admin-vue-template@1675783906103019.onaliyun.com
    
【AccessKey ID】
     LTAI4GEWZbLZocBzXKYEfmmq

【SECRET】
     rZLsruKxWex2qGYVA3UsuBgW5B3uJQ

 

 

 

 

Step4:
  给创建的用户添加权限(OSS 权限)。

 

 

 

 

 

 

3、使用 -- 服务端上传代码

(1)创建一张表 back_oss,用于存储 文件 url 地址。

USE admin_template;

-- 文件上传
CREATE TABLE back_oss (
  id bigint NOT NULL COMMENT '文件 ID',
  file_url varchar(500) COMMENT 'URL 地址',
  oss_name varchar(200) COMMENT '存储在 OSS 中的文件名',
  file_name varchar(100) COMMENT '文件名',
  create_time datetime COMMENT '创建时间',
  PRIMARY KEY (id),
  UNIQUE INDEX (oss_name)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='文件上传';

 

 

 

(2)使用 mybatis-plus 代码生成器为该表生成基本代码。
  此处,我将代码生成在 modules/oss 中,也可生成在原来的路径中。

 

 

 

当然,对于 创建时间 这个字段,可以使用 mybatis-plus 的 @TableField 注解对其进行填充。
之前有过介绍,此处不再重复介绍
可参考:https://www.cnblogs.com/l-y-h/p/13083375.html#_label2_1

package com.lyh.admin_template.back.modules.oss.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

/**
 * <p>
 * 文件上传
 * </p>
 *
 * @author lyh
 * @since 2020-06-19
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="BackOss对象", description="文件上传")
public class BackOss implements Serializable {

    private static final long serialVersionUID=1L;

    @ApiModelProperty(value = "文件 ID")
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;

    @ApiModelProperty(value = "URL 地址")
    private String fileUrl;

    @ApiModelProperty(value = "存储在 OSS 中的文件名")
    private String ossName;

    @ApiModelProperty(value = "文件名")
    private String fileName;

    @TableField(fill = FieldFill.INSERT)
    @ApiModelProperty(value = "创建时间")
    private Date createTime;
}

 

 

 

注:
  由于 mapper 生成的位置与之前代码不一致,需要在配置文件中,对其进行扫描。

@MapperScan(basePackages = {"com.lyh.admin_template.back.mapper", "com.lyh.admin_template.back.modules.oss.mapper"})

 

 

 

(3)由于涉及到 阿里云 的相关配置信息,就要考虑到配置信息修改问题。
  处理一:可以使用 配置文件 存储,通过修改配置文件来修改 OSS 相关信息。


  处理二:可以使用数据库存储配置信息(Json 形式),通过修改数据库数据的方式对其进行修改。
数据表设计如下:

USE admin_template;

-- 系统配置信息
CREATE TABLE back_config (
   id bigint NOT NULL COMMIT '配置信息 ID',
   param_key varchar(50) COMMENT 'key',
   param_value varchar(2000) COMMENT 'value',
   status tinyint DEFAULT 1 COMMENT '状态   0:隐藏   1:显示',
   remark varchar(500) COMMENT '备注',
   PRIMARY KEY (id),
   UNIQUE INDEX (param_key)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COMMENT='系统配置信息表';

 

此处仅使用 处理一。在配置文件中填写相关的配置信息。

# 阿里云配置信息
aliyun:
  # common 配置信息
  accessKeyId: LTAI4GEWZbLZocBzXKYEfmmq
  accessKeySecret: rZLsruKxWex2qGYVA3UsuBgW5B3uJQ
  # OSS 相关配置信息
  endPoint: http://oss-cn-beijing.aliyuncs.com
  bucketName: admin-vue-template
  domain: http://admin-vue-template.oss-cn-beijing.aliyuncs.com

 

 

 

(4)添加 OSS 依赖

<!-- aliyun oss -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.8.0</version>
</dependency>

 

 

 

(5)编写一个 OSS 工具类 (OssUtil.java),通过其来操作 文件上传。
  通过 @Value 来获取配置文件(application.yml)中的值。
注:
  若使用 @Value 获取到的值为 null,需在类上 标注 @Component 注解。

package com.lyh.admin_template.back.common.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.UUID;

/**
 * Oss 工具类,用于操作 OSS
 */
@Data
@Component
public class OssUtil {
    @Value("${aliyun.endPoint}")
    private String endPoint;
    @Value("${aliyun.bucketName}")
    private String bucketName;
    @Value("${aliyun.accessKeyId}")
    private String accessKeyId;
    @Value("${aliyun.accessKeySecret}")
    private String accessKeySecret;
    @Value("${aliyun.domain}")
    private String domain;

    /**
     * 设置文件上传路径(prefix + 日期 + uuid + suffix)
     */
    public String getPath(String prefix, String suffix) {
        // 生成 UUID
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        // 格式化日期
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        // 拼接文件路径
        String path = dateTimeFormatter.format(LocalDateTime.now()) + "/" + uuid;
        if (StringUtils.isNotEmpty(prefix)) {
            path = prefix + "/" + path;
        }
        return path + "-" + suffix;
    }

    /**
     * 上传文件
     */
    public String upload(InputStream inputStream, String path) {
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 上传文件到 指定 bucket
            ossClient.putObject(bucketName, path, inputStream);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("上传文件失败");
        }
        return path;
    }

    /**
     * 上传文件
     */
    public String upload(byte[] data, String path) {
        return upload(new ByteArrayInputStream(data), path);
    }

    /**
     * 上传文件,自定义 前后缀
     */
    public String uploadSuffix(byte[] data, String prefix ,String suffix) {
        return upload(data, getPath(prefix, suffix));
    }

    /**
     * 上传文件,自定义 前后缀
     */
    public String uploadSuffix(InputStream inputStream, String prefix, String suffix) {
        return upload(inputStream, getPath(prefix, suffix));
    }

    /**
     * 获取文件 url
     */
    public String getUrl(String key) {
        // 用于保存 url 地址
        URL url = null;
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 设置 url 过期时间(10 年)
            Date expiration = new Date(new Date().getTime() + 1000L * 60 * 60 * 24 * 365 * 10);
            // 获取 url 地址
            url = ossClient.generatePresignedUrl(bucketName, key, expiration);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("获取文件 url 失败");
        }
        return url != null ? url.toString() : null;
    }
}

 

 

 

(6)编写 测试代码简单测试一下。
  使用 Swagger 简单测试一下(此处只上传单文件,可以使用 Swagger 进行测试,多文件可以使用 Postman 进行测试)。

package com.lyh.admin_template.back.modules.oss.controller;


import com.lyh.admin_template.back.common.exception.GlobalException;
import com.lyh.admin_template.back.common.utils.OssUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.modules.oss.entity.BackOss;
import com.lyh.admin_template.back.modules.oss.service.BackOssService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * <p>
 * 文件上传 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-06-19
 */
@RestController
@RequestMapping("/oss/back-oss")
@Api(tags = "文件上传")
public class BackOssController {

    @Autowired
    private OssUtil ossUtil;
    @Autowired
    private BackOssService backOssService;

    @ApiOperation(value = "上传文件")
    @PostMapping("/upload")
    public Result upload(@ApiParam MultipartFile file) {
        // 用于保存文件 url
        String url = null;
        // 用于保存文件信息
        BackOss backOss = new BackOss();
        try {
            // 获取文件上传路径
            url = ossUtil.uploadSuffix(file.getInputStream(), "aliyun", file.getOriginalFilename());

            // 保存文件路径到数据库中
            backOss.setFileName(file.getOriginalFilename());
            backOss.setOssName(url);
            backOss.setFileUrl(ossUtil.getUrl(url));
            backOssService.save(backOss);
        } catch (IOException e) {
            throw new GlobalException("文件上传失败");
        }
        return Result.ok().message("文件上传成功").data("file", backOss);
    }

    @ApiOperation(value = "获取所有文件信息")
    @GetMapping("/getAll")
    public Result getAll() {
        return Result.ok().data("file", backOssService.list());
    }
}

 

 

 

测试结果如下:

 

 

 

4、使用 -- 服务端签名后直传(vue + element-ui 方式传送文件)

(1)简介:
  前面的一种文件传输方式是将 文件 从前台传输到 后台,再由后台向 OSS 服务器传输。增加了后台服务器的压力(只适用于传输小文件、图片等)。
  采用服务端签名后直传的方式,是由 前台调用后台接口,返回一个签名数据,前台根据这个签名数据直接向 OSS 服务器发送文件(适合传输大文件)。
详情参考:
  https://www.cnblogs.com/l-y-h/p/12805028.html#_label2_3

(2)接口代码
  可以在 工具类 OssUtil.java 中把相关逻辑封装一下。
  逻辑参考下面代码中 getPolicy() 方法。

package com.lyh.admin_template.back.common.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Oss 工具类,用于操作 OSS
 */
@Data
@Component
public class OssUtil {
    @Value("${aliyun.endPoint}")
    private String endPoint;
    @Value("${aliyun.bucketName}")
    private String bucketName;
    @Value("${aliyun.accessKeyId}")
    private String accessKeyId;
    @Value("${aliyun.accessKeySecret}")
    private String accessKeySecret;
    @Value("${aliyun.domain}")
    private String domain;

    /**
     * 设置文件上传路径(prefix + 日期 + uuid + suffix)
     */
    public String getPath(String prefix, String suffix) {
        // 生成 UUID
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        // 格式化日期
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        // 拼接文件路径
        String path = dateTimeFormatter.format(LocalDateTime.now()) + "/" + uuid;
        if (StringUtils.isNotEmpty(prefix)) {
            path = prefix + "/" + path;
        }
        return path + "-" + suffix;
    }

    /**
     * 上传文件
     */
    public String upload(byte[] data, String path) {
        return upload(new ByteArrayInputStream(data), path);
    }

    /**
     * 上传文件,自定义 前后缀
     */
    public String uploadSuffix(byte[] data, String prefix ,String suffix) {
        return upload(data, getPath(prefix, suffix));
    }

    /**
     * 上传文件,自定义 前后缀
     */
    public String uploadSuffix(InputStream inputStream, String prefix, String suffix) {
        return upload(inputStream, getPath(prefix, suffix));
    }

    /**
     * 上传文件
     */
    public String upload(InputStream inputStream, String path) {
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 上传文件到 指定 bucket
            ossClient.putObject(bucketName, path, inputStream);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("上传文件失败");
        }
        return path;
    }

    /**
     * 获取文件 url
     */
    public String getUrl(String key) {
        // 用于保存 url 地址
        URL url = null;
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 设置 url 过期时间(10 年)
            Date expiration = new Date(new Date().getTime() + 1000L * 60 * 60 * 24 * 365 * 10);
            // 获取 url 地址
            url = ossClient.generatePresignedUrl(bucketName, key, expiration);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("获取文件 url 失败");
        }
        return url != null ? url.toString() : null;
    }

    /**
     * 用于获取签名数据
     */
    public Map<String, String> getPolicy() {
        return getPolicy(getPath("aliyun", "signature"));
    }

    /**
     * 用于获取签名数据,用于服务端直传文件到服务器
     */
    public Map<String, String> getPolicy(String path) {
        // 用于保存
        Map<String, String> map = new HashMap<>();
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 用于设置 post 上传条件
            PolicyConditions policyConditions = new PolicyConditions();
            // 设置最大上传文件大小(1G)
            policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            // 设置文件前缀
            policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, path);
            // 设置签名过期时间(6 小时)
            Date expiration = new Date(new Date().getTime() + 1000L * 60 * 60 * 6);
            // 生成 policy
            String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
            // 设置编码字符集(UTF-8)
            byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
            // 设置加密格式(Base64)
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            // 计算签名
            String postSignature = ossClient.calculatePostSignature(postPolicy);
            // 封装数据
            map.put("ossaccessKeyId", accessKeyId);
            map.put("policy", encodedPolicy);
            map.put("signature", postSignature);
            map.put("key", path);
            map.put("expire", String.valueOf(expiration.getTime() / 1000));
            map.put("host", domain);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("获取签名数据失败");
        }
        return map;
    }
}

 

(3)编写测试接口用于测试。

package com.lyh.admin_template.back.modules.oss.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.admin_template.back.common.utils.OssUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.modules.oss.entity.BackOss;
import com.lyh.admin_template.back.modules.oss.service.BackOssService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 文件上传 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-06-19
 */
@RestController
@RequestMapping("/oss/back-oss")
@Api(tags = "文件上传")
public class BackOssController {

    @Autowired
    private OssUtil ossUtil;
    @Autowired
    private BackOssService backOssService;

    @ApiOperation(value = "获取签名数据")
    @GetMapping("/policy")
    public Result policy() {
        return Result.ok().data("policyData", ossUtil.getPolicy());
    }

    @ApiOperation(value = "保存并获取文件 url")
    @PostMapping("/saveUrl")
    public Result saveUrl(@RequestParam String key, @RequestParam String fileName) {
        BackOss backOss = new BackOss();
        backOss.setOssName(key);
        backOss.setFileName(fileName);
        backOss.setFileUrl(ossUtil.getUrl(key));
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("oss_name", key);
        backOssService.saveOrUpdate(backOss, queryWrapper);
        return Result.ok().data("file", backOssService.getOne(queryWrapper));
    }
}

 

(4)前台代码(vue + element-ui):
  此处仅用于测试接口。并未整合到实际代码中(后续在整合到前台代码中)。
  此处采用普通 html,并引入 vue、element-ui 相关 cdn 进行演示。

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
        <!-- 引入样式 -->
        <link rel="stylesheet" href="https://unpkg.com/element-ui@2.3.7/lib/theme-chalk/index.css">
        <!-- 引入组件库 -->
        <script src="https://unpkg.com/element-ui@2.3.7/lib/index.js"></script>
    </head>
    <body>
        <div id="test">
            <!--
                使用 element-ui 上传组件 (el-upload)。
                action 用于指定上传的地址(必写)。
                on-preview 点击文件列表中文件触发。
                on-remove 移除文件列表中文件触发。
                before-upload 用于上传文件前触发(可用于检测文件大小、格式之类的)。
                on-success 文件上传成功后触发。
                on-error 文件上传失败触发。
                multiple 用于支持选择多文件。
                limit 表示每次可以选择的文件数目。
                on-exceed 文件数目超出限制时触发。
                file-list 表示上传文件列表。
                data 表示额外传递的参数。
                accept 用于指定格式(默认 *-->
            <el-upload :action="policyData.host" :on-preview="handlePreview" :on-remove="handleRemove" :before-upload="beforeUpload"
             :on-success="handleSuccess" :on-error="handleError" multiple :limit="3" :on-exceed="handleExceed" :file-list="fileList"
             :data="policyData" accept=".jpg,.JPG,.jpeg,.JPEG,.png,.PNG,.gif,.GIF">
                <el-button size="small" type="primary">点击上传</el-button>
                <!-- tip 表示提示文字 -->
                <div slot="tip">只能上传jpg/png/gif文件,且不超过 5 MB</div>
            </el-upload>
        </div>
        <script type="text/javascript">
            var vm = new Vue({
                el: "#test",
                data: {
                    fileList: [{
                        name: 'food.jpeg',
                        url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'
                    }],
                    policyData: {
                        "signature": "H3mPx51DPO73i0NJKTLgzvjX5dA=",
                        "expire": "1592989560",
                        "host": "http://admin-vue-template.oss-cn-beijing.aliyuncs.com",
                        "ossaccessKeyId": "LTAI4GEWZbLZocBzXKYEfmmq",
                        "key": "aliyun/20200624/be4678d9d2db4e5fb34f195e0b854615-",
                        "policy": "eyJleHBpcmF0aW9uIjoiMjAyMC0wNi0yNFQwOTowNjowMC45NDBaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJhbGl5dW4vMjAyMDA2MjQvYmU0Njc4ZDlkMmRiNGU1ZmIzNGYxOTVlMGI4NTQ2MTUtIl1dfQ=="
                    }
                },
                methods: {
                    handleRemove(file, fileList) {
                        console.log(file, fileList)
                    },
                    handlePreview(file) {
                        console.log(file)
                    },
                    handleExceed(files, fileList) {
                        this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
                    },
                    beforeUpload(file) {
                        let size = file.size / 1024 / 1024
                        let type = ".jpg,.JPG,.jpeg,.JPEG,.png,.PNG,.gif,.GIF".split(",");
                        let fileType = file.name.substring(file.name.lastIndexOf("."))
                        if (size > 5) {
                            this.$message.warning(`上传文件不能超过 5 M`)
                            return false
                        }
                        if (type.indexOf(fileType) === -1) {
                            this.$message.warning(`上传文件格式不正确`)
                            return false
                        }
                    },
                    handleSuccess(response, file, fileList) {
                        console.log(response)
                        console.log(file)
                        console.log(fileList)
                    },
                    handleError(error, file, fileList) {
                        console.log(error)
                        console.log(file)
                        console.log(fileList)
                    },
                }
            });
        </script>
    </body>
</html>

 

(5)测试接口:
  由于并未整合 axios 发送请求,所以手动通过 swagger 触发接口并将数据粘贴到相应地方进行测试。
  首先调用后台接口 policy 获取到签名数据,将该数据复制并替换前台代码 policyData 中。
  然后执行上传文件即可。
  最后调用 saveUrl 接口,将 url 以及文件信息保存到 数据库中。

 

 

 

5、json 数据显示问题 -- 日期少 8 小时、 id 值与数据库值不一致

(1)问题:
如下图所示:
  返回的 json 数据,可以看到 id 值与 日期值 与数据库有明显的区别。

 

 

 

 

 

 

(2)解决 id 与数据库不一致问题。
原因分析:
  由于后台代码,id 生成策略选择 type = IdType.ASSIGN_ID,其会通过雪花算法生成一个长的 Long 型数字。而这个数字传递到前台超过了 js 的数字存储范围,使数字精度丢失。

解决思路:
  在 Long 类型转为 Json 之前,将其 变为 String 类型,这样前台获取的即为 String 类型,从而保证精度。

解决方式一:(有局限性,需要对每个实体类进行标注)
  在实体类上标注 @JsonSerialize 注解,并指定序列化方式。

@TableId(value = "id", type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;

 

 

 

解决方式二:(通用)
  编写一个 Jackson2ObjectMapperBuilderCustomizer 对象,并交给 Spring 管理。

@Configuration
public class Config {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer builderCustomizer() {
        return builder -> {
            // 所有 Long 类型转换成 String 到前台
            builder.serializerByType(Long.class, ToStringSerializer.instance);
        };
    }
}

 

 

 

(3)解决日期少 8 小时问题。
原因分析:
  少 8 小时,即时区的问题。

解决思路:
  给其时区添加 8 小时,同时可以指定 日期输出格式。

解决方式一:(有局限性,需要对每个实体类进行标注)
  在实体类上标注 @JsonFormat 注解,并指定转换格式 以及 时区。

@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date createTime;

 

 

 

解决方式二:(通用)
  在 配置文件中,配置时区以及格式。

spring:
  # 设置 json 中日期显示格式
  jackson:
    # 设置显示格式
    date-format: yyyy-MM-dd HH:mm:ss
    # 设置时区
    time-zone: GMT+8

 

 

 

(4)再次获取数据。
  上面两个问题,本项目中均采用解决方式二去解决。

 

 

 

 

 

 

6、删除文件

(1)删除文件
  删除 oss 文件的同时也要删除数据库中的数据。

(2)代码实现:
Step1:
  在工具类中编写 oss 删除逻辑。

package com.lyh.admin_template.back.common.utils;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Oss 工具类,用于操作 OSS
 */
@Data
@Component
public class OssUtil {
    @Value("${aliyun.endPoint}")
    private String endPoint;
    @Value("${aliyun.bucketName}")
    private String bucketName;
    @Value("${aliyun.accessKeyId}")
    private String accessKeyId;
    @Value("${aliyun.accessKeySecret}")
    private String accessKeySecret;
    @Value("${aliyun.domain}")
    private String domain;

    /**
     * 设置文件上传路径(prefix + 日期 + uuid + suffix)
     */
    public String getPath(String prefix, String suffix) {
        // 生成 UUID
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        // 格式化日期
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        // 拼接文件路径
        String path = dateTimeFormatter.format(LocalDateTime.now()) + "/" + uuid;
        if (StringUtils.isNotEmpty(prefix)) {
            path = prefix + "/" + path;
        }
        return path + "-" + suffix;
    }

    /**
     * 上传文件
     */
    public String upload(byte[] data, String path) {
        return upload(new ByteArrayInputStream(data), path);
    }

    /**
     * 上传文件,自定义 前后缀
     */
    public String uploadSuffix(byte[] data, String prefix ,String suffix) {
        return upload(data, getPath(prefix, suffix));
    }

    /**
     * 上传文件,自定义 前后缀
     */
    public String uploadSuffix(InputStream inputStream, String prefix, String suffix) {
        return upload(inputStream, getPath(prefix, suffix));
    }

    /**
     * 上传文件
     */
    public String upload(InputStream inputStream, String path) {
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 上传文件到 指定 bucket
            ossClient.putObject(bucketName, path, inputStream);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("上传文件失败");
        }
        return path;
    }

    /**
     * 获取文件 url
     */
    public String getUrl(String key) {
        // 用于保存 url 地址
        URL url = null;
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 设置 url 过期时间(10 年)
            Date expiration = new Date(new Date().getTime() + 1000L * 60 * 60 * 24 * 365 * 10);
            // 获取 url 地址
            url = ossClient.generatePresignedUrl(bucketName, key, expiration);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("获取文件 url 失败");
        }
        return url != null ? url.toString() : null;
    }

    /**
     * 用于获取签名数据
     */
    public Map<String, String> getPolicy() {
        return getPolicy(getPath("aliyun", "signature"));
    }

    /**
     * 用于获取签名数据,用于服务端直传文件到服务器
     */
    public Map<String, String> getPolicy(String path) {
        // 用于保存
        Map<String, String> map = new HashMap<>();
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
            // 用于设置 post 上传条件
            PolicyConditions policyConditions = new PolicyConditions();
            // 设置最大上传文件大小(1G)
            policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            // 设置文件前缀
            policyConditions.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, path);
            // 设置签名过期时间(6 小时)
            Date expiration = new Date(new Date().getTime() + 1000L * 60 * 60 * 6);
            // 生成 policy
            String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions);
            // 设置编码字符集(UTF-8)
            byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
            // 设置加密格式(Base64)
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            // 计算签名
            String postSignature = ossClient.calculatePostSignature(postPolicy);
            // 封装数据
            map.put("ossaccessKeyId", accessKeyId);
            map.put("policy", encodedPolicy);
            map.put("signature", postSignature);
            map.put("key", path);
            map.put("expire", String.valueOf(expiration.getTime() / 1000));
            map.put("host", domain);
            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("获取签名数据失败");
        }
        return map;
    }

    /**
     * 删除 OOS 中的文件
     */
    public void deleteObject(String objectName) {
        try {
            // 创建 OSSClient 实例。
            OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);

            // 删除指定 bucket 中的文件
            ossClient.deleteObject(bucketName, objectName);

            // 关闭 OSSClient
            ossClient.shutdown();
        } catch (Exception e) {
            throw new RuntimeException("删除文件失败");
        }
    }
}

 

 

 

Step2:
  调用工具类,并删除数据库的数据。

package com.lyh.admin_template.back.modules.oss.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lyh.admin_template.back.common.utils.OssUtil;
import com.lyh.admin_template.back.common.utils.Result;
import com.lyh.admin_template.back.modules.oss.service.BackOssService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 文件上传 前端控制器
 * </p>
 *
 * @author lyh
 * @since 2020-06-19
 */
@RestController
@RequestMapping("/oss/back-oss")
@Api(tags = "文件上传")
public class BackOssController {

    @Autowired
    private OssUtil ossUtil;
    @Autowired
    private BackOssService backOssService;

    @ApiOperation(value = "删除文件")
    @DeleteMapping("/delete/object")
    public Result deleteObject(@RequestParam String key) {
        ossUtil.deleteObject(key);
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("oss_name", key);
        backOssService.remove(queryWrapper);
        return Result.ok();
    }
}

 

 

 

Step3:
  简单测试一下。
  先上传一个文件,然后根据其 oss_name 将文件删除。

 

 

 

7、下载文件

(1)最简单的方式:
  只支持部分类型(比如:image/jpeg),有些类型会直接打开(比如:video/mp4)
  直接使用 window.open(url) ,此时会触发浏览器下载功能(文件名默认不可更改)。

【举例:】
  【举例:】
  window.open("http://admin-vue-template.oss-cn-beijing.aliyuncs.com/aliyun/20200624/025a5edee3a34df19ed8b0d51d4c8053-signature?Expires=1908339884&OSSAccessKeyId=LTAI4GEWZbLZocBzXKYEfmmq&Signature=budNdWygT241vRNOVW9OCioZ4jQ%3D")

 

(2)使用 Blob 流处理文件
Step1:
  访问 url 地址时可能产生跨域问题。此处采用一个粗暴的方法,直接关闭 Chrome 安全策略进行测试(非必须操作)。
关闭 Chrome 安全策略(替换 chrome.exe 位置,命令行执行,会弹出一个浏览器窗口)

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --no-sandbox --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp

 

Step2:
  用 CDN 方式引入 axios 发送请求。
  此处只简单在 html 页面中使用并测试,项目中可以对其进行适当修改。

<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.js"></script>

 

Step3:
  编写 Blob 转为文件的逻辑。

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
        <!-- 引入样式 -->
        <link rel="stylesheet" href="https://unpkg.com/element-ui@2.3.7/lib/theme-chalk/index.css">
        <!-- 引入组件库 -->
        <script src="https://unpkg.com/element-ui@2.3.7/lib/index.js"></script>
        <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.js"></script>
    </head>
    <body>
        <div id="test">
            <!--
                使用 element-ui 上传组件 (el-upload)。
                action 用于指定上传的地址(必写)。
                on-preview 点击文件列表中文件触发。
                on-remove 移除文件列表中文件触发。
                before-upload 用于上传文件前触发(可用于检测文件大小、格式之类的)。
                on-success 文件上传成功后触发。
                on-error 文件上传失败触发。
                multiple 用于支持选择多文件。
                limit 表示每次可以选择的文件数目。
                on-exceed 文件数目超出限制时触发。
                file-list 表示上传文件列表。
                data 表示额外传递的参数。
                accept 用于指定格式(默认 *-->
            <el-upload :action="policyData.host" :on-preview="handlePreview" :on-remove="handleRemove" :before-upload="beforeUpload"
             :on-success="handleSuccess" :on-error="handleError" multiple :limit="3" :on-exceed="handleExceed" :file-list="fileList"
             :data="policyData" accept=".jpg,.JPG,.jpeg,.JPEG,.png,.PNG,.gif,.GIF">
                <el-button size="small" type="primary">点击上传</el-button>
                <!-- tip 表示提示文字 -->
                <div slot="tip">只能上传jpg/png/gif文件,且不超过 5 MB</div>
            </el-upload>
            <el-button size="small" type="primary" @click="download">下载</el-button>
        </div>
        <script type="text/javascript">
            var vm = new Vue({
                el: "#test",
                data: {
                    fileList: [{
                        name: 'food.jpeg',
                        url: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100'
                    }],
                    policyData: {
                        "signature": "Q5n13+6c7PZg2PKf6JgQ6rXvJbE=",
                        "expire": "1593001415",
                        "host": "http://admin-vue-template.oss-cn-beijing.aliyuncs.com",
                        "ossaccessKeyId": "LTAI4GEWZbLZocBzXKYEfmmq",
                        "key": "aliyun/20200624/025a5edee3a34df19ed8b0d51d4c8053-signature",
                        "policy": "eyJleHBpcmF0aW9uIjoiMjAyMC0wNi0yNFQxMjoyMzozNS4zNzJaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJhbGl5dW4vMjAyMDA2MjQvMDI1YTVlZGVlM2EzNGRmMTllZDhiMGQ1MWQ0YzgwNTMtc2lnbmF0dXJlIl1dfQ=="
                    }
                },
                methods: {
                    handleRemove(file, fileList) {
                        console.log(file, fileList)
                    },
                    handlePreview(file) {
                        console.log(file)
                    },
                    handleExceed(files, fileList) {
                        this.$message.warning(`当前限制选择 3 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`)
                    },
                    beforeUpload(file) {
                        let size = file.size / 1024 / 1024
                        let type = ".jpg,.JPG,.jpeg,.JPEG,.png,.PNG,.gif,.GIF".split(",");
                        let fileType = file.name.substring(file.name.lastIndexOf("."))
                        if (size > 5) {
                            this.$message.warning(`上传文件不能超过 5 M`)
                            return false
                        }
                        if (type.indexOf(fileType) === -1) {
                            this.$message.warning(`上传文件格式不正确`)
                            return false
                        }
                    },
                    handleSuccess(response, file, fileList) {
                        console.log(response)
                        console.log(file)
                        console.log(fileList)
                    },
                    handleError(error, file, fileList) {
                        console.log(error)
                        console.log(file)
                        console.log(fileList)
                    },
                    download() {
                        let url =
                            "https://admin-vue-template.oss-cn-beijing.aliyuncs.com/aliyun/20200624/025a5edee3a34df19ed8b0d51d4c8053-signature?Expires=1592995819&OSSAccessKeyId=TMP.3KjPgiYUJXZdW4yUwvDi2w58gCkdgAn2XFbExLPMgdWe4H6Y7JSzdVYxM5hiAn7PaKuBNG6zFhw9x2hB2GGTo5HPTXBwoY&Signature=4qZqZFgPW5NRYYHbCBRzgpZIpXA%3D"

                        let url2 =
                            "https://admin-vue-template.oss-cn-beijing.aliyuncs.com/aliyun/20200624/89481add65a04224b7ec97088e1ec2a7-test3.mp4?Expires=1592995837&OSSAccessKeyId=TMP.3KjPgiYUJXZdW4yUwvDi2w58gCkdgAn2XFbExLPMgdWe4H6Y7JSzdVYxM5hiAn7PaKuBNG6zFhw9x2hB2GGTo5HPTXBwoY&Signature=orq3cbgSz8Zgejv0Gsf0UMDyydw%3D"

                        axios.get(url, {
                            responseType: 'blob'
                        }).then(res => {
                            this.blobToFile(res.data, res.data.type)
                        }).catch(error => {
                            console.log(error)
                        })

                        axios.get(url2, {
                            responseType: 'blob'
                        }).then(res => {
                            console.log(res)
                            this.blobToFile(res, "video/mp4")
                        }).catch(error => {
                            console.log(error)
                        })
                    },

                    blobToFile(res, type) {
                        // res.data是后台返回的二进制数据,type:types为下载的数据类型
                        let blob = new Blob([res], {
                            type: type
                        })
                        let downLoadEle = document.createElement('a') 
                        let href = URL.createObjectURL(blob)
                        downLoadEle.href = href
                        // ooo为自定义文件名
                        downLoadEle.download = 'ooo'
                        document.body.appendChild(downLoadEle)
                        downLoadEle.click()
                        document.body.removeChild(downLoadEle) 
                        window.URL.revokeObjectURL(href)
                    }
                }
            });
        </script>
    </body>
</html>


Step4:
  测试效果如下,使用 get 请求,根据 url 获取文件流,并将其内容置为 超链接,通过超链接的形式进行下载。(可以自定义文件名)

 

posted on 2020-06-28 14:42  累成一条狗  阅读(202)  评论(0编辑  收藏

导航

统计