文章目录
- 效果演示
- 前端
- 后端Java
效果演示
先说一下我们的需求,我们的需求就是文件上传,之前的接口是只支持上传图片的,之后需求是需要支持上传pdf,所以我就得换接口,把原先图片上传的接口换为后端ceph,但是其实大致的处理流程都差不多,都是上传到后端然后得到url地址。
要实现点击预览文件,那么就需要使用到element的groupPreview。
前端
ElementUI
文件上传的页面使用的是ElementUI的
下面是index.vue页面,有点小bug(但是我真改不动,前端还在学习中)
<template>
<div>
<el-upload
class="upload-demo"
:action="uploadAction"
:on-remove="handleRemove"
:on-success="handleSuccess"
:on-error="handleError"
:before-upload="beforeUpload"
:on-preview="groupPreview"
multiple
:file-list="fileList"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png/pdf文件,且不超过20M</div>
<!-- <div v-for="(file, index) in fileList" :key="index">
<a :href="getURL(file)" target="_blank">{{ file.name }}</a>
</div> -->
</el-upload>
</div>
</template>
<script setup name="uploadImage">
import { ref, defineProps, onMounted, defineEmits } from 'vue';
import { ElMessage } from 'element-plus';
const props = defineProps(['modelValue'])
const fileList = ref([])
const urls = ref([])
const uploadAction = ref('/merchant/api/common/upload/image')
const emits = defineEmits(['update:modelValue','input'])
onMounted(() => {
setDefaultFileList()
})
const setDefaultFileList = () => {
//这里的modelValue就是mainProduct
if (props.modelValue && props.modelValue.length > 0) {
fileList.value = []
let i = 1;
props.modelValue.forEach(element => {
fileList.value.push({
// name: "文件"+i,
url: element,
name: "点击预览文件",
})
i=i+1;
});
}
}
const getURL = (file) => {
// 根据文件索引获取对应的URL地址
return this.urls[this.fileList.indexOf(file)];
}
const handleRemove = (file, fileList) => {
let fileList1 = fileList.map(item => item.response.data.name)
emits("update:modelValue", fileList1);
}
//文件上传还有一点bug
//如果不把所有文件都删掉,只删除部分的那么上传失败
const handleSuccess = (response, file, fileList) => {
let fileList1 = fileList.map(item => item.response.data.name)
emits("update:modelValue", fileList1);
}
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:8081/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);
res.send({
code: 200,
msg: "图片上传成功",
data: {
//这里的name,result。data的值就是那个url地址
//这里就是向后端接口发送请求然后获取url地址
name: result.data,
},
});
} 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,就不贴出了