文件上传下载

思路:上传:使用组件上传附件,在选择文件时调用接口A,接口通过File类操作,将文件先存到后台的临时存储路径,然后将这个临时存储路径告诉前端(告诉前端是因为前端会对文件列表进行增加和删除等操作,把这些信息给前端,他会将信息组成一个列表,最后将列表回传给后端,相当于确认哪些文件是需要的),然后前端确认过的文件列表以及其他表单信息,点击保存,调用保存接口时将表单整个信息包括文件列表信息传给后端保存,后端通过文件列表信息找到临时路径,然后用Files.move(Path a,Path b)方法,将临时文件移动到常规路径,最后在保存表单信息时,将文件路径一并保存到数据库表里

下载:通过数据库表查询到文件路径,访问服务器文件存储的常规路径(这里可能会被拦截,因为没有权限,下面会提),最后通过前端组件在前端显示

前端用luniapp的uni-file-picker组件(需要去官网下载安装才能用)

<uni-file-picker
    limit="9" title="最多选择9张图片"
    v-model="imageValue"
    fileMediatype="image"
    mode="grid"
    @select="select"
    @progress="progress"
    @success="success"
    @fail="fail"
    @delete="deleteFile"
    ref="attachedFile"
    :disable-preview="false"
/>

由于没有配置服务空间,实际上能触发的事件只有select和delete事件,所以我在select事件里直接上传,即选完文件自动上传

下面时select方法,即上传方法

注意:这个方法看上去是批量上传,实际上是将文件列表遍历,然后每个文件调用一次上传方法,所以每次调用接口都只会上传一个文件

不是我不想批量上传,而是如果批量上传,前后端的写法会有很大改动,后端甚至无法用MultipartFile[]接收参数,所以为了尽快实现功能,先使用这个笨办法,批传的方法可再去网上找找

上传方法本文里我分两步写,是为了观看方便,实际上应将两个代码块拼在一起,组成一个方法

上阕主要是拼请求头Content-Type,还有sessionid是用作校验的,相当于token,但是这个有一点很不好就是他是拼在请求体form-data,在下面传参的时候会很难受

select(e){
        console.log('选择文件:',e)
        let that = this;
        let data={};
        const sessionid = uni.getStorageSync('sessionid');
        if (sessionid && data) {
          data.JSESSIONID = sessionid;
        }
        if (sessionid && !data) {
          data = {
            JSESSIONID: sessionid
          }
        }
        let header = {
          'Content-Type': 'application/x-www-form-urlencoded'
        };
        

 下阕写上传方法,用的是uniapp的api,uni.uploadFile,具体参数查看uniapp官网

        e.tempFiles.forEach((item,index)=>{
          uni.uploadFile({
            url: that.baseUrl + '/api/hr/oaLeave/upload', //真实的接口地址
            filePath: item.file.path,
            name: 'file',
            method:"POST",
            header:header,
            formData: data,
            success: (data) => {
              let res = JSON.parse(data.data);
              if (res.code === 0) {
                res.data.forEach((item1)=>{
                  item1.uuid = item.uuid
                  that.formList.fileInfoList.push(item1);
                })
              } 
              }
            },
            fail: function (res) {
              console.log(res);
            },
          });
        })

      },

删除方法只是操作前端列表:
//删除文件
      deleteFile(e){
        console.log('删除文件:',e)
        this.formList.fileInfoList=this.formList.fileInfoList.filter(item=>{
          return  item.uuid!==e.tempFile.uuid;
        })
      },

后端接收文件并存入临时文件路径

Global.getUploadPath()这个方法是自己写的工具类,就是获取aplication.yml文件中的skyweek(项目名).profile的值,我的这个值是: D:/SIP/Release/upload

其他就是File类进行IO操作,还有MultipartFile对象自带的transfer方法,用来将文件存储到指定路径

文件名的生成我用了日期+UUID作为文件名,防止重复

HrOaAttacheFileDTO是我封装的一个存储文件信息的类,其中有文件路径,原文件名,UUID生成的文件名(之后在保存接口中这些信息会存储到数据库中,再之后下载文件时会用到这些数据),最后回传给前端一个List<HrOaAttacheFileDTO>,前端会通过这个列表的数据,自己维护一个列表,最后将列表回传给后端,用于确认哪些文件是最终需要的

@PostMapping("/upload")
    @ResponseBody
    public AjaxResult uploadImage (@RequestParam(required = true,name = "file") MultipartFile[] multipartFiles) {
        String filePath = Global.getUploadPath() + "/HrOaAttendance/temp";
        File fileParent = new File(filePath);
        if (!fileParent.exists()){
            fileParent.mkdirs();
        }
        List<HrOaAttacheFileDTO> fileInfoList = Arrays.stream(multipartFiles).map((singleFile)-> {
            try {
                //上传临时文件,返回临时路径
                String fileName = FileUploadUtils.uploadHrOaAttendance(filePath,singleFile);
                return new HrOaAttacheFileDTO(fileName,singleFile.getOriginalFilename(),fileName.substring(fileName.lastIndexOf("/")+1));
            } catch (IOException e) {
                throw new RuntimeException(e);
            } catch (InvalidExtensionException e) {
                throw new RuntimeException(e);
            }
        }).collect(Collectors.toList());

        return AjaxResult.success(fileInfoList);
    }


/**
     *
     * @param baseDir   相对应用的基目录
     * @param file      上传的文件
     * @return 返回上传成功的文件名
     * @throws FileSizeLimitExceededException       如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException                          比如读写文件出错时
     * @throws InvalidExtensionException            文件校验异常
     */
    public static final String
    uploadHrOaAttendance(String baseDir, MultipartFile file)
            throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException,
            InvalidExtensionException {
        int fileNamelength = file.getOriginalFilename().length();
        if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
            throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }
        //自定义方法,校验文件用的
        assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);

        //生成文件名
        String fileName = extractFilename(file);

        //getAbsoluteFile即用路径字符串和文件名字符串创建一个File对象
        File desc = getAbsoluteFile(baseDir, fileName);
        
        //multipartfile对象存储文件的api,multipartFile.transferTo(file对象),
        //将multipartFile对象存储到该file对象的路径下,
        //如果file本身有文件,则multipartfile文件覆盖file文件
        file.transferTo(desc);
        String pathFileName ="/Release"+ baseDir.substring(baseDir.indexOf(Global.getProfile())+Global.getProfile().length())+"/"+fileName;
        return pathFileName;
    }

这里提一嘴,在前后端如何唯一确定一个文件,而且文件本身没有id,怎么办呢?

前端:select事件是组件内置的,他的回调函数自带参数e,这个e中就有在前端存储的临时文件路径,自动生成的uuid等信息,我这里就用uuid唯一确定一个文件

后端:前端会将文件信息做成一个列表传给后端,文件信息中就有后端的存储路径的字符串,所以可以直接用存储路径的字符串作为参数new一个File对象,或者new一个Path对象,然后用exists方法看他是否存在就行,这样就找到文件了,这个file或者path对象就能唯一确定一个文件

经过以上步骤,我们现在在服务器的 D:/SIP/Release/upload/HrOaAttendance/temp路径下会有一些临时文件,前端也维护了一张列表,存储了确认需要的文件和这个文件的相关信息

这时候,该调用保存接口了,前端将其他表单信息和文件信息封装到请求体里面,然后发给后端,然后我就踩坑了。。。
补充知识点:

前端发请求的常用方式_爱丽丝和她的巫师帽的博客-优快云博客

坑:由于我是application/x-www-form-urlencoded方式发送的请求,不能发送复杂数据,即在前端不能对象套对象,否则解析不出数据

原来我前端的data为

data:{
    name:ni,
    sex:man,
    fileInfoList:[
        {fileUrl:"upload/1awdc1rc13cs1.jpg",fileName:"1awdc1rc13cs1.jpg"},
        {fileUrl:"upload/1awdc1rc13cs2.jpg",fileName:"1awdc1rc13cs2.jpg"},
        {fileUrl:"upload/1awdc1rc13cs3.jpg",fileName:"1awdc1rc13cs3.jpg"},
    ],

}

可以看到fileInfoList是一个列表里包含对象,但是application/x-www-form-urlencoded会将其解析为键值对,其他都没事,但是fileInfoList会变成:

fileInfoList:[object:object,object:object,object:object]

于是后端根本就收不到数据,用于接收数据的List<HrOaAttacheFileDTO>里面全是null。

解决方法:前端将fileInfoList用JSON.stringify(fileInfoList)直接将其作为字符串发送

后端本来想用@Deserializer来拦截并转换为java对象,但是由于application/x-www-form-urlencoded不会反序列化,所以不能触发这个注解

只能用笨办法,在实体类中加一个String参数来接收,然后手动转java对象

List<HrOaAttacheFileDTO> fileDTOList = JSON.parseArray(hrOaLeave.getFileInfoListP(),HrOaAttacheFileDTO.class);
List<HrOaAttacheFileDTO> fileDTOList = JSON.parseArray(hrOaLeave.getFileInfoListP(),HrOaAttacheFileDTO.class);

我们现在有了文件信息,于是通过路径将文件从临时地址移动到存储地址

用路径字符串作为参数new两个Path对象,然后用Files.move(源path,目标path)移动文件,源path代表的源文件会消失,目标path上会出现一个文件

        List<HrOaAttacheFileDTO> fileDTOList = JSON.parseArray(hrOaLeave.getFileInfoListP(),HrOaAttacheFileDTO.class);
        fileDTOList.stream().forEach(item->{
            Path tempPath = Paths.get((item.getFilesUrl().substring(0,item.getFilesUrl().lastIndexOf("/"))).replace("/Release",Global.getProfile()));
            Path realPath = Paths.get((item.getFilesUrl().replace("temp","leave").
                    substring(0,item.getFilesUrl().lastIndexOf("/"))).replace("/Release",Global.getProfile()));
            if (!Files.exists(tempPath)){
                throw new RuntimeException("找不到临时附件,请重新上传");
            }
            try {

                if (!Files.exists(realPath)){
                    Files.createDirectories(realPath);
                }
                Path tempPathAndFileName = Paths.get(tempPath.toString(),item.getStorageFileName());
                Path realPathAndFileName = Paths.get(realPath.toString(),item.getStorageFileName());
                Files.move(tempPathAndFileName,realPathAndFileName);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });

下载文件:

现在只有图片通过url来下载图片的功能,文件下载还没试过

首先是下载路径映射,我们希望能用url来访问图片,而再服务器上的存储地址却是绝对路径:D:/SIP/Release/upload,所以我们需要映射

配置类:

@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
    {
        /** 本地文件上传路径 */
        registry.addResourceHandler("/Release/**").addResourceLocations("file:" + Global.getUploadPath() + "/");

        /** swagger配置 */
        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}

即为,访问http://10.2.3.1:8091/Release/HrOaAttendance/leave/图片.jpg就等于访问

服务器下D:/SIP/Release/upload/HrOaAttendance/leave/图片.jpg这个文件

如果访问不到,那很可能是因为没权限被拦截了,我是shiro做的权限控制

倒数第三行加上一句

//访问上传文件夹的文件
  filterChainDefinitionMap.put("/Release/**","anon");

变成下面的样子

/**
     * Shiro过滤器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
    {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 身份认证失败,则跳转到登录页面的配置
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // 权限认证失败,则跳转到指定页面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        // Shiro连接约束配置,即过滤链的定义
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        //访问上传文件夹的文件
        filterChainDefinitionMap.put("/Release/**","anon");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
}

重新访问http://10.2.3.1:8091/Release/HrOaAttendance/leave/图片.jpg,得到图片

完成

前端显示之前上传的附件

        <uni-file-picker
            v-model="recordData.oaFileInfoList"
            fileMediatype="all"
            mode="list"
            ref="attachedFile"
            :readonly="true"
            @viewFileButtonClick="viewFileButtonClick"
        />        
@viewFileButtonClick="viewFileButtonClick"是自定义的事件,修改uni-file-picker的源码,给每个文件添加一个点击事件,用于下载文件和预览用
v-model="recordData.oaFileInfoList"绑定的参数需含有以下三个参数,用于显示,这是官网说的,实测发现url不写也不影响显示,只是要自己做下载的逻辑
[
	{
		"name":"file.txt",
		"extname":"txt",
		"url":"https://xxxx",
		// ...
	}
]

具体修改uni-file-picker就不展示了,因为很简单,下次再用花个10分钟再看一遍源码就行

这里直接放页面里的方法:viewFileButtonClick,用于在点击文件时进行下载和预览操作

viewFileButtonClick(item){
        console.log(1111111111,item)
        downloadOaFile({
          filePath:item.filesUrl,
          fileName:item.storageFileName,
        }).then(res=>{
          if (res.code==200){
            console.log(res)
          }
        })
      },

但是由于我的系统不能直接存储和读取文件,而是必须经过一道中转,sip需要通过sftp去访问oa服务器,然后在oa上进行存取文件的操作

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值