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

  

 

 

 

 

 

 

 

 

 

 

posted @ 2022-06-11 20:41  emdzz  阅读(147)  评论(0)    收藏  举报