[Java project] Vue+ElementUI+Ceph realizes multi-type file upload function and realizes file preview function

Effect demo

insert image description here

Let me talk about our needs first, our needs are file uploads, the previous interface only supports uploading pictures, and later needs to support uploading pdf, so I have to change the interface, and replace the original picture upload interface with the backend ceph , but in fact the general processing flow is similar, they are all uploaded to the backend and then get the url address.

To achieve clicking to preview files, you need to use the groupPreview of element.
insert image description here

front end


The page uploaded by the ElementUI
insert image description here
file uses the index.vue page under ElementUI. There is a small bug, but with my unremitting efforts (about 15h hahaha), I finally solved it for him. I can only say that it feels like this The first step of "proficient in the front end".
Then we found a problem that if the user directly clicks submit before the file is uploaded, the file will be lost, so we added a global lock in the file upload to ensure that the user cannot upload the file at this time. Submit
insert image description here

The page code is as follows

        <!-- :on-preview="groupPreview" -->
<template>
    <div>
      <el-upload
        class="upload-demo"
        :action="uploadAction"
        :on-remove="handleRemove"
        :on-success="handleSuccess"
        :on-error="handleError"
        :before-upload="beforeUpload"
        :on-preview="groupPreview"
        :file-list="fileListTemp"
        :on-progress="testFile"
        >
        <el-button size="small" type="primary">点击上传</el-button>
        <div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件,且不超过20M<br>
            上传完毕之后请审核一下您上传的文件在确认是否提交
        </div>
      </el-upload>
    </div>
</template>
<script setup name="uploadImage">
import {
      
       ref, defineProps, onMounted, defineEmits } from 'vue';
import {
      
       ElMessage,ElLoading  } from 'element-plus';
const props = defineProps(['modelValue'])
const fileList = ref([])
let fileListTemp = ref([])
const urls = ref([])
const uploadAction = ref('/merchant/api/common/upload/image')
const emits = defineEmits(['update:modelValue','input'])

onMounted(() => {
      
      
    setDefaultFileList()
})
const setDefaultFileList = () => {
      
      
    //这里的modelValue就是v-model传递过来的files
    console.log("----props.modelValue----");
    console.log(props.modelValue);
    console.log("------fileListTemp--");
    if (props.modelValue && props.modelValue.length > 0) {
      
      
        fileListTemp.value = []
        props.modelValue.forEach(element => {
      
      
            let index = element.indexOf("|");
            fileListTemp.value.push({
      
      
                name: element.substr(0,index), 
                url: element.substr(index+1),
            })
        });
        console.log(fileListTemp)
        console.log("--------");
    }
}
const handleRemove = (file, fileList) => {
      
      
    fileListTemp.value = []
    emits("update:modelValue", fileListTemp);
}

let loading = null
const testFile = ()=>{
      
      
    loading =  ElLoading.service({
      
      
        lock:true,
        text:'文件上传中...'
    })
}

const handleSuccess = (response, file, fileList) => {
      
      
            let fileTemp = response.data.name+"|"+response.data.url;
            console.log("--------fileTemp---------")
            console.log(fileTemp);
            console.log("-----------------")
            fileListTemp.value.push({
      
      
                fileTemp
            });
    
    // fileListTemp.value.push(response.data.url);
    console.log("---------文件集合fileListTemp.value--------")
    console.log(fileListTemp.value);
    console.log("-----------------")
    emits("update:modelValue", fileListTemp);
    loading.close()
}

const handleError = (error) => {
      
      
    console.log('handleError', error)
    ElMessage.error('文件上传失败');
}
const groupPreview = (file)=>{
      
      
    window.open(file.url);
}
const beforeUpload = (file) => {
      
      
    // console.log(file)
    const isLt2M = file.size / 1024 / 1024 < 20;
    if (!isLt2M) {
      
      
        ElMessage.error('上传文件大小不能超过 20MB!');
    }
    return isLt2M;
}
</script>

Probably the situation is that when we add a file, a request will be sent to the backend, and the path of this backend is

const uploadAction = ref('/merchant/api/common/upload/image')

When you click to upload a picture, our webpage will send this request, which will actually be processed by the router, as follows

var express = require("express");
var router = express.Router();
var request = require("superagent");
var multer = require("multer");

module.exports = (app) => {
    
    
  /**
   * 上传图片
   */
  router.post("/upload/image", async (req, res, next) => {
    
    
    var storage = multer.memoryStorage();
    var upload = multer({
    
     storage }).single("file");
    upload(req, res, async (err) => {
    
    
      try {
    
    
        const url = "http://localhost:8080/supplier/outerapi/api/ceph/upload";
        const response = await request.post(url).attach("file", req.file.buffer, req.file.originalname);
        const result = JSON.parse(response.text);
        // console.log(result)
        res.send({
    
    
          code: 200,
          msg: "图片上传成功",
          data: {
    
    
            name: result.data[0].name, //结构为:name | url
            url: result.data[0].url, // url
          },
        });
      } catch (err) {
    
    
        res.send({
    
    
          code: 500,
          msg: "图片上传异常",
        });
      }
    });
  });
  //使用/merchant/api/common作为路径前缀
  app.use("/merchant/api/common", router);
};


You can see that there is a url here, and this url is the interface for your backend to process front-end file uploads.
And you can see that our return type requirements are code, msg, data, where data requires name and url, which are the file name and file path respectively.

backend java

Let's look at the code of the control layer first. Here our OSS service uses ceph. You can also use minio, etc. instead.

 /**
     * 上传订单附件接口,文件可以一次传多个
     * Content-Type:multipart/form-data;
     * 文件参数名:file
     *
     * @param files 请求的文件
     * @return 返回
     */
    @PostMapping("/upload")
    public BaseResponse<List<FileInfo>> uploadFile(@RequestParam("file")
                                                     List<MultipartFile> files) {
    
    
        BaseResponse<List<FileInfo>> res = new BaseResponse<List<FileInfo>>();
        res.setCode(500);
        res.setMsg("上传失败,请稍后重试");
        ArrayList<FileInfo> listFile = new ArrayList<FileInfo>();
        try {
    
    
            //遍历请求头里的文件
            //for (int i = 0; i < files.size(); i++) {
    
    
            for (MultipartFile file : files) {
    
    
                //获取文件的原始文件名
                String fileName = file.getResource().getFilename();
                //获取文件后缀,如 .jpg
                String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
                //ceph请求参数
                CephRequest cephRequest = new CephRequest();
                cephRequest.originFileName = fileName;
                //文件名设置一个随机数,避免重复
                SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
                String uuid = UUID.randomUUID().toString();
                cephRequest.setFileName(dateFormat.format(new Date())
                        + uuid.substring(uuid.lastIndexOf("-")) + fileSuffix);
                //这里可以设置块名称也可以不设置,不设置用默认的
                cephRequest.setBlockName("");
                cephRequest.setMaxDays(5 * 365); //五年
                /* 设置请求头信息 */
                cephRequest.setContextType(file.getContentType());
                // 从OkHttp里面提交的postfileName 需要根据实际文件后缀修改contentType
                if (cephRequest.getContextType().equals("application/from-data")) {
    
    
                    if (fileName.endsWith(".pdf")) {
    
    
                        cephRequest.setContextType("application/pdf");
                    } else if (fileName.endsWith(".jpeg")) {
    
    
                        cephRequest.setContextType("image/jpeg");
                    } else if (fileName.endsWith(".png")) {
    
    
                        cephRequest.setContextType("image/png");
                    } else if (fileName.endsWith(".bmp")) {
    
    
                        cephRequest.setContextType("image/bmp");
                    } else if (fileName.endsWith(".jpg")) {
    
    
                        cephRequest.setContextType("image/jpg");
                    }
                }
                cephRequest.setFileStream(file.getInputStream());
                LogUtil.info(JSON.toJSONString(cephRequest), "cephRequest");
                //上传文件到ceph
                var cephResponse = cephService.uploadCeph(cephRequest);
                if (cephResponse.isOk()) {
    
    
                    //ceph上传成功后,添加到返回参数里
                    listFile.add(new FileInfo(cephRequest.getOriginFileName(),cephResponse.getFileUrl()));
                } else {
    
    
                    res.setMsg(cephResponse.getErrorMsg());
                    return res;
                }
            }
            //上传成功的标识
            if (listFile.size() > 0) {
    
    
                res.setCode(200);
                res.setMsg("ok");
            }
        } catch (Exception ex) {
    
    
            res.setCode(500);
            res.setMsg("上传失败,请稍后重试!" + ex.getMessage());
        } finally {
    
    
            res.setData(listFile);
        }
        return res;

Entity class

@Data
public class CephRequest {
    
    
    /**
     * 要保存的文件名称
     */
    public String fileName;
    /**
     * 要上传的文件流
     */
    public InputStream fileStream;
    /**
     * 设置文件内容的类型
     */
    public String contextType;
    /**
     * 要保存的天数,不传默认是365*3
     */
    public int maxDays;
    /**
     * 块名称,不传用默认的
     */
    public String blockName;
    /**
     * 原始文件名.
     */
    public String originFileName;
}

Then the service layer of ceph


@Service
public class CephService {
    
    
    /**
     * CEPH服务地址
     */
    public static String SERVICE_URL = "";

    /**
     * 块名称,可以自定义修改
     */
    public static String BLOCK_NAME = "";
    /**
     * ceph key
     */
    public static String ACCESS_KEY = "";
    /**
     * ceph密钥
     */
    public static String SECRET_KEY = "";
    /**
     * CEPH客户端
     */
    private AmazonS3Client s3client = null;
    /**
     * oss存储管理类
     */
    private final ICephManager cephManager;

    public CephService(ICephManager cephManager) {
    
    
        this.cephManager = cephManager;
    }

    public CephResponse uploadCeph(CephRequest param) {
    
    
        CephResponse res = new CephResponse();
        res.setOk(false);
        try {
    
    
            // 一、初始化ceph客户端
            AWSCredentials credentials = new BasicAWSCredentials(ACCESS_KEY, SECRET_KEY);
            ClientConfiguration clientCfg = new ClientConfiguration();
            clientCfg.setProtocol(Protocol.HTTP);
            s3client = new AmazonS3Client(credentials, clientCfg);
            //设置存储的服务器
            s3client.setEndpoint(SERVICE_URL);
            s3client.setS3ClientOptions(new S3ClientOptions().withPathStyleAccess(true));

            //二、上传文件
            InputStream input = param.getFileStream();
            //参数验证
            if ("".equals(param.getFileName())) {
    
    
                res.setErrorMsg("需要文件名参数");
                return res;
            } else if (input == null) {
    
    
                res.setErrorMsg("未获取到文件流");
                return res;
            } else if (param.getMaxDays() <= 0) {
    
    
                res.setErrorMsg("有效期必须大于0");
                return res;
            }
            String bucket = BLOCK_NAME;
            if (!"".equals(param.getBlockName())) {
    
    
                bucket = param.getBlockName();
            }
            // 1、先上传文件
            ObjectMetadata meta = new ObjectMetadata();
            meta.setContentLength(input.available());
            // 这里如果有请求头就设置一下,没有就不设置
            if (!StringUtils.isEmpty(param.getContextType())) {
    
    
                meta.setContentType(param.getContextType());
                System.out.println(param.getContextType());
            }
            //第二个参数可以修改为目录+文件名
            s3client.putObject(bucket, param.getFileName(), input, meta);
            //2、生成文件的外链
            GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, param.getFileName());
            Calendar nowTime = Calendar.getInstance();
            nowTime.add(Calendar.MINUTE, 60 * 24 * param.getMaxDays());
            request.setExpiration(nowTime.getTime());

            URL url = s3client.generatePresignedUrl(request);
            //替换文件路径 换为最后项目需要使用的路径
            //是否需要这个代码看你的业务
            res.setFileUrl(url.toString().replace("http://.com/", "https:///"));
            if ("".equals(res.getFileUrl())) {
    
    
                res.setErrorMsg("ceph上传文件失败");
                return res;
            }

            //3、保存到DB中.
            //TODO 如果保存到DB异常怎么办?
            try {
    
    
                SysCephfile file = new SysCephfile();
                file.setCreateTime(LocalDateTime.now());
                file.setBlockName(BLOCK_NAME);
                file.setExpireTime(LocalDateTime.now().plusDays(param.getMaxDays()));
                file.setOriginFileName(param.getOriginFileName());
                file.setCephKey(param.getFileName());
                // param.getContextType()
                file.setUrl(res.getFileUrl());
                file.setFileId(0L);
                this.cephManager.saveCephFile(file);
            } catch (Exception ex) {
    
    
                //根据SysCephfile的信息去删除ceph中的图片
                //s3client.deleteObject();
                LogUtil.error(ex, "保存oss存储对象异常");
            }

            res.setOk(true);
            res.setErrorMsg("");
        } catch (Exception ex) {
    
    
            res.setErrorMsg("上传ceph异常," + ex.getMessage() + ex.getStackTrace());
        } finally {
    
    
            return res;
        }
    }

}

Entity class

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_cephfile")
public class SysCephfile implements Serializable {
    
    

private static final long serialVersionUID = 1L;

            /**
            * 自增长主键
            */
            @TableId(value = "id", type = IdType.AUTO)
    @FieldName("自增长主键")
    private Long id;

            /**
            * 具体存储的blockname
            */
    @FieldName("具体存储的blockname")
    private String blockName;

            /**
            * 创建时间
            */
    @FieldName("创建时间")
    private LocalDateTime createTime;

            /**
            * 过期时间
            */
    @FieldName("过期时间")
    private LocalDateTime expireTime;

            /**
            * 原始文件名
            */
    @FieldName("原始文件名")
    private String originFileName;

            /**
            * 传ceph的key(对外)
            */
    @FieldName("传ceph的key(对外)")
    private String cephKey;

            /**
            * 上传后的url
            */
    @FieldName("上传后的url")
    private String url;

            /**
            * 具体归属的档案ID,是0就是没保存的.
            */
    @FieldName("具体归属的档案ID,是0就是没保存的.")
    private Long fileId;


}

The last mapper layer uses mybatisplus, without any code, pure CRUD, so it will not be posted

data storage format

Our file data is stored in MongoDB in the format of a string, as follows

"files": ["QQ screenshot 20230508204810.png|https://xxxxx", "QQxxx.jpg|https://xxxxx"]
Our data structure is an array of strings, and we set the content in front of the array | It is set as the file name, and the latter is the file path. The reason why it is specially designed is due to business requirements, and I don't want to design it this way.
insert image description here
insert image description here
insert image description here

insert image description here
My understanding is that the front end will store the data queried from the database into this large array, and then we can directly use the corresponding data in it.
Among them, our uploadImage itself is an embedded page, which is introduced in the script setup, roughly as follows

<script setup name="certificateinfo">
const uploadImage = defineAsyncComponent(() => import('@/components/uploadImage/index.vue'));
</script>

Where we need to use this upload page, we introduce it in the following way

	<div class="width-full">
                <el-form-item label="" :prop="'certificationList.'+index+'.files'" class="form-item-upload" :rules="[{ required: false, message: '请上传资质附件扫描件', trigger: 'change' }]">
                    <slot name="label">
                        <div class="flex-row">
                            <label class="el-form-item__label required">资质附件扫描件</label>
                            <span class="el-form-item__label">资质附件扫描件 (原件或加盖公章复印件,该图片将会展示给用户,请确保图片清晰度)</span>
                            <!-- <span class="example-btn">示例图</span> -->
                        </div>
                    </slot>
                    <!-- <uploadImage v-model="item.pics"></uploadImage> -->
                    <uploadImage v-model="item.files"></uploadImage>
                </el-form-item>
            </div>

Guess you like

Origin blog.csdn.net/Zhangsama1/article/details/131581933
Recommended