【Java】图片上传逻辑
后台逻辑:
后台服务,用Dubbo框架作为一个文件微服务
package cn.ymcd.aisw.service;
import cn.ymcd.aisw.dto.RpcResult;
/**
* @Description 文件服务
* @Author jianglun
* @Date 2022/4/7 0007 09:56
* @Version 1.0
*/
public interface IEFileService {
/**
* 文件上传
*
* @param subDir 文件存储子目录,如果文件直接存储到对应业务目录的根目录下面,则直接传null即可
* @param fileName 文件名
* @param fileContent 文件内容
* @Author: jianglun
* @Date: 2022/04/07 10:01
**/
RpcResult uploadFile(String subDir, String fileName, byte[] fileContent);
/**
* @param fileName 文件名,包含子目录
* @Description:删除文件
* @Author: jianglun
* @Date: 2022/04/07 10:03
**/
RpcResult deleteFile(String fileName);
}
Dubbo 文件接口实现:
package cn.ymcd.aisw.service.impl;
import cn.ymcd.aisw.common.config.FileSettingConfig;
import cn.ymcd.aisw.dto.RpcFileDTO;
import cn.ymcd.aisw.dto.RpcResult;
import cn.ymcd.aisw.service.IEFileService;
import cn.ymcd.wss.util.log.YmcdLogger;
import com.alibaba.dubbo.config.annotation.Service;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.*;
/**
* @Description 暴露文件dubbo服务
* @Author jianglun
* @Date 2022/4/7 10:07
* @Version 1.0
*/
@Service(interfaceClass = IEFileService.class, version = "1.0.0",timeout = 5000)
public class RpcFileServiceImpl implements IEFileService {
private static final YmcdLogger LOGGER = new YmcdLogger(RpcFileServiceImpl.class);
@Autowired
private FileSettingConfig config;
@Override
public RpcResult uploadFile(String subDir, String fileName, byte[] fileContent) {
/* try {
String url = FileUtils.uploadFile(multiFile, fileStore + dir);
RpcFileDTO result = new RpcFileDTO();
result.setName(multiFile.getOriginalFilename());
result.setUrl(url);
return result;
} catch (IOException e) {
e.printStackTrace();
}
return null;*/
RpcResult result = RpcResult.getInstances();
// 1、参数验证
if (StringUtils.isBlank(fileName)) {
return result.error(RpcResult.NULL_ERR, "文件名为空!");
}
if (ArrayUtils.isEmpty(fileContent)) {
return result.error(RpcResult.NULL_ERR, "文件内容为空!");
}
// 3、拼接文件存储目录,/appserver/[bustype]/[subDir]/xxxxx.jpg
StringBuilder filePath = new StringBuilder();
filePath.append(config.getBasePath().getDir_store_url()).append(File.separator);
if (StringUtils.isNotBlank(subDir)) {
filePath.append(subDir).append(File.separator);
}
// 4、创建文件夹
try {
org.apache.commons.io.FileUtils.forceMkdir(new File(filePath.toString()));
} catch (IOException e) {
LOGGER.error("aisw-files-server 创建文件夹失败!", e);
return result.error(RpcResult.CREATE_DIR_ERR, "创建文件夹失败");
}
// 5、保存文件
FileOutputStream fos = null;
InputStream in = null;
long ret = 0;
try {
filePath.append(fileName);
File saveFile = new File(filePath.toString());
fos = new FileOutputStream(saveFile);
in = new ByteArrayInputStream(fileContent);
ret = IOUtils.copyLarge(in, fos);
} catch (IOException e) {
LOGGER.error("aisw-files-server 存储文件失败!", e);
return result.error(RpcResult.STORAGE_FILE_ERR, "存储文件失败");
} finally {
IOUtils.closeQuietly(in);
IOUtils.closeQuietly(fos);
}
String storeUrl = filePath.toString().substring(config.getBasePath().getDir_store_url().length() + 1);
String url = config.getBasePath().getDir_agent_url() + File.separator + storeUrl;
RpcFileDTO rpcFileDTO = RpcFileDTO.newBuilder().agentUrl(url).storeUrl(storeUrl).build();
return (ret > 0) ? result.success().data(rpcFileDTO) : result.error(RpcResult.STORAGE_FILE_ERR, "拷贝存储文件失败");
}
@Override
public RpcResult deleteFile(String fileName) {
RpcResult result = RpcResult.getInstances();
// 1、参数验证
if (org.apache.commons.lang3.StringUtils.isBlank(fileName)) {
return result.error(RpcResult.NULL_ERR, "文件名为空!");
}
// 2、拼接文件存储目录,/appserver/[bustype]/subDir/xxxxx.jpg
StringBuilder filePath = new StringBuilder();
filePath.append(config.getBasePath().getDir_store_url()).append(File.separator).toString();
filePath.append(fileName);
// 3、删除文件
File file = new File(filePath.toString());
boolean deleteRet = file.delete();
return deleteRet ? result.success() : result.error(RpcResult.DELETE_FILE_ERR, "删除文件失败");
}
}
文件服务是作为服务提供者,没有消费的需要,所以只有发布的Dubbo接口
我的是后台管理服务,通过Dubbo接口标明一致,就可以调用文件服务了
这里是专门套接文件Dubbo接口写的文件上传删除接口
package cn.ymcd.aisw.common.controller;
import cn.ymcd.aisw.common.CommonExtendUtils;
import cn.ymcd.aisw.common.RpcResult;
import cn.ymcd.aisw.service.IEFileService;
import cn.ymcd.comm.base.message.ResultMessage;
import cn.ymcd.comm.base.util.Assert;
import cn.ymcd.comm.security.annotation.IgnoreLoginCheck;
import cn.ymcd.wss.util.CommonUtil;
import cn.ymcd.wss.util.StringUtils;
import cn.ymcd.wss.util.log.YmcdLogger;
import cn.ymcd.wss.util.security.Base64Util;
import com.alibaba.dubbo.config.annotation.Reference;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* @projectName: aisw-root
* @author: DaiZhiZhou
* @date: 2022年04月27日 10:04
* @version: 1.0
*/
@RestController
@RequestMapping("${sys.path}/file")
public class FileController {
private static final YmcdLogger LOGGER = new YmcdLogger(FileController.class);
@Reference(interfaceClass = IEFileService.class, version = "1.0.0", check = false)
private IEFileService fileService;
/**
* 文件上传接口,上传的同时支持附带源文件地址可以删除源文件
* http://localhost:8083/sys/file/uploadFile
* @param file
* @param subDir
* @param originPath
* @return cn.ymcd.aisw.common.RpcResult
* @author DaiZhiZhou
* @createTime 2022/6/11 19:59
*/
@PostMapping(value = "/uploadFile")
@ApiOperation(value = "上传附件", notes = "上传附件")
@IgnoreLoginCheck
public RpcResult uploadFile(
@RequestParam(value = "file", required = true)
MultipartFile file,
@RequestParam(value = "subDir", required = true)
String subDir,
@RequestParam(value = "originPath", required = false)
String originPath
) {
Assert.isNull(file, ResultMessage.NULL_ERROR, "文件内容");
Assert.isEmpty(subDir, ResultMessage.NULL_ERROR, "文件存储子目录");
try {
String fileData = CommonUtil.encodeBase64File(file);
String storeFilename = CommonExtendUtils.packageRpcFileName(file);
LOGGER.info(fileService.toString());
// 不关心你是否删除成功
if (!StringUtils.isEmpty(originPath)) {
RpcResult rpcResult = fileService.deleteFile(originPath);
LOGGER.info("删除结果: {}", rpcResult.toString());
}
return fileService.uploadFile(subDir, storeFilename, Base64Util.decode(fileData));
} catch (Exception ex) {
LOGGER.error("aisw-wx-applet 上传附件出错.错误信息:{}", ex.getMessage());
}
return null;
}
/**
* http://localhost:8083/sys/file/deleteFile
* 删除附件
* @param param
* @return cn.ymcd.aisw.common.RpcResult
* @author DaiZhiZhou
* @createTime 2022/4/12 09:23
*
*/
@PostMapping(value = "/deleteFile")
@ApiOperation(value = "删除附件", notes = "删除附件")
@IgnoreLoginCheck
public RpcResult deleteFile(
@RequestBody
@ApiParam(name = "附件路径", value = "json格式", required = true)
Map<String, String> param
) {
String path = param.get("path");
Assert.isEmpty(path, ResultMessage.NULL_ERROR, "文件路径");
RpcResult rpcResult = fileService.deleteFile(path);
return rpcResult;
}
}
上传成功之后返回前缀路径 + 随机生成的文件名,将这个文件存储信息和其他记录一并放到MYSQL存储
如要删除文件,则把这个存储信息投送给删除接口即可
项目接口框架做了一个Xss过滤处理,文件接口是需要被Xss进行忽略的
另外后台服务还需要配置文件大小控制
xss.exclude.urls=/sys/file/uploadFile spring.servlet.multipart.maxFileSize=10MB spring.servlet.multipart.maxRequestSize=10MB
前端逻辑
重点是前端怎么控制图片资源不会造成文件余留
这是Vue表单项,一个酒店图标上传
<el-form-item label="酒店图标" prop="merchantIconUrl" :label-width="formLabelWidth">
<el-upload
class="avatar-uploader"
:action="uploadApi"
name="file"
:drag="true"
:data="apiParam"
:show-file-list="false"
:on-success="handleMerchantIconSuccess"
:before-upload="beforeMerchantIconUrl"
>
<img v-if="renderUrl" :src="renderUrl" class="avatar" :onerror="errImg">
<i v-else class="el-icon-plus avatar-uploader-icon" />
</el-upload>
</el-form-item>
初始的接口地址和参数:
uploadApi: `${window._config.default_service}sys/file/uploadFile`,
apiParam: { // 门店二维码路径参数
subDir: 'merchantPic'
},
源文件信息,和新的文件信息被区分开来
1、新文件使用一个暂存变量,每次上传的时候,都会存放在这个变量中
2、如果用户第一次上传觉得图片不行,会进行重复上传,那就需要把上一次上传的图片从磁盘中删除
3、如果用户撤销了操作,退出窗口,那么上传过的图片也需要删除掉
我的逻辑是这样写的:
handleMerchantIconSuccess(res, file) {
// 渲染图片
this.renderUrl = URL.createObjectURL(file.raw)
// 再次上传时删除上一张图片,所以这里需要存储这个图片的存储path
this.apiParam.originPath = res.data.data.storeUrl
// 上传成功之后返回的信息覆盖存储到暂存变量中
this.merchantIconDataCache.agentUrl = res.data.data.agentUrl
this.merchantIconDataCache.storeUrl = res.data.data.storeUrl
},
每一次上传就会携带上一次上传的存储path,这样就会删掉上次上传的图片
原来接口是不这么写的,上传就是上传,删除就是删除
那逻辑也是一样的,再写一个删除的调用,只不过是放在前端这里来写
handleMerchantIconSuccess(res, file) {
// 渲染图片
this.renderUrl = URL.createObjectURL(file.raw)
// 再次上传时删除上一张图片
this.deleteImageAction(this.merchantIconDataCache.storeUrl)
// 存储文件服务响应的地址,先存到缓存对象中
this.merchantIconDataCache.agentUrl = res.data.data.agentUrl
this.merchantIconDataCache.storeUrl = res.data.data.storeUrl
},
再最后表单提交时,去检查暂存变量是否存在新文件信息
如果存在,则表明需要把原记录的图片删除,新文件的图片需要更新上去
否则这个图片path不更新
/* 提交信息 */
submitForm(refTag) {
/* 如果暂存区存在,表示上传了新的图片,如果没有则删除这个属性,不进行更新 */
if (this.merchantIconDataCache.storeUrl) {
// 覆盖新图之前还要删除原来的图片
this.deleteImageAction(this.form.merchantIconUrl)
this.form.merchantIconUrl = this.merchantIconDataCache.storeUrl
}
// MAC地址正则 https://blog.csdn.net/superbeyone/article/details/83748369
const regexForMac = /^([0-9a-fA-F]{2})(([/\s:][0-9a-fA-F]{2}){5})$/
// 对MAC地址参数处理 https://blog.csdn.net/weixin_40195422/article/details/109520572
if (this.form.mac && this.form.mac.length === 6 * 2) {
const convertMac = []
for (let i = 0; i < this.form.mac.length; i += 2) {
convertMac.push(this.form.mac.slice(i, i + 2))
}
console.log(this.form.mac, convertMac)
this.form.mac = convertMac.join('-')
} else if (this.form.mac && this.form.mac.match(regexForMac)) {
this.form.mac = this.form.mac.replaceAll(':', '-')
}
updatePlatformInfo(this.form).then(res => {
if (res.data === true) {
this.$message({
message: '更新成功',
type: 'success'
})
this.clearLegacy()
this.$emit('close', null)
}
})
},
clearLegacy() {
// 重置暂存地址,防止回调删除图片
this.merchantIconDataCache.storeUrl = ''
this.merchantIconDataCache.agentUrl = ''
}
用户撤销操作的时候检查暂存变量,如果存在path信息,则调用删除
// 撤销事件绑定的方法
cancel() {
// 调用父组件的close事件
this.$emit('close', this.merchantIconDataCache.storeUrl)
},
// 这里写的有点绕,是让父组件调用这个方法删除
deleteImageAction(filePath) {
if (filePath) {
deleteFile({ path: filePath }).then(res => {
console.log(`then -> ${JSON.stringify(res)}`)
})
}
},
// 父组件的close方法
merchantDialogClose(val) {
console.log(val)
this.merchantDialogVisible = false
this.$refs.merchantSetting.deleteImageAction(val)
},


浙公网安备 33010602011771号