【Java项目】Vue+ElementUI+Ceph实现多类型文件上传功能并实现文件预览功能

简介: 【Java项目】Vue+ElementUI+Ceph实现多类型文件上传功能并实现文件预览功能

效果演示

先说一下我们的需求,我们的需求就是文件上传,之前的接口是只支持上传图片的,之后需求是需要支持上传pdf,所以我就得换接口,把原先图片上传的接口换为后端ceph,但是其实大致的处理流程都差不多,都是上传到后端然后得到url地址。

要实现点击预览文件,那么就需要使用到element的groupPreview。

前端

ElementUI

文件上传的页面使用的是ElementUI

下面是index.vue页面,有点小bug,不过在我坚持不懈的努力下(大概15h吧哈哈哈),终于给他解决了,只能说,感觉这是“精通前端”的第一步。

然后之后发现了一个问题就是如果用户在文件还没有上传完毕之前,就直接点击了提交,那么就会导致文件的丢失,所以之后我们添加了一个文件上传中的一个全局锁定,保证此时用户不能进行提交操作

页面代码如下

<!-- :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>

大概情况就是我们添加文件的时候,会发送一个请求到后端,这个后端的路径为

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

当点击上传图片之后,我们的网页会发送这个请求,这个请求其实是会被router处理的,如下

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);
};

可以看到这里有一个url,这个url就是你的后端处理前端文件上传的那个接口了。

并且可以看到我们的返回类型要求是code,msg,data,其中data要求有name和url,分别是文件名称和文件路径。

后端Java

我们先来看控制层代码,这里我们的OSS服务使用的是ceph,你也可以使用minio等来代替。

/**
     * 上传订单附件接口,文件可以一次传多个
     * 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;

实体类

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

然后是ceph的service层

@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;
        }
    }
}

实体类

@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;
}

最后的mapper层使用的是mybatisplus,没有任何代码,纯CRUD,就不贴出了

数据存储格式

我们的这些文件数据是存储在MongoDB中的,格式为字符串,如下

“files”: [“QQ截图20230508204810.png|https://xxxxx”,“QQxxx.jpg|https://xxxxx”]

我们的数据结构就是一个字符串数组,并且我们把数组 | 前面的内容设定为文件名称,后面的为文件路径,之所以专门设计是由于业务需求导致,我也不想这么设计。

我的理解是前端会将先从数据库查询出来的数据存放到这个大数组中,之后我们直接使用里面对应的数据就好了。

其中,我们的uploadImage他本身是一个内嵌页面,是在script setup里面引入的,大概如下

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

我们在需要使用这个上传页面的地方通过如下方式引入

<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>


相关文章
|
20天前
|
IDE Java 开发工具
【Java基础-环境搭建-创建项目】IntelliJ IDEA创建Java项目的详细步骤
IntelliJ IDEA创建Java项目的图文详细步骤,手把手带你创建Java项目
159 10
【Java基础-环境搭建-创建项目】IntelliJ IDEA创建Java项目的详细步骤
|
19天前
|
Java 测试技术 项目管理
【JavaEE】从 0 到 1 掌握 Maven 构建 Java 项目核心技巧 解锁 Java 项目高效管理实用实例
本文从Maven基础概念讲起,涵盖安装配置、核心概念(如POM与依赖管理)及优化技巧。结合Java Web项目实例,演示如何用Maven构建和管理项目,解决常见问题,助你高效掌握这一强大工具,提升Java开发与项目管理能力。适合初学者及进阶开发者学习。资源链接:[点此获取](https://pan.quark.cn/s/14fcf913bae6)。
56 6
|
3月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
144 23
|
3月前
|
存储 传感器 缓存
java变量与数据类型:整型、浮点型与字符类型
### Java数据类型全景表简介 本文详细介绍了Java的基本数据类型和引用数据类型,涵盖每种类型的存储空间、默认值、取值范围及使用场景。特别强调了`byte`、`int`、`long`、`float`、`double`等基本类型在不同应用场景中的选择与优化,如文件流处理、金融计算等。引用数据类型部分则解析了`String`、数组、类对象、接口和枚举的内存分配机制。
109 15
|
2月前
|
Java
java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?
本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。
65 0
|
3月前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
本文主要讲解Java中的泛型擦除机制及其引发的问题与解决方法。泛型擦除是指编译期间,Java会将所有泛型信息替换为原始类型,并用限定类型替代类型变量。通过代码示例展示了泛型擦除后原始类型的保留、反射对泛型的破坏以及多态冲突等问题。同时分析了泛型类型不能是基本数据类型、静态方法中无法使用泛型参数等限制,并探讨了解决方案。这些内容对于理解Java泛型的工作原理和避免相关问题具有重要意义。
157 0
|
22天前
|
算法 Java 调度
Java多线程基础
本文主要讲解多线程相关知识,分为两部分。第一部分涵盖多线程概念(并发与并行、进程与线程)、Java程序运行原理(JVM启动多线程特性)、实现多线程的两种方式(继承Thread类与实现Runnable接口)及其区别。第二部分涉及线程同步(同步锁的应用场景与代码示例)及线程间通信(wait()与notify()方法的使用)。通过多个Demo代码实例,深入浅出地解析多线程的核心知识点,帮助读者掌握其实现与应用技巧。
|
4月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
276 60
【Java并发】【线程池】带你从0-1入门线程池
|
2月前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
115 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
1月前
|
Java
java 多线程异常处理
本文介绍了Java中ThreadGroup的异常处理机制,重点讲解UncaughtExceptionHandler的使用。通过示例代码展示了当线程的run()方法抛出未捕获异常时,JVM如何依次查找并调用线程的异常处理器、线程组的uncaughtException方法或默认异常处理器。文章还提供了具体代码和输出结果,帮助理解不同处理器的优先级与执行逻辑。