思路:上传:使用组件上传附件,在选择文件时调用接口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上进行存取文件的操作