通用文件上传设计

版权声明:本文为博主原创文章,未经博主允许不得转载。 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配置代理文件资源。

猜你喜欢

转载自blog.csdn.net/minaki_/article/details/85163343