diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java index d51d61d0..7701881e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -13,12 +13,15 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import com.ruoyi.common.config.MinioConfig; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.common.utils.file.MinioService; import com.ruoyi.framework.config.ServerConfig; /** @@ -35,6 +38,12 @@ public class CommonController @Autowired private ServerConfig serverConfig; + @Autowired(required = false) + private MinioService minioService; + + @Autowired(required = false) + private MinioConfig minioConfig; + private static final String FILE_DELIMETER = ","; /** @@ -53,14 +62,30 @@ public class CommonController throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); } String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); - String filePath = RuoYiConfig.getDownloadPath() + fileName; - response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); - FileUtils.setAttachmentResponseHeader(response, realFileName); - FileUtils.writeBytes(filePath, response.getOutputStream()); - if (delete) + if (isMinioEnabled()) { - FileUtils.deleteFile(filePath); + // MinIO 下载 + String objectName = "download/" + fileName; + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + minioService.download(objectName, response.getOutputStream()); + if (delete) + { + minioService.delete(objectName); + } + } + else + { + // 本地文件系统下载 + String filePath = RuoYiConfig.getDownloadPath() + fileName; + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + FileUtils.setAttachmentResponseHeader(response, realFileName); + FileUtils.writeBytes(filePath, response.getOutputStream()); + if (delete) + { + FileUtils.deleteFile(filePath); + } } } catch (Exception e) @@ -77,16 +102,28 @@ public class CommonController { try { - // 上传文件路径 - String filePath = RuoYiConfig.getUploadPath(); - // 上传并返回新文件名称 - String fileName = FileUploadUtils.upload(filePath, file); - String url = serverConfig.getUrl() + fileName; AjaxResult ajax = AjaxResult.success(); - ajax.put("url", url); - ajax.put("fileName", fileName); - ajax.put("newFileName", FileUtils.getName(fileName)); ajax.put("originalFilename", file.getOriginalFilename()); + + if (isMinioEnabled()) + { + // MinIO 上传 + String url = FileUploadUtils.uploadToMinio(minioService, "upload", file, + MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + ajax.put("url", url); + ajax.put("fileName", url); + ajax.put("newFileName", FileUtils.getName(file.getOriginalFilename())); + } + else + { + // 本地文件系统上传 + String filePath = RuoYiConfig.getUploadPath(); + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + } return ajax; } catch (Exception e) @@ -103,22 +140,39 @@ public class CommonController { try { - // 上传文件路径 - String filePath = RuoYiConfig.getUploadPath(); List urls = new ArrayList(); List fileNames = new ArrayList(); List newFileNames = new ArrayList(); List originalFilenames = new ArrayList(); - for (MultipartFile file : files) + + if (isMinioEnabled()) { - // 上传并返回新文件名称 - String fileName = FileUploadUtils.upload(filePath, file); - String url = serverConfig.getUrl() + fileName; - urls.add(url); - fileNames.add(fileName); - newFileNames.add(FileUtils.getName(fileName)); - originalFilenames.add(file.getOriginalFilename()); + // MinIO 上传 + for (MultipartFile file : files) + { + String url = FileUploadUtils.uploadToMinio(minioService, "upload", file, + MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + urls.add(url); + fileNames.add(url); + newFileNames.add(FileUtils.getName(file.getOriginalFilename())); + originalFilenames.add(file.getOriginalFilename()); + } } + else + { + // 本地文件系统上传 + String filePath = RuoYiConfig.getUploadPath(); + for (MultipartFile file : files) + { + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + } + AjaxResult ajax = AjaxResult.success(); ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); @@ -160,4 +214,12 @@ public class CommonController log.error("下载文件失败", e); } } + + /** + * 判断 MinIO 是否启用 + */ + private boolean isMinioEnabled() + { + return minioService != null && minioConfig != null && minioConfig.isEnabled(); + } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/FileUploadController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/FileUploadController.java index e5dd3939..ab07fc54 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/FileUploadController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/FileUploadController.java @@ -1,12 +1,15 @@ package com.ruoyi.web.controller.common; import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.config.MinioConfig; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.exception.NonCaptureException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.common.utils.file.MinioService; import com.ruoyi.framework.config.ServerConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,26 +28,53 @@ public class FileUploadController { @Autowired private ServerConfig serverConfig; + @Autowired(required = false) + private MinioService minioService; + + @Autowired(required = false) + private MinioConfig minioConfig; + @Anonymous @PostMapping("/upload") @SuppressWarnings("DuplicatedCode") public AjaxResult uploadFile(MultipartFile file) { try { log.info("文件 {} 上传中...", file.getOriginalFilename()); - // 上传文件路径 - String filePath = RuoYiConfig.getUploadPath(); - // 上传并返回新文件名称 - String fileName = FileUploadUtils.upload(filePath, file); - String url = serverConfig.getUrl() + fileName; AjaxResult ajax = AjaxResult.success(); - ajax.put("url", url); - ajax.put("fileName", fileName); - ajax.put("newFileName", FileUtils.getName(fileName)); ajax.put("originalFilename", file.getOriginalFilename()); + + if (isMinioEnabled()) + { + // MinIO 上传 + String url = FileUploadUtils.uploadToMinio(minioService, "upload", file, + MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + ajax.put("url", url); + ajax.put("fileName", url); + ajax.put("newFileName", FileUtils.getName(file.getOriginalFilename())); + } + else + { + // 本地文件系统上传 + String filePath = RuoYiConfig.getUploadPath(); + String fileName = FileUploadUtils.upload(filePath, file); + String url = serverConfig.getUrl() + fileName; + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + } + log.info("文件 {} 上传成功!", file.getOriginalFilename()); return ajax; } catch (Exception e) { throw new NonCaptureException(StringUtils.format("文件 {} 上传失败!", file.getOriginalFilename()), e); } } -} \ No newline at end of file + + /** + * 判断 MinIO 是否启用 + */ + private boolean isMinioEnabled() + { + return minioService != null && minioConfig != null && minioConfig.isEnabled(); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java index 89e9bd33..0d50d067 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.config.MinioConfig; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; @@ -21,6 +22,7 @@ import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.MimeTypeUtils; +import com.ruoyi.common.utils.file.MinioService; import com.ruoyi.framework.web.service.TokenService; import com.ruoyi.system.service.ISysUserService; @@ -39,6 +41,12 @@ public class SysProfileController extends BaseController @Autowired private TokenService tokenService; + @Autowired(required = false) + private MinioService minioService; + + @Autowired(required = false) + private MinioConfig minioConfig; + /** * 个人信息 */ @@ -124,7 +132,15 @@ public class SysProfileController extends BaseController if (!file.isEmpty()) { LoginUser loginUser = getLoginUser(); - String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION); + String avatar; + if (isMinioEnabled()) + { + avatar = FileUploadUtils.uploadToMinio(minioService, "avatar", file, MimeTypeUtils.IMAGE_EXTENSION); + } + else + { + avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION); + } if (userService.updateUserAvatar(loginUser.getUsername(), avatar)) { AjaxResult ajax = AjaxResult.success(); @@ -137,4 +153,12 @@ public class SysProfileController extends BaseController } return error("上传图片异常,请联系管理员"); } + + /** + * 判断 MinIO 是否启用 + */ + private boolean isMinioEnabled() + { + return minioService != null && minioConfig != null && minioConfig.isEnabled(); + } } diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 0606a0f8..e3bb383f 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -13,6 +13,19 @@ ruoyi: # 验证码类型 math 数字计算 char 字符验证 captchaType: math +# MinIO 对象存储配置(开启后将文件存储到 MinIO 而非本地文件系统) +minio: + # 是否启用(false 则使用本地文件系统) + enabled: false + # MinIO 服务地址 + endpoint: http://140.143.206.120:10990 + # 访问密钥 + accessKey: adminMinio + # 秘密密钥 + secretKey: adminMinio + # 存储桶名称 + bucketName: double-rack + # 开发环境配置 server: # 服务器的HTTP端口,默认为8080 diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml index b0dddd96..a301ad1b 100644 --- a/ruoyi-common/pom.xml +++ b/ruoyi-common/pom.xml @@ -113,6 +113,13 @@ UserAgentUtils + + + io.minio + minio + 8.5.7 + + javax.servlet diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/MinioConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/MinioConfig.java new file mode 100644 index 00000000..754bb62b --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/MinioConfig.java @@ -0,0 +1,79 @@ +package com.ruoyi.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * MinIO 对象存储配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "minio") +public class MinioConfig +{ + /** 是否启用 MinIO(false 则使用本地文件系统) */ + private boolean enabled = false; + + /** MinIO 服务地址 */ + private String endpoint; + + /** 访问密钥 */ + private String accessKey; + + /** 秘密密钥 */ + private String secretKey; + + /** 存储桶名称 */ + private String bucketName; + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public String getEndpoint() + { + return endpoint; + } + + public void setEndpoint(String endpoint) + { + this.endpoint = endpoint; + } + + public String getAccessKey() + { + return accessKey; + } + + public void setAccessKey(String accessKey) + { + this.accessKey = accessKey; + } + + public String getSecretKey() + { + return secretKey; + } + + public void setSecretKey(String secretKey) + { + this.secretKey = secretKey; + } + + public String getBucketName() + { + return bucketName; + } + + public void setBucketName(String bucketName) + { + this.bucketName = bucketName; + } +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java index 5a0ef640..8abe2097 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUploadUtils.java @@ -214,6 +214,36 @@ public class FileUploadUtils return false; } + /** + * 上传文件到 MinIO(使用与本地文件系统相同的文件名规则) + * + * @param minioService MinIO 服务实例 + * @param baseDir 基础目录(如 upload、avatar) + * @param file 上传的文件 + * @param allowedExtension 允许的文件扩展名 + * @return MinIO 文件访问 URL + * @throws IOException 上传失败 + */ + public static final String uploadToMinio(MinioService minioService, 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); + + // 生成 objectName: upload/2025/04/13/originalName_uuid.ext + String fileName = extractFilename(file); + String objectName = baseDir + "/" + fileName; + + return minioService.upload(file, objectName); + } + /** * 获取文件名的后缀 * diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MinioService.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MinioService.java new file mode 100644 index 00000000..b03f1eeb --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/MinioService.java @@ -0,0 +1,254 @@ +package com.ruoyi.common.utils.file; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import io.minio.BucketExistsArgs; +import io.minio.GetObjectArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; +import io.minio.StatObjectArgs; +import io.minio.errors.ErrorResponseException; +import io.minio.errors.InsufficientDataException; +import io.minio.errors.InternalException; +import io.minio.errors.InvalidResponseException; +import io.minio.errors.ServerException; +import io.minio.errors.XmlParserException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import com.ruoyi.common.config.MinioConfig; + +import io.minio.http.Method; + +/** + * MinIO 对象存储服务 + * + * @author ruoyi + */ +@Service +@ConditionalOnProperty(prefix = "minio", name = "enabled", havingValue = "true") +public class MinioService +{ + private static final Logger log = LoggerFactory.getLogger(MinioService.class); + + private MinioClient minioClient; + + private MinioConfig minioConfig; + + @Autowired + public MinioService(MinioConfig minioConfig) + { + this.minioConfig = minioConfig; + this.minioClient = MinioClient.builder() + .endpoint(minioConfig.getEndpoint()) + .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey()) + .build(); + ensureBucketExists(); + } + + /** + * 确保存储桶存在,不存在则自动创建 + */ + private void ensureBucketExists() + { + try + { + String bucketName = minioConfig.getBucketName(); + boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + if (!exists) + { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + log.info("MinIO 存储桶 [{}] 已自动创建", bucketName); + } + } + catch (Exception e) + { + log.error("MinIO 存储桶初始化失败", e); + throw new RuntimeException("MinIO 存储桶初始化失败: " + e.getMessage(), e); + } + } + + /** + * 上传文件到 MinIO + * + * @param file 上传的文件 + * @param objectName 对象名称(含路径,如 upload/2025/04/13/xxx.png) + * @return 文件访问 URL + */ + public String upload(MultipartFile file, String objectName) throws IOException + { + try + { + String contentType = file.getContentType() != null ? file.getContentType() : "application/octet-stream"; + minioClient.putObject( + PutObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .stream(file.getInputStream(), file.getSize(), -1) + .contentType(contentType) + .build()); + log.info("MinIO 上传成功: {} -> bucket:{}", objectName, minioConfig.getBucketName()); + return getUrl(objectName); + } + catch (Exception e) + { + log.error("MinIO 上传失败: {}", objectName, e); + throw new IOException("MinIO 上传失败: " + e.getMessage(), e); + } + } + + /** + * 上传输入流到 MinIO + * + * @param stream 输入流 + * @param objectName 对象名称 + * @param size 数据大小 + * @param contentType 内容类型 + * @return 文件访问 URL + */ + public String upload(InputStream stream, String objectName, long size, String contentType) throws IOException + { + try + { + String ct = (contentType != null) ? contentType : "application/octet-stream"; + minioClient.putObject( + PutObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .stream(stream, size, -1) + .contentType(ct) + .build()); + return getUrl(objectName); + } + catch (Exception e) + { + log.error("MinIO 上传失败: {}", objectName, e); + throw new IOException("MinIO 上传失败: " + e.getMessage(), e); + } + } + + /** + * 获取文件访问 URL(7天有效期) + * + * @param objectName 对象名称 + * @return 预签名 URL + */ + public String getUrl(String objectName) + { + try + { + return minioClient.getPresignedObjectUrl( + io.minio.GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(minioConfig.getBucketName()) + .object(objectName) + .expiry(7 * 24 * 60 * 60) // 7天有效期 + .build()); + } + catch (Exception e) + { + log.warn("MinIO 获取 URL 失败: {}", objectName, e); + // 返回拼接直连地址 + return minioConfig.getEndpoint() + "/" + minioConfig.getBucketName() + "/" + objectName; + } + } + + /** + * 下载文件到输出流 + * + * @param objectName 对象名称 + * @param os 输出流 + */ + public void download(String objectName, OutputStream os) throws IOException + { + try + { + try (InputStream is = minioClient.getObject( + GetObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .build())) + { + byte[] buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > -1) + { + os.write(buf, 0, len); + } + } + } + catch (Exception e) + { + log.error("MinIO 下载失败: {}", objectName, e); + throw new IOException("MinIO 下载失败: " + e.getMessage(), e); + } + } + + /** + * 删除文件 + * + * @param objectName 对象名称 + */ + public void delete(String objectName) throws IOException + { + try + { + minioClient.removeObject( + RemoveObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .build()); + log.info("MinIO 删除成功: {}", objectName); + } + catch (Exception e) + { + log.error("MinIO 删除失败: {}", objectName, e); + throw new IOException("MinIO 删除失败: " + e.getMessage(), e); + } + } + + /** + * 判断对象是否存在 + * + * @param objectName 对象名称 + * @return true 存在 + */ + public boolean exists(String objectName) + { + try + { + minioClient.statObject( + StatObjectArgs.builder() + .bucket(minioConfig.getBucketName()) + .object(objectName) + .build()); + return true; + } + catch (ErrorResponseException e) + { + return false; + } + catch (Exception e) + { + log.warn("MinIO 检查对象存在性失败: {}", objectName, e); + return false; + } + } + + /** + * 获取 MinIO 配置 + */ + public MinioConfig getMinioConfig() + { + return minioConfig; + } +}