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


目录
打赏
0
0
0
0
5
分享
相关文章
|
26天前
|
java变量与数据类型:整型、浮点型与字符类型
### Java数据类型全景表简介 本文详细介绍了Java的基本数据类型和引用数据类型,涵盖每种类型的存储空间、默认值、取值范围及使用场景。特别强调了`byte`、`int`、`long`、`float`、`double`等基本类型在不同应用场景中的选择与优化,如文件流处理、金融计算等。引用数据类型部分则解析了`String`、数组、类对象、接口和枚举的内存分配机制。
55 15
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
|
26天前
|
课时11:Java数据类型划分(浮点类型)
课时11介绍了Java中的浮点数据类型。主要内容包括:1. 定义小数,默认使用Double类型;2. 定义Float变量,需在数值后加&quot;F&quot;或&quot;f&quot;进行强制转换;3. 观察不同类型计算结果,如Int型除法会丢失精度,需至少包含一个Double或Float类型以确保准确性。总结指出,在复杂计算中推荐使用Double类型以避免精度损失。
|
26天前
|
课时10:Java数据类型划分(整型类型)
本文主要围绕Java中整型数据展开,详细讲解整型变量、常量的概念,整型数据运算规则,包括数据溢出问题及解决方法,数据类型转换(自动转换与强制转换)的原理和注意事项,同时介绍了整型数据默认值的相关知识,以及byte数据类型与int数据类型的关系和使用场景,帮助读者全面掌握Java整型数据的相关内容。
Java泛型类型擦除以及类型擦除带来的问题
本文主要讲解Java中的泛型擦除机制及其引发的问题与解决方法。泛型擦除是指编译期间,Java会将所有泛型信息替换为原始类型,并用限定类型替代类型变量。通过代码示例展示了泛型擦除后原始类型的保留、反射对泛型的破坏以及多态冲突等问题。同时分析了泛型类型不能是基本数据类型、静态方法中无法使用泛型参数等限制,并探讨了解决方案。这些内容对于理解Java泛型的工作原理和避免相关问题具有重要意义。
智慧产科一体化管理平台源码,基于Java,Vue,ElementUI技术开发,二开快捷
智慧产科一体化管理平台覆盖从备孕到产后42天的全流程管理,构建科室协同、医患沟通及智能设备互联平台。通过移动端扫码建卡、自助报道、智能采集数据等手段优化就诊流程,提升孕妇就诊体验,并实现高危孕产妇五色管理和孕妇学校三位一体化管理,全面提升妇幼健康宣教质量。
58 12
深潜数据海洋:Java文件读写全面解析与实战指南
通过本文的详细解析与实战示例,您可以系统地掌握Java中各种文件读写操作,从基本的读写到高效的NIO操作,再到文件复制、移动和删除。希望这些内容能够帮助您在实际项目中处理文件数据,提高开发效率和代码质量。
37 4
|
1月前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
172 60
【Java并发】【线程池】带你从0-1入门线程池
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
73 23
|
29天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
100 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等