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 获取文件流,并将其内容置为 超链接,通过超链接的形式进行下载。(可以自定义文件名)