(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 获取文件流,并将其内容置为 超链接,通过超链接的形式进行下载。(可以自定义文件名)
来源:oschina
链接:https://my.oschina.net/u/4352701/blog/4328951