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


    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)GoogleChromeApplicationchrome.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.data, "video/mp4")   // this.blobToFile(res.data, res.data.type)
                            }).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 获取文件流,并将其内容置为 超链接,通过超链接的形式进行下载。(可以自定义文件名)

  • 相关阅读:
    Java学习086Springboot 自定义启动 banner 信息
    Java学习085Springboot 解决 InetAddress.getLocalHost().getHostName() took 13387 milliseconds to respond. Please verify your network configuration
    PySe023pandas.read_csv 读取 csv 文件,指定列数据类型 解决字符串数据列变为数字的问题
    Linux027Centos JDK 环境离线安装配置
    Java学习087自定义MANIFEST.MF 文件并打包生效
    如何在跨平台的环境中创建可以跨平台的后台服务,它就是 Worker Service。
    如何为Windows服务增加Log4net和EventLog的日志功能。
    微服务与SOA的区别
    java:对象的内存解析
    快速学习一个新技术的方法
  • 原文地址:https://www.cnblogs.com/huoyz/p/14378934.html
Copyright © 2020-2023  润新知