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


相关文章
|
2月前
|
JavaScript 容器
乾坤qiankun框架搭建 主应用为vue3的项目。
乾坤qiankun框架搭建 主应用为vue3的项目。
187 2
|
11天前
|
Java
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
73 34
|
10天前
|
NoSQL Java 关系型数据库
Liunx部署java项目Tomcat、Redis、Mysql教程
本文详细介绍了如何在 Linux 服务器上安装和配置 Tomcat、MySQL 和 Redis,并部署 Java 项目。通过这些步骤,您可以搭建一个高效稳定的 Java 应用运行环境。希望本文能为您在实际操作中提供有价值的参考。
70 26
|
1月前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
1月前
|
安全 Java 测试技术
🎉Java零基础:全面解析枚举的强大功能
【10月更文挑战第19天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
119 60
|
22天前
|
XML Java 测试技术
从零开始学 Maven:简化 Java 项目的构建与管理
Maven 是一个由 Apache 软件基金会开发的项目管理和构建自动化工具。它主要用在 Java 项目中,但也可以用于其他类型的项目。
36 1
从零开始学 Maven:简化 Java 项目的构建与管理
|
1月前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
21天前
|
Java
Java项目中高精度数值计算:为何BigDecimal优于Double
在Java项目开发中,涉及金额计算、面积计算等高精度数值操作时,应选择 `BigDecimal` 而非 `Double`。`BigDecimal` 提供任意精度的小数运算、多种舍入模式和良好的可读性,确保计算结果的准确性和可靠性。例如,在金额计算中,`BigDecimal` 可以精确到小数点后两位,而 `Double` 可能因精度问题导致结果不准确。
|
1月前
|
Java Android开发
Eclipse 创建 Java 项目
Eclipse 创建 Java 项目
39 4
|
1月前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
30 2