SpringBoot+Vue.js实现大文件分片上传、断点续传与极速秒传
亲测好用,这里就直接上代码了,代码有详细的解释。
0. 建表语句
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for file_info
-- ----------------------------
DROP TABLE IF EXISTS `file_info`;
CREATE TABLE `file_info` (
`id` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'id',
`file_path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '相对路径',
`file_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件名',
`suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '后缀',
`file_size` int(11) NULL DEFAULT NULL COMMENT '大小|字节B',
`file_use` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用途|枚举[FileUseEnum]:COURSE(\'C\', \'讲师\'), TEACHER(\'T\', \'课程\')',
`created_at` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`shard_index` int(11) NULL DEFAULT NULL COMMENT '已上传分片',
`shard_total` int(11) NULL DEFAULT NULL COMMENT '分片总数',
`shard_size` int(11) NULL DEFAULT NULL COMMENT '分片大小|B',
`file_key` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件标识',
`vod` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'vod|阿里云vod',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `path_unique`(`file_path`) USING BTREE,
UNIQUE INDEX `key_unique`(`file_key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文件' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
1. vue代码
<template>
<div>
<el-card class="box-card">
<el-row>
<el-col :span="6">
<el-upload
class="upload-vhr"
action="no"
list-type="text"
ref="uploadFile"
accept="no"
:auto-upload="false"
:on-exceed="handleExceed"
:http-request="customUpload"
:on-change="handleChange"
:on-remove="handleRemove"
:limit="1"
:file-list="fileList">
<el-input placeholder="请输入内容" v-model="fileName">
<template slot="append">
<el-button type="primary" icon="el-icon-folder-opened">
选择文件
</el-button>
</template>
</el-input>
<!--<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>-->
</el-upload>
</el-col>
<el-col :span="6">
<el-button type="primary" icon="el-icon-folder-opened" @click="submitUpload">
提交
</el-button>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script>
import {hex_md5} from "@/utils/md5.js";
export default {
name: "EmpAdv",
/*props: {
afterUpload: {
type: Function,
default: null
},
},*/
data() {
return {
file: "",
fileList: [],
fileName: "",
url: {
upload: "/file/upload",
check: "/file/check"
}
};
},
methods: {
submitUpload() {
if (this.fileList == '') {
this.$message.warning("请选择需要上传的文件!")
} else {
// 调用文件上传的钩子函数
this.$refs.uploadFile.submit();
this.fileList = []
}
},
//自定义上传文件钩子,发送上传文件请求
customUpload() {
let file = this.file;
let key = hex_md5(file.name + file.size + file.type);
let suffix = file.name.substr(file.name.lastIndexOf(".") + 1).toLowerCase();
// 文件分片
let shardSize = 20 * 1024 * 1024; // 以20M为一个分片
let shardIndex = 1; //分片索引, 1表示第一个分片
let size = file.size;
let shardTotal = Math.ceil(size / shardSize);
let param = {
"shardIndex": shardIndex,
"shardSize": shardSize,
"shardTotal": shardTotal,
"fileUse": "C",
"fileName": file.name,
"suffix": suffix,
"fileSize": size,
"fileKey": key
}
this.check(param);
},
/**
* 检查文件状态,是否已上传过?传到第几个分片?
*/
check(param) {
this.getRequest(this.url.check, {"fileKey": param.fileKey}).then(resp => {
if (resp && resp.status) {
let obj = resp.data;
if (!obj) {
param.shardIndex = 1;
console.log("没有找到文件记录,从分片1开始上传");
this.upload(param);
} else if (obj.shardIndex === obj.shardTotal) {
// 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
this.$message.success("文件极速秒传成功!");
} else {
param.shardIndex = obj.shardIndex + 1;
console.log("找到文件记录,从分片" + param.shardIndex + "开始上传");
this.upload(param);
}
} else {
this.$message.error("文件上传失败");
}
})
},
upload(param) {
let shardIndex = param.shardIndex;
let shardTotal = param.shardTotal;
let shardSize = param.shardSize;
let fileShard = this.getFileShard(shardIndex, shardSize);
// 将图片转为 base64 进行传输
let fileReader = new FileReader();
fileReader.onload = (e => {
let base64 = e.target.result;
param.shard = base64;
this.postRequest(this.url.upload, param).then(resp => {
if (resp && resp.status) {
this.fileName = "";
this.fileList = []
} else {
this.$message.error(resp.msg)
}
let respData = resp.data
if (shardIndex < shardTotal) {
// 上传下一个分片
param.shardIndex = param.shardIndex + 1;
this.upload(param);
} else {
this.$message.success("上传成功")
}
})
})
fileReader.readAsDataURL(fileShard);
},
getFileShard(shardIndex, shardSize) {
let file = this.file;
// 当前分片起始位置
let start = (shardIndex - 1) * shardSize;
//当前分片结束位置
let end = Math.min(file.size, start + shardSize);
let fileShard = file.slice(start, end);
return fileShard;
},
handleRemove(file, fileList) {
// 删除上传文件
this.fileName = "";
this.fileList = []
},
handleChange(file, fileList) {
// 文件状态钩子,选择文件时触发
this.fileList = fileList;
this.fileName = file.name;
this.file = this.fileList[0].raw;
},
handleExceed(files, fileList) {
this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
}
}
}
</script>
<style scoped>
</style>
2. md5加密工具类
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
export function hex_md5(s) {
return binl2hex(core_md5(str2binl(s), s.length * chrsz));
}
function str2binl(str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for (var i = 0; i < str.length * chrsz; i += chrsz)
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (i % 32);
return bin;
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length
*/
function core_md5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for (var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* Convert an array of little-endian words to a hex string.
*/
function binl2hex(binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for (var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) +
hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF);
}
return str;
}
3. 配置文件
我这里配置了一些基础配置:druid、log4j2、mybatis等。
集成log4j2可以看这里: https://blog.csdn.net/weixin_42201180/article/details/111028263
要是不想配置log4j2可以注释掉:
server:
port: 8090
servlet:
context-path: /vhr
address:
spring:
profiles:
active: dev
application:
name: vhr
servlet:
multipart:
maxFileSize: 100MB
maxRequestSize: 100MB
# 数据源配置
datasource:
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/vhr?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&serverTimezone=UTC
data-username: root
data-password: root
druid:
# 初始化时建立物理连接的个数,
initial-size: 5
# 最小连接池数量
min-idle: 5
# 最大连接池数量
max-active: 20
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位毫秒
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-while-idle: true
# 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-borrow: false
# 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
test-on-return: false
# 是否缓存preparedStatement,也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。mysql5.5+建议开启
pool-prepared-statements: true
# 当值大于0时poolPreparedStatements会自动修改为true
max-pool-prepared-statement-per-connection-size: 20
# 通过别名的方式配置扩展插件: stat:监控统计,wall:防sql注入,log4j:日志
filters: stat,wall,slf4j
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
mybatis:
# 注意:一定要对应mapper映射xml文件的所在路径
mapper-locations: classpath:/mapper/*Mapper.xml
# 注意:对应实体类的路径
type-aliases-package: com.javaboy.vhr.entity
configuration:
map-underscore-to-camel-case: true
# 日志配置
logging:
level:
com.javaboy.vhr.mapper: DEBUG
config: classpath:log4j2.yml # 指定log4j配置文件的位置
localUploadFilePath: D:/vhr/localUploadFilePath/
4. 实体类
package com.javaboy.vhr.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
/**
* @author: gaoyang
* @date: 2021-03-23 10:46:31
* @description: 文件(FileInfo)实体类
*/
@Getter
@Setter
@ApiModel("文件实体类")
public class FileInfo implements Serializable {
private static final long serialVersionUID = 694649584012557460L;
@ApiModelProperty("id")
private String id;
@ApiModelProperty("相对路径")
private String filePath;
@ApiModelProperty("文件名")
private String fileName;
@ApiModelProperty("后缀")
private String suffix;
@ApiModelProperty("大小|字节B")
private Integer fileSize;
@ApiModelProperty("用途|枚举[FileUseEnum]:COURSE('C', '讲师'), TEACHER('T', '课程')")
private String fileUse;
@ApiModelProperty("创建时间")
private Date createdAt;
@ApiModelProperty("修改时间")
private Date updatedAt;
@ApiModelProperty("已上传分片")
private Integer shardIndex;
@ApiModelProperty("分片大小|B")
private Integer shardSize;
@ApiModelProperty("分片总数")
private Integer shardTotal;
@ApiModelProperty("文件标识")
private String fileKey;
@ApiModelProperty("base64")
private String shard;
@ApiModelProperty("vod|阿里云vod")
private String vod;
}
5. 后端接口-controller
package com.javaboy.vhr.controller;
import com.github.pagehelper.PageInfo;
import com.javaboy.vhr.entity.FileInfo;
import com.javaboy.vhr.enums.FileUseEnum;
import com.javaboy.vhr.service.FileInfoService;
import com.javaboy.vhr.utils.Base64ToMultipartFile;
import com.javaboy.vhr.utils.result.ResultDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.*;
/**
* @author: gaoyang
* @date: 2021-03-23 10:46:33
* @description: 文件(FileInfo)表控制层
*/
@Slf4j
@Api(tags = "文件API")
@RestController
@RequestMapping("/file")
public class FileInfoController {
@Value("${localUploadFilePath}")
private String FILE_PATH;
@Resource
private FileInfoService fileInfoService;
@ApiOperation(value = "文件上传")
@PostMapping("/upload")
public ResultDTO<FileInfo> upload(@RequestBody FileInfo fileInfo) throws InterruptedException {
String use = fileInfo.getFileUse();
String key = fileInfo.getFileKey();
String suffix = fileInfo.getSuffix();
String shardBase64 = fileInfo.getShard();
MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);
// 保存文件到本地
FileUseEnum useEnum = FileUseEnum.getByCode(use);
// 如果目录不存在则创建
String dir = useEnum.name().toLowerCase();
File fullDir = new File(FILE_PATH + dir);
if (!fullDir.exists()) {
fullDir.mkdirs();
}
// course\6sfSqfOwzmik4A4icMYuUe.mp4
String path = new StringBuffer(dir)
.append(File.separator)
.append(key)
.append(".")
.append(suffix)
.toString();
// course\6sfSqfOwzmik4A4icMYuUe.mp4.1
String localPath = new StringBuffer(path)
.append(".")
.append(fileInfo.getShardIndex())
.toString();
String fullPath = FILE_PATH + localPath;
File dest = new File(fullPath);
try {
// 保存文件
shard.transferTo(dest);
} catch (IOException e) {
log.error(e.getMessage());
return ResultDTO.error("上传失败-" + e.getMessage(), null);
}
// 保存文件记录
fileInfo.setFilePath(path);
FileInfo model = this.fileInfoService.queryByKey(fileInfo.getFileKey());
if (model == null) {
this.fileInfoService.insert(fileInfo);
} else {
model.setShardIndex(fileInfo.getShardIndex());
this.fileInfoService.update(model);
}
if (fileInfo.getShardIndex().equals(fileInfo.getShardTotal())) {
this.merge(fileInfo);
}
return ResultDTO.success("上传成功", fileInfo);
}
/**
* 文件合并
*/
public void merge(FileInfo fileInfo) throws InterruptedException {
log.info("合并分片开始");
// course\6sfSqfOwzmik4A4icMYuUe.mp4
String path = fileInfo.getFilePath();
Integer shardTotal = fileInfo.getShardTotal();
File newFile = new File(FILE_PATH, path);
// 文件追加写入
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(newFile, true);
} catch (FileNotFoundException e) {
log.error(e.getMessage());
}
// 分片文件
FileInputStream fileInputStream = null;
byte[] bytes = new byte[10 * 1024 * 1024];
int len;
try {
for (Integer i = 0; i < shardTotal; i++) {
// 读取第 i 个分片
fileInputStream = new FileInputStream(new File(FILE_PATH + path + "." + (i + 1)));
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
}
} catch (IOException e) {
log.error("合并分片异常-" + e.getMessage());
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
outputStream.close();
log.info("IO流关闭");
} catch (IOException e) {
log.error("IO流关闭失败-", e.getMessage());
}
}
log.info("合并分片结束");
// 释放虚拟机对文件的占用
System.gc();
Thread.sleep(100);
log.info("删除分片开始");
for (Integer i = 0; i < shardTotal; i++) {
String filePath = FILE_PATH + path + "." + (i + 1);
File file = new File(filePath);
boolean result = file.delete();
log.info("删除{},{}", filePath, result ? "成功" : "失败");
}
log.info("删除分片结束");
}
@ApiOperation(value = "文件分片检查")
@GetMapping("/check")
public ResultDTO<FileInfo> check(@RequestParam(name = "fileKey") String fileKey) {
FileInfo fileInfo = this.fileInfoService.queryByKey(fileKey);
return ResultDTO.success(fileInfo);
}
}
6. 接口实现类serviceImpl
这里就不贴service代码了,大家自动生成即可。
package com.javaboy.vhr.service.impl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.javaboy.vhr.entity.FileInfo;
import com.javaboy.vhr.mapper.FileInfoMapper;
import com.javaboy.vhr.service.FileInfoService;
import com.javaboy.vhr.utils.UuidUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* @author: gaoyang
* @date: 2021-03-23 10:46:35
* @description: 文件(FileInfo)表服务实现类
*/
@Service("fileInfoService")
public class FileInfoServiceImpl implements FileInfoService {
@Resource
private FileInfoMapper fileInfoMapper;
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
@Override
public FileInfo queryById(String id) {
return this.fileInfoMapper.queryById(id);
}
/**
* 新增数据
*
* @param fileInfo 实例对象
* @return 实例对象
*/
@Override
public FileInfo insert(FileInfo fileInfo) {
fileInfo.setId(UuidUtil.getShortUuid());
fileInfo.setCreatedAt(new Date());
fileInfo.setUpdatedAt(new Date());
this.fileInfoMapper.insert(fileInfo);
return fileInfo;
}
/**
* 修改数据
*
* @param fileInfo 实例对象
* @return 实例对象
*/
@Override
public FileInfo update(FileInfo fileInfo) {
fileInfo.setUpdatedAt(new Date());
this.fileInfoMapper.update(fileInfo);
return this.queryById(fileInfo.getId());
}
/**
* 通过文件标识查询
* @param fileKey
* @return
*/
@Override
public FileInfo queryByKey(String fileKey) {
return this.fileInfoMapper.queryByKey(fileKey);
}
}
mybatis语句这里也不贴了,就是简单的增删改查。
参考文章:http://blog.ncmem.com/wordpress/2023/12/11/springbootvue-js%e5%ae%9e%e7%8e%b0%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%86%e7%89%87%e4%b8%8a%e4%bc%a0%e3%80%81%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e4%b8%8e%e6%9e%81%e9%80%9f%e7%a7%92%e4%bc%a0/
欢迎入群一起讨论