亲测好用,这里就直接上代码了,代码有详细的解释。
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语句这里也不贴了,就是简单的增删改查。
技术交流+微:JavaBoy_1024