若依集成MinIO实现图片云存储

首先需要在服务器上部署并配置好MinIO,因为我已经配置过很多次,此处不再重复记录。
  1. 后端配置及工具代码

在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;
    }
}
  1. 前端使用

  1. 以上传头像图片为例
先写接口
// 用户头像上传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>

 

点赞

当前页面评论已关闭。

隐藏
变装