文件上传接口设计

1.问题描述

18年初跳槽来到新公司,在开发中惊奇的发现公司项目中的文件上传功能竟然存在多个接口,他们为每个需要文件上传的业务实现了接口,仔细看了下这些上传文件代码,他们主要区别在于每个业务存储的文件类型、文件大小、是否要压缩需要限制或者是文件路径需要指定不同。这几个特性完全可以配置化定义规则(上家公司就是这么做的,配置化实现公用文件上传接口是一个正确的选择)。究其原因可能大家害怕更改之前的接口会照成bug,索性自己重写一个上传接口(反正是复制粘贴过来改下内容吧)。
下面我们进入主题,如何实现一个根据配置化定义规则的文件上传设计

2.接口定义

后端服务我们直接使用springboot2.X框架,文件上传相关配置信息将入数据库,这样可支持动态修改。测试用例我提供一个html页面,或者你可以直接使用postman。更详细的内容处理请看代码(实现很简单的)
API信息:
接口地址:http://127.0.0.1:8080/uploadFile
请求格式:post | multipart/form-data;

参数名 必填/类型 说明
bizType 是/String 不同业务对应不同的类型,根据此类型查找文件上传的配置
bizId 否/Long 业务id,可以通过该id找到对于的业务信息
isShrink 否(默认false)/boolean 是否需要压缩代码为了简介,暂未实现支持是否压缩
file 文件信息,支持多文件上传

返回列表 JSON: [FileInfo]

3.数据库设计

文件信息配置表 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文件已被物理删除)

4.主要代码实现

4.1.实体类信息

对应数据库表结构建立对应实体类`

/**
 * 文件配置信息

* 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; // 最后修改时间 // setter & getter }
/**
 * 文件信息

* 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=文件已物理删除 // setter & getter

4.2.数据访问dao

dao层,本实例为了简单,并未提供数据库操作相关代码,该模块需要用户根据自己项目架构自己实现。
当前文件存储到/home/data下

@Repository
public class FileConfDao {
    // 根据业务类型bizType获取该业务的配置信息
    public FileConf selectByBizType(String bizType) {
        // todo 为了简单,未正真的对数据库操作 
        // FileConf fileConf = dbdao.findByBizType(bizType);
        FileConf fileConf = new FileConf();
        fileConf.setBizType(bizType);
        fileConf.setPath("/home/data");
        fileConf.setResourceRealm("/res");
        fileConf.setEnabled(true);
        return fileConf;
    }
    // 存储文件的信息
     public FileInfo insert(FileInfo fileInfo) {
       //  dbdao.insert(fileInfo)
        return fileInfo;
    }
}

4.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()));
                fileConfDao.insert(fileInfo); 
            } 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;
    }
}

4.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;
    }
}

4.5.html测试代码

使用postman测试更简单,记得考虑跨越问题
引用博客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>

跨域问题解决

    @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.最后

综上一个根据配置规则定义的公用上传文件设计便完成了,这样我们项目所有和上传文件相关的业务都可以使用该接口,只需要简单的在数据库中配置下和业务相关的信息便可。当然可能有些业务还需要更细的定制,如要指定存储服务器,是否需要压缩,一定时间内需要删除文件等等,那你只需在该设计上添加新的配置字段,然后实现对该配置字段的支持就行了。



、﹗∕
— 〇 -
╱︱ ヽ
但行好事、莫问前程!
>.freerme、我是lilee[https://blog.csdn.net/freerme]
_________________ *_*______
____ ____ ____

你可能感兴趣的:(spring,文件上传,通用文件上传)