【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) },