版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/minaki_/article/details/85163343
1.实现描述
前端采用XMLHttpRequest进行文件上传。后端(springboot2.x
)接口支持多文件同时上传,接口可根据数据库动态配置验证(文件大小、文件类型)合法性,该接口可满足上传文件大小500mb的需求(大于500mb建议采用其他实现方式)。不同的功能业务都可以复用此接口。
**接口支持请求大小(文件大小)必须满足在框架允许的最大请求范围内(小于max-request-size)
接口地址:http://url[:prot]/uploadFile (http://127.0.0.1:8080/uploadFile)
请求格式:post | multipart/form-data;
参数名 | 必填/类型 | 说明 |
---|---|---|
bizType | 是/String | 不同业务对应不同的类型,根据数据库配置 |
bizId | 否/Long | 业务id,如发送文件的用户ID、发布新闻id等 |
fileData | 是/bit[] | 上传的文件的信息流 |
返回列表 JSON: LIST[FileInfo]
2.数据库设计
文件信息配置表 file_conf
字段 | 类型 | 约束 | 说明 |
---|---|---|---|
id | int(11) | 必填 | 自增主键id |
bize_type | varchar(20) | 必填 | 业务类型,不同业务不同的类型 |
file_type_limit | varchar(200) | 非必填 | 允许上传的文件类型(mine-type标准),为空时不限制类型 |
file_size_limit | varchar(20) | 非必填 | 允许上传的文件大小(kb),为空时不限制大小 |
path | varchar(50) | 必填 | 服务器存储文件的路径 |
description | varchar(100) | 非必填 | 描述,如描述该业务类型对应的文件上传业务功能的业务表 |
resource_realm | varchar (100) | 必填 | 外部访问文件资源相对根路径 |
enabled | tinyint(4) | 必填 | 是否可用(默认1可用,0禁用),用于禁止某个业务上传文件的功能 |
creat_time | datetime | 必填 | 创建时间 |
last_update_time | datetime | 非必填 | 最近修改时间 |
文件信表 file_info
字段 | 类型 | 约束 | 说明 |
---|---|---|---|
id | int(11) | 必填 | 自增主键id |
bize_type | varchar(20) | 必填 | 业务类型 |
bize_id | int(11) | 非必填 | 业务id |
original_name | varchar (255) | 必填 | 文件原名称 |
new_name | varchar(50) | 必填(唯一) | 文件新名称(随机码 |
file_type | varchar(20) | 必填 | 文件类型 |
file_size | varchar(20) | 必填 | 文件大小(kb) |
file_path | varchar(200) | 必填 | 文件服务器存储绝对路径 |
relative_path | varchar(200) | 必填 | 文件相对路径,域名+此字段为该资源的请求地址 |
creat_time | datetime | 必填 | 创建时间 |
last_update_time | datetime | 非必填 | 最近修改时间 |
del_flag | tinyint(1) | 必填 | 逻辑删除(默认0正常,1文件已被物理删除) |
3.主要代码实现
3.1.model
对应数据库表结构建立对应实体类
/**
* 文件配置信息</p>
* table: file_conf
* @author lilee
* @version 1.0.0
* @date 2018/12/20 14:55
*/
public class FileConf {
private Long id; // 主键ID
private String bizType; // 上传服务类型
private String fileTypeLimit; // 文件类型(mine-type标准),为空不限制上传类型
private String fileSizeLimit; //(kb)文件限制大小,为空不限制上传大小(但要满足框架支持的上传文件大小)
private String path; // 服务器文件夹路径
private String description; // 描述
private String resourceRealm; // 访问资源路径
private Boolean enabled; // 是否可用(默认1可用,0禁用)
private Date createTime; // 创建时间
private Date lastUpdateTime; // 最后修改时间
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public String getFileTypeLimit() {
return fileTypeLimit;
}
public void setFileTypeLimit(String fileTypeLimit) {
this.fileTypeLimit = fileTypeLimit;
}
public String getFileSizeLimit() {
return fileSizeLimit;
}
public void setFileSizeLimit(String fileSizeLimit) {
this.fileSizeLimit = fileSizeLimit;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getResourceRealm() {
return resourceRealm;
}
public void setResourceRealm(String resourceRealm) {
this.resourceRealm = resourceRealm;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getLastUpdateTime() {
return lastUpdateTime;
}
public void setLastUpdateTime(Date lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
}
/**
* 文件信息</p>
* table: file_info
* @author lilee
* @version 1.0.0
* @date 2018/12/20 14:55
*/
public class FileInfo {
private Long id; // 主键ID
private String originalName; // 文件原名称
private String newName; // 文件新名称
private String fileType; // 文件类型(image/jpg, image/png, video/mp4, xsl,doc等)
private String fileSize; // 文件大小(kb)
private String filePath; // 文件服务器存储路径
private String relativePath; // 文件相对路径
private Long bizId; // 业务ID
private String bizType; // 上传服务类型(业务类型)
private Date createTime; // 创建时间
private Date lastUpdateTime; // 最后修改时间
private Boolean delFlag; // 数据删除标记0=正常,1=文件已物理删除
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getOriginalName() {
return originalName;
}
public void setOriginalName(String originalName) {
this.originalName = originalName;
}
public String getNewName() {
return newName;
}
public void setNewName(String newName) {
this.newName = newName;
}
public String getFileType() {
return fileType;
}
public void setFileType(String fileType) {
this.fileType = fileType;
}
public String getFileSize() {
return fileSize;
}
public void setFileSize(String fileSize) {
this.fileSize = fileSize;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getRelativePath() {
return relativePath;
}
public void setRelativePath(String relativePath) {
this.relativePath = relativePath;
}
public Long getBizId() {
return bizId;
}
public void setBizId(Long bizId) {
this.bizId = bizId;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getLastUpdateTime() {
return lastUpdateTime;
}
public void setLastUpdateTime(Date lastUpdateTime) {
this.lastUpdateTime = lastUpdateTime;
}
public Boolean getDelFlag() {
return delFlag;
}
public void setDelFlag(Boolean delFlag) {
this.delFlag = delFlag;
}
}
3.2.dao
dao层,本实例为了简单,并未提供数据库操作相关代码,该模块需要用户根据自己项目架构自己实现
当前文件存储到/home/data下
@Repository
public class FileConfDao {
/* todo 为了简单,未正真的对数据库操作
select *
from file_conf
where biz_type = #{bizType,jdbcType=VARCHAR} and enabled = 1
*/
public FileConf selectByBizType(String bizType) {
FileConf fileConf = new FileConf();
fileConf.setBizType(bizType);
fileConf.setPath("/home/data");
fileConf.setResourceRealm("/res");
fileConf.setEnabled(true);
return fileConf;
}
}
3.3.service
该模块实现文件上传主要逻辑,部分数据验证、错误提示、返回数据的封装需要根据自己的需求做调整。
****相关数据库操作需要补全****
/**
* TODO 文件上传service
*
* @author lilee
* @version 1.0.0
* @date 2018/12/20 14:55
*/
@Service
public class FileUploadService {
@Resource
private FileConfDao fileConfDao;
// @Resource
// private FileInfoDao fileInfoDao;
protected static Logger log = LoggerFactory.getLogger(FileUploadService.class);
/**
* 文件上传
* @param mpfList 文件信息集
* @param bizType 业务类型(必传)
* @param bizId 业务id
* @param extraPath 额外的路径,首部和结尾不能带斜杠'/'
* @return
*/
public List<FileInfo> uploadFile(List<MultipartFile> mpfList, String bizType, Long bizId, String extraPath) {
// 验证数据begin
// 获取对应业务文件配置信息
FileConf fileConf = this.fileConfDao.selectByBizType(bizType);
if(fileConf == null){
log.info("file conf is null"); // 打印文件配置信息
return null;
}
// 验证文件信息是否符合配置信息
if (!validateFileInfo(mpfList, fileConf)) {
// 验证失败
log.info("fileInfo is error"); // 打印文件配置信息
return null;
}
// 信息验证end
List<FileInfo> files = new ArrayList<>();
FileInfo fileInfo = null;
String path = fileConf.getPath(); // 文件存储的目录
// 获取相对路径,由file_conf、额外路径
String relativePath = fileConf.getResourceRealm() + "/"
+ (StringUtils.isEmpty(extraPath) ? "" : extraPath + "/");
// 验证服务器存储路径是否存在,若不存在,则新建文件夹
File serFile = new File(path + relativePath);
if (!serFile.exists()) {
serFile.mkdirs();
}
// 循环上传文件
for (MultipartFile mpf : mpfList) {
String originalFileName = mpf.getOriginalFilename(); // 获取源文件名
// 生成新文件名
String newFileName = "F" + UUID.randomUUID().toString().replace("-", "").toUpperCase()
+ originalFileName.substring(originalFileName.lastIndexOf("."));
// 组装数据
fileInfo = new FileInfo();
fileInfo.setOriginalName(originalFileName);
fileInfo.setFileSize(String.valueOf(mpf.getSize() / 1024)); // 单位(kb)
fileInfo.setFileType(mpf.getContentType()); // 文件类型
fileInfo.setNewName(newFileName); // 文件新名字
fileInfo.setRelativePath(relativePath + newFileName); // 文件相对路径
fileInfo.setFilePath(path + relativePath + newFileName); // 文件物理路径
fileInfo.setBizType(bizType);
fileInfo.setBizId(bizId);
fileInfo.setDelFlag(false);
// 存储文件并记录到数据库
try {
FileCopyUtils.copy(mpf.getBytes(), new FileOutputStream(fileInfo.getFilePath()));
// fileInfoDao.insert(fileInfo); // todo 本实例未进行任何数据库操作,需要根据自己项目环境添加这部分
} catch (IOException e) {
log.error("upload file error!", e);
return null;
}
files.add(fileInfo);
}
return files;
}
private boolean validateFileInfo(List<MultipartFile> mpfList, FileConf fileConf) {
if (mpfList == null || fileConf == null) { return false; }
for (MultipartFile mpf : mpfList) {
// 验证文件大小是否超出配置大小
if (!StringUtils.isEmpty(fileConf.getFileSizeLimit()) && mpf.getSize() / 1024 > Integer.parseInt(fileConf.getFileSizeLimit())) {
return false;
}
// 验证文件类型是否符合文件配置的要求
if (!StringUtils.isEmpty(fileConf.getFileTypeLimit()) && fileConf.getFileTypeLimit().indexOf(mpf.getContentType()) < 0) {
return false;
}
}
return true;
}
}
3.4.controller
****简单controller****
@RestController
public class FileUploadController {
@Resource
private FileUploadService fileUploadService;
/**
* 文件上传接口
* @param request
* @param bizType 业务类型(必传)
* @param bizId 业务id
* @param extraPath 额外的路径,首部和结尾不能带斜杠'/'
* @return
*/
@RequestMapping(value ="/uploadFile", method = RequestMethod.POST)
public List<FileInfo> uploadFile(MultipartHttpServletRequest request, String bizType, Long bizId, String extraPath) {
int count = 0;
List<FileInfo> result = this.fileUploadService.uploadFile(request.getMultiFileMap().get("fileData"), bizType, bizId, extraPath);
return result;
}
}
3.5.前端测试代码
引用博客
https://www.cnblogs.com/tianyuchen/p/5594641.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>fileUpload Test</title>
<script type="text/javascript">
var xhr;
var ot;//
var oloaded;
//上传文件方法
function upladFile() {
var url = "http://127.0.0.1:8080/uploadFile"; // 接收上传文件的后台地址
var form = new FormData(); // FormData 对象
var fileObj = document.getElementById("file").files; // js 获取文件对象
if (fileObj != null) {
for (var i=0; i< fileObj.length; i++) {
form.append('fileData', fileObj[i], fileObj[i].name);
}
}
xhr = new XMLHttpRequest(); // XMLHttpRequest 对象
xhr.open("post", url, true); //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
xhr.onload = uploadComplete; //请求完成(成功)
xhr.onerror = uploadFailed; //请求失败
xhr.upload.onprogress = progressFunction; //【上传进度调用方法实现】
xhr.upload.onloadstart = function(){ //上传开始执行方法(初始)
ot = new Date().getTime(); //设置上传开始时间
oloaded = 0; //设置上传开始时,以上传的文件大小为0
};
xhr.send(form);
}
//上传进度实现方法,上传过程中会频繁调用该方法
function progressFunction(evt) {
var progressBar = document.getElementById("progressBar");
var percentageDiv = document.getElementById("percentage");
// event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
if (evt.lengthComputable) {
progressBar.max = evt.total;
progressBar.value = evt.loaded;
percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
}
var time = document.getElementById("time");
var nt = new Date().getTime();//获取当前时间
var pertime = (nt-ot) / 1000; //计算出上次调用该方法时到现在的时间差,单位为s
ot = new Date().getTime(); //重新赋值时间,用于下次计算
var perload = evt.loaded - oloaded; //计算该分段上传的文件大小,单位b
oloaded = evt.loaded;//重新赋值已上传文件大小,用以下次计算
//上传速度计算
var speed = perload / pertime; //单位b/s
var bspeed = speed;
var units = 'b/s';//单位名称
if(speed/1024 > 1){
speed /= 1024;
units = 'k/s';
}
if(speed/1024 > 1){
speed /= 1024;
units = 'M/s';
}
speed = speed.toFixed(1);
//剩余时间
var resttime = ((evt.total - evt.loaded) / bspeed).toFixed(1);
time.innerHTML = ',速度:' + speed + units + ',剩余时间:' + resttime + 's';
if(bspeed == 0)
time.innerHTML = '上传已取消';
}
//上传成功响应
function uploadComplete(evt) {
alert("上传成功!");
}
//上传失败
function uploadFailed(evt) {
alert("上传失败!");
}
//取消上传
function cancleUploadFile(){
xhr.abort();
}
</script>
</head>
<body>
<progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
<span id="percentage"></span><span id="time"></span>
<br /><br />
<input type="file" id="file" name="myfile" multiple="multiple"/>
<input type="button" onclick="upladFile()" value="上传(多选)" />
<input type="button" onclick="cancleUploadFile()" value="取消" />
</body>
</html>
测试会遇到跨域的问题,需要自己根据使用的框架解决,如果你使用了springboot,可在Application主函数添加如下代码:
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("PUT", "DELETE","GET","POST")
.allowedHeaders("*")
.exposedHeaders("access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin",
"access-control-max-age",
"X-Frame-Options")
.allowCredentials(false).maxAge(3600);
}
};
}
4.提示
上传文件的接口返回的url需要另外提供接口支持,可使用nginx配置代理文件资源。