首先需要在服务器上部署并配置好MinIO,因为我已经配置过很多次,此处不再重复记录。
-
后端配置及工具代码
在ruoyi-common/pom.xml文件添加minio依赖。
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.0.3</version>
<exclusions>
<exclusion>
<artifactId>okio</artifactId>
<groupId>com.squareup.okio</groupId>
</exclusion>
<exclusion>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.4.1</version>
</dependency>
在ruoyi-admin文件application.yml,添加minio配置
# Minio配置 minio: url: http://localhost:9000 accessKey: minioadmin secretKey: minioadmin bucketName: ruoyi
在CommonController.java自定义Minio服务器上传请求
/**
* 自定义 Minio 服务器上传请求
*/
@PostMapping("/uploadMinio")
public AjaxResult uploadFileMinio(MultipartFile file) {
try {
// 上传并返回新文件名称
String fileName = FileUploadUtils.uploadMinio(file);
AjaxResult ajax = AjaxResult.success();
ajax.put("url", fileName);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
新增MinioConfig配置类,完整代码如下
package com.ruoyi.common.config;
import io.minio.MinioClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Created with IntelliJ IDEA.
*
* @Author: muluo
* @Date: 2024/09/14/10:38
* @Description:
*/
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
/**
* 服务地址
*/
private static String url;
/**
* 用户名
*/
private static String accessKey;
/**
* 密码
*/
private static String secretKey;
/**
* 存储桶名称
*/
private static String bucketName;
public static String getUrl() {
return url;
}
public void setUrl(String url) {
MinioConfig.url = url;
}
public static String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
MinioConfig.accessKey = accessKey;
}
public static String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
MinioConfig.secretKey = secretKey;
}
public static String getBucketName() {
return bucketName;
}
public void setBucketName(String bucketName) {
MinioConfig.bucketName = bucketName;
}
@Bean
public MinioClient getMinioClient() {
return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
}
}
新增MinioUtil工具类,完整代码如下
package com.ruoyi.common.utils.file;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.http.Method;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
/**
* Minio 文件存储工具类
*
* @author muluo
*/
public class MinioUtil {
/**
* 上传文件
*
* @param bucketName 桶名称
* @param fileName
*
* @throws IOException
*/
public static String uploadFile(String bucketName,
String fileName,
MultipartFile multipartFile) throws IOException {
String url;
MinioClient minioClient = SpringUtils.getBean(MinioClient.class);
try (InputStream inputStream = multipartFile.getInputStream()) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(inputStream, multipartFile.getSize(), -1)
.contentType(multipartFile.getContentType())
.build());
url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(fileName)
.method(Method.GET)
.build());
url = url.substring(0, url.indexOf('?'));
return ServletUtils.urlDecode(url);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
}
在FileUploadUtils工具类新增minio的相关配置,完整代码如下(若依版本3.8.8)
package com.ruoyi.common.utils.file;
import com.ruoyi.common.config.MinioConfig;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.file.FileNameLengthLimitExceededException;
import com.ruoyi.common.exception.file.FileSizeLimitExceededException;
import com.ruoyi.common.exception.file.InvalidExtensionException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.Seq;
import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Objects;
/**
* 文件上传工具类
*
* @author ruoyi
*/
public class FileUploadUtils {
/**
* 默认大小 50M
*/
public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024;
/**
* 默认的文件名最大长度 100
*/
public static final int DEFAULT_FILE_NAME_LENGTH = 100;
/**
* 默认上传的地址
*/
private static String defaultBaseDir = RuoYiConfig.getProfile();
/**
* Minio默认上传的地址
*/
private static final String bucketName = MinioConfig.getBucketName();
public static void setDefaultBaseDir(String defaultBaseDir) {
FileUploadUtils.defaultBaseDir = defaultBaseDir;
}
public static String getDefaultBaseDir() {
return defaultBaseDir;
}
public static String getBucketName() {
return bucketName;
}
/**
* 以默认配置进行文件上传
*
* @param file 上传的文件
*
* @return 文件名称
*
* @throws Exception
*/
public static String upload(MultipartFile file) throws IOException {
try {
return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* 根据文件路径上传
*
* @param baseDir 相对应用的基目录
* @param file 上传的文件
*
* @return 文件名称
*
* @throws IOException
*/
public static String upload(String baseDir,
MultipartFile file) throws IOException {
try {
return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* 文件上传
*
* @param baseDir 相对应用的基目录
* @param file 上传的文件
* @param allowedExtension 上传文件类型
*
* @return 返回上传成功的文件名
*
* @throws FileSizeLimitExceededException 如果超出最大大小
* @throws FileNameLengthLimitExceededException 文件名太长
* @throws IOException 比如读写文件出错时
* @throws InvalidExtensionException 文件校验异常
*/
public static String upload(String baseDir,
MultipartFile file,
String[] allowedExtension) throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, InvalidExtensionException {
int fileNameLength = Objects.requireNonNull(file.getOriginalFilename()).length();
if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
}
assertAllowed(file, allowedExtension);
String fileName = extractFilename(file);
String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
file.transferTo(Paths.get(absPath));
return getPathFileName(baseDir, fileName);
}
/**
* 以默认BucketName配置上传到Minio服务器
*
* @param file 上传的文件
*
* @return 文件名称
*
* @throws Exception
*/
public static String uploadMinio(MultipartFile file) throws IOException {
try {
return uploadMinino(getBucketName(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* 自定义bucketName配置上传到Minio服务器
*
* @param file 上传的文件
*
* @return 文件名称
*
* @throws Exception
*/
public static String uploadMinio(MultipartFile file,
String bucketName) throws IOException {
try {
return uploadMinino(bucketName, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
public static String uploadMinino(String bucketName,
MultipartFile file,
String[] allowedExtension) throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, InvalidExtensionException {
int fileNameLength = file.getOriginalFilename().length();
if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
}
assertAllowed(file, allowedExtension);
try {
String fileName = extractFilename(file);
return MinioUtil.uploadFile(bucketName, fileName, file);
}
catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* 编码文件名
*/
public static String extractFilename(MultipartFile file) {
return StringUtils.format("{}/{}_{}.{}",
DateUtils.datePath(),
FilenameUtils.getBaseName(file.getOriginalFilename()),
Seq.getId(Seq.uploadSeqType),
getExtension(file));
}
public static File getAbsoluteFile(String uploadDir,
String fileName) {
File desc = new File(uploadDir + File.separator + fileName);
if (!desc.exists()) {
if (!desc.getParentFile().exists()) {
desc.getParentFile().mkdirs();
}
}
return desc;
}
public static String getPathFileName(String uploadDir,
String fileName) {
int dirLastIndex = RuoYiConfig.getProfile().length() + 1;
String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
return Constants.RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
}
/**
* 文件大小校验
*
* @param file 上传的文件
*
* @return
*
* @throws FileSizeLimitExceededException 如果超出最大大小
* @throws InvalidExtensionException
*/
public static void assertAllowed(MultipartFile file,
String[] allowedExtension) throws FileSizeLimitExceededException, InvalidExtensionException {
long size = file.getSize();
if (size > DEFAULT_MAX_SIZE) {
throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
}
String fileName = file.getOriginalFilename();
String extension = getExtension(file);
if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) {
if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) {
throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, fileName);
}
else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) {
throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, fileName);
}
else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) {
throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, fileName);
}
else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) {
throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, fileName);
}
else {
throw new InvalidExtensionException(allowedExtension, extension, fileName);
}
}
}
/**
* 判断MIME类型是否是允许的MIME类型
*
* @param extension
* @param allowedExtension
*
* @return
*/
public static boolean isAllowedExtension(String extension,
String[] allowedExtension) {
for (String str : allowedExtension) {
if (str.equalsIgnoreCase(extension)) {
return true;
}
}
return false;
}
/**
* 获取文件名的后缀
*
* @param file 表单文件
*
* @return 后缀名
*/
public static String getExtension(MultipartFile file) {
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
if (StringUtils.isEmpty(extension)) {
extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
}
return extension;
}
}
-
前端使用
-
以上传头像图片为例
先写接口
// 用户头像上传Minio
export function uploadMinIOAvatar(data) {
return request({
url: '/system/user/profile/avatarMinIO',
method: 'post',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: data
})
}
修改ruoyi-ui包里面的src/views/system/user/profile/userAvatar.vue
/** 上传图片 */
function uploadImg() {
proxy.$refs.cropper.getCropBlob(data => {
let formData = new FormData();
formData.append("avatarfile", data, options.filename);
uploadMinIOAvatar(formData).then(response => {
open.value = false;
// options.img = import.meta.env.VITE_APP_BASE_API + response.imgUrl; 修改这一行就行
options.img = response.imgUrl;
userStore.avatar = options.img;
proxy.$modal.msgSuccess("修改成功");
visible.value = false;
});
});
}
修改src/store/modules/user.js的这一行
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : user.avatar;
<template>
<div class="my-groups-container">
<h2>创建群组</h2>
<el-row :gutter="20">
</el-row>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { listCpGroupInfoByUserID } from "../../../api/codePunch/group.js";
import useUserStore from "../../../store/modules/user.js"
import { storeToRefs } from 'pinia';
import { ElMessage, ElMessageBox } from 'element-plus';
import {deleteCpGroupMemberByTwoId} from "../../../api/codePunch/groupmember.js";
const joinedGroups = ref([]);
const totalJoined = ref(0);
const userStore = useUserStore();
const { id: userId, name: userName } = storeToRefs(userStore);
const currentUserId = ref(userId.value);
const groupAvatar = new URL('@/assets/images/defaultGroupAvatar.png', import.meta.url); // 默认群组头像
const joinedGroupParams = computed(() => ({
pageNum: 1,
pageSize: 10,
userId: currentUserId.value ? Number(currentUserId.value) : null
}));
const deleteGroupParams = computed(() => ({
userId: currentUserId.value ? Number(currentUserId.value) : null,
groupId: null
}));
function getJoinedGroups() {
if (currentUserId.value !== null && Number.isInteger(Number(currentUserId.value))) {
listCpGroupInfoByUserID(joinedGroupParams.value).then(response => {
joinedGroups.value = response.rows;
totalJoined.value = response.total;
});
} else {
throw new Error('currentUserId must be a valid Long type');
}
}
getJoinedGroups();
function deleteGroup(groupId) {
ElMessageBox.confirm('确定要退出这个群组吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 调用删除群组的API
deleteCpGroupMemberByTwoId(userId.value, groupId).then(response => {
if (response.code == 200) {
getJoinedGroups();
console.log("退出成功")
}
}).catch(error => {
});
ElMessage({
type: 'success',
message: '群组退出成功'
});
// 更新群组列表
// });
}).catch(() => {
// 取消操作
ElMessage({
type: 'info',
message: '已取消删除'
});
});
}
</script>
<style scoped>
.my-groups-container {
padding: 20px;
}
.group-card {
margin-bottom: 20px;
}
.group-header {
display: flex;
align-items: center;
}
.group-avatar {
margin-right: 15px;
}
.group-info {
flex: 1;
}
.group-footer {
text-align: right;
}
.group-footer .el-button {
margin-top: 10px;
}
.delete-button {
width: 80px; /* 增加按钮宽度 */
height: 30px; /* 增加按钮高度 */
font-size: 16px; /* 增加字体大小 */
padding: 0; /* 移除内边距 */
background-color: transparent; /* 移除背景颜色 */
color: #fff; /* 设置字体颜色 */
border: none; /* 移除边框 */
position: relative; /* 设置相对定位 */
}
.delete-button::before {
content: '';
position: absolute;
width: 80px; /* 设置宽度 */
height: 30px; /* 设置高度 */
background-color: #e7aeae; /* 设置背景颜色 */
border-radius: 5%; /* 设置圆角 */
left: 50%; /* 设置水平居中 */
top: 50%; /* 设置垂直居中 */
transform: translate(-50%, -50%); /* 设置水平垂直居中 */
}
.delete-button::after {
content: '退出群组';
position: absolute;
left: 50%; /* 设置水平居中 */
top: 50%; /* 设置垂直居中 */
transform: translate(-50%, -50%); /* 设置水平垂直居中 */
font-size: 16px; /* 设置字体大小 */
color: #fff; /* 设置字体颜色 */
white-space: nowrap; /* 强制单行显示 */
}
</style>