diff --git a/heritage-plugins/heritage-oss/pom.xml b/heritage-plugins/heritage-oss/pom.xml
new file mode 100644
index 0000000..0b8e858
--- /dev/null
+++ b/heritage-plugins/heritage-oss/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+ org.leocoder.heritage
+ heritage-plugins
+ ${revision}
+
+
+ heritage-oss
+ heritage-oss
+ 阿里云OSS对象存储模块
+
+
+
+
+ org.leocoder.heritage
+ heritage-common
+ ${revision}
+
+
+
+
+ com.aliyun.oss
+ aliyun-sdk-oss
+
+
+
+
+ io.minio
+ minio
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/annotation/EnableCoderOss.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/annotation/EnableCoderOss.java
new file mode 100644
index 0000000..795585f
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/annotation/EnableCoderOss.java
@@ -0,0 +1,18 @@
+package org.leocoder.heritage.oss.annotation;
+
+import org.leocoder.heritage.oss.config.OssAutoConfiguration;
+import org.springframework.context.annotation.Import;
+
+import java.lang.annotation.*;
+
+/**
+ * 启用Coder OSS插件注解
+ *
+ * @author Leocoder
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(OssAutoConfiguration.class)
+public @interface EnableCoderOss {
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/MinioConfig.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/MinioConfig.java
new file mode 100644
index 0000000..af56dc8
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/MinioConfig.java
@@ -0,0 +1,71 @@
+package org.leocoder.heritage.oss.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * MinIO配置类
+ *
+ * @author Leocoder
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "coder.minio")
+public class MinioConfig {
+
+ /**
+ * MinIO服务端点
+ */
+ private String endpoint;
+
+ /**
+ * Access Key
+ */
+ private String accessKey;
+
+ /**
+ * Secret Key
+ */
+ private String secretKey;
+
+ /**
+ * 存储桶名称
+ */
+ private String bucketName;
+
+ /**
+ * 自定义域名
+ */
+ private String domain;
+
+ /**
+ * 路径前缀
+ */
+ private String pathPrefix = "coder-files";
+
+ /**
+ * 连接超时时间(毫秒)
+ */
+ private Long connectTimeout = 10000L;
+
+ /**
+ * 写入超时时间(毫秒)
+ */
+ private Long writeTimeout = 10000L;
+
+ /**
+ * 读取超时时间(毫秒)
+ */
+ private Long readTimeout = 10000L;
+
+ /**
+ * 是否启用MinIO存储
+ */
+ private Boolean enabled = false;
+
+ /**
+ * 区域设置(可选)
+ */
+ private String region;
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/OssAutoConfiguration.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/OssAutoConfiguration.java
new file mode 100644
index 0000000..9729b49
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/OssAutoConfiguration.java
@@ -0,0 +1,167 @@
+package org.leocoder.heritage.oss.config;
+
+import com.aliyun.oss.ClientBuilderConfiguration;
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import io.minio.MinioClient;
+import lombok.extern.slf4j.Slf4j;
+import org.leocoder.heritage.oss.service.LocalStorageService;
+import org.leocoder.heritage.oss.service.MinioStorageService;
+import org.leocoder.heritage.oss.service.OssStorageService;
+import org.leocoder.heritage.oss.service.StorageServiceFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+
+/**
+ * OSS自动配置类
+ *
+ * @author Leocoder
+ */
+@Configuration
+@EnableConfigurationProperties({OssConfig.class, MinioConfig.class})
+@Slf4j
+public class OssAutoConfiguration {
+
+ /**
+ * @description [OSS客户端配置]
+ * @author Leocoder
+ */
+ @Bean
+ @ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true")
+ @ConditionalOnMissingBean
+ public OSS ossClient(OssConfig ossConfig) {
+ log.info("初始化OSS客户端: endpoint={}, bucketName={}",
+ ossConfig.getEndpoint(), ossConfig.getBucketName());
+
+ try {
+ // 创建ClientBuilderConfiguration
+ ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
+
+ // 设置连接超时时间
+ clientBuilderConfiguration.setConnectionTimeout(ossConfig.getConnectTimeout().intValue());
+
+ // 设置读取超时时间
+ clientBuilderConfiguration.setSocketTimeout(ossConfig.getReadTimeout().intValue());
+
+ // 设置连接池大小
+ clientBuilderConfiguration.setMaxConnections(200);
+
+ // 设置请求超时时间
+ clientBuilderConfiguration.setRequestTimeout(30000);
+
+ // 设置失败重试次数
+ clientBuilderConfiguration.setMaxErrorRetry(3);
+
+ // 创建OSS客户端
+ OSS ossClient = new OSSClientBuilder().build(
+ ossConfig.getEndpoint(),
+ ossConfig.getAccessKeyId(),
+ ossConfig.getAccessKeySecret(),
+ clientBuilderConfiguration
+ );
+
+ // 验证Bucket是否存在
+ if (!ossClient.doesBucketExist(ossConfig.getBucketName())) {
+ log.warn("OSS Bucket不存在: {}", ossConfig.getBucketName());
+ } else {
+ log.info("OSS客户端初始化成功");
+ }
+
+ return ossClient;
+
+ } catch (Exception e) {
+ log.error("OSS客户端初始化失败", e);
+ throw new RuntimeException("OSS客户端初始化失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * @description [OSS存储服务]
+ * @author Leocoder
+ */
+ @Bean
+ @ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true")
+ @ConditionalOnMissingBean
+ public OssStorageService ossStorageService(OSS ossClient, OssConfig ossConfig) {
+ log.info("初始化OSS存储服务");
+ return new OssStorageService(ossClient, ossConfig);
+ }
+
+ /**
+ * @description [本地存储服务(始终可用)]
+ * @author Leocoder
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public LocalStorageService localStorageService(Environment environment) {
+ log.info("初始化本地存储服务");
+ return new LocalStorageService(environment);
+ }
+
+ /**
+ * @description [MinIO客户端配置]
+ * @author Leocoder
+ */
+ @Bean
+ @ConditionalOnProperty(name = "coder.minio.enabled", havingValue = "true")
+ @ConditionalOnMissingBean
+ public MinioClient minioClient(MinioConfig minioConfig) {
+ log.info("初始化MinIO客户端: endpoint={}, bucketName={}",
+ minioConfig.getEndpoint(), minioConfig.getBucketName());
+
+ try {
+ MinioClient.Builder builder = MinioClient.builder()
+ .endpoint(minioConfig.getEndpoint())
+ .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey());
+
+ // 如果配置了区域,则设置区域
+ if (minioConfig.getRegion() != null && !minioConfig.getRegion().trim().isEmpty()) {
+ builder.region(minioConfig.getRegion());
+ }
+
+ MinioClient minioClient = builder.build();
+
+ // 设置超时时间
+ minioClient.setTimeout(
+ minioConfig.getConnectTimeout(),
+ minioConfig.getWriteTimeout(),
+ minioConfig.getReadTimeout()
+ );
+
+ log.info("MinIO客户端初始化成功");
+ return minioClient;
+
+ } catch (Exception e) {
+ log.error("MinIO客户端初始化失败", e);
+ throw new RuntimeException("MinIO客户端初始化失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * @description [MinIO存储服务]
+ * @author Leocoder
+ */
+ @Bean
+ @ConditionalOnProperty(name = "coder.minio.enabled", havingValue = "true")
+ @ConditionalOnMissingBean
+ public MinioStorageService minioStorageService(MinioClient minioClient, MinioConfig minioConfig) {
+ log.info("初始化MinIO存储服务");
+ return new MinioStorageService(minioClient, minioConfig);
+ }
+
+ /**
+ * @description [存储服务工厂(始终可用)]
+ * @author Leocoder
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public StorageServiceFactory storageServiceFactory(ApplicationContext applicationContext) {
+ log.info("初始化存储服务工厂");
+ return new StorageServiceFactory(applicationContext);
+ }
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/OssConfig.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/OssConfig.java
new file mode 100644
index 0000000..78e5cc2
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/config/OssConfig.java
@@ -0,0 +1,66 @@
+package org.leocoder.heritage.oss.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * OSS配置类
+ *
+ * @author Leocoder
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "coder.oss")
+public class OssConfig {
+
+ /**
+ * OSS服务端点
+ */
+ private String endpoint;
+
+ /**
+ * Access Key ID
+ */
+ private String accessKeyId;
+
+ /**
+ * Access Key Secret
+ */
+ private String accessKeySecret;
+
+ /**
+ * 存储桶名称
+ */
+ private String bucketName;
+
+ /**
+ * 自定义域名
+ */
+ private String domain;
+
+ /**
+ * 路径前缀
+ */
+ private String pathPrefix = "coder-files";
+
+ /**
+ * 是否启用HTTPS
+ */
+ private Boolean https = true;
+
+ /**
+ * 连接超时时间(毫秒)
+ */
+ private Long connectTimeout = 10000L;
+
+ /**
+ * 读取超时时间(毫秒)
+ */
+ private Long readTimeout = 10000L;
+
+ /**
+ * 是否启用OSS存储
+ */
+ private Boolean enabled = false;
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/LocalStorageService.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/LocalStorageService.java
new file mode 100644
index 0000000..476e863
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/LocalStorageService.java
@@ -0,0 +1,158 @@
+package org.leocoder.heritage.oss.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.leocoder.heritage.common.constants.CoderConstants;
+import org.leocoder.heritage.common.exception.BusinessException;
+import org.leocoder.heritage.common.utils.file.FileUtil;
+import org.leocoder.heritage.common.utils.file.UploadUtil;
+import org.leocoder.heritage.common.utils.ip.IpUtil;
+import org.leocoder.heritage.common.utils.ip.ServletUtil;
+import org.springframework.core.env.Environment;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * 本地存储服务实现
+ * 基于现有的本地文件存储逻辑进行封装
+ *
+ * @author Leocoder
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class LocalStorageService implements StorageService {
+
+ private final Environment env;
+
+ /**
+ * @description [获取本地存储基本路径]
+ * @author Leocoder
+ */
+ private String getBasePath() {
+ return env.getProperty("coder.filePath", "/tmp/coder-files");
+ }
+
+ /**
+ * @description [上传文件到本地存储]
+ * @author Leocoder
+ */
+ @Override
+ public Map uploadFile(MultipartFile file, String fileName, String folderPath) {
+ try {
+ log.info("本地存储上传文件: fileName={}, folderPath={}", fileName, folderPath);
+
+ // 构建完整的本地存储路径
+ String fullPath = getBasePath() + "/" + folderPath;
+
+ // 使用现有的上传工具类
+ Map fileMap = UploadUtil.coderSingleFile(file, fullPath, 2);
+
+ // UploadUtil已经返回了正确的相对路径,不需要再次构建URL
+
+ log.info("本地存储上传成功: {}", fileMap.get("filePath"));
+ return fileMap;
+
+ } catch (Exception e) {
+ log.error("本地存储上传失败", e);
+ throw new BusinessException(500, "文件上传失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * @description [从本地存储删除文件]
+ * @author Leocoder
+ */
+ @Override
+ public boolean deleteFile(String filePath) {
+ try {
+ if (!StringUtils.hasText(filePath)) {
+ log.warn("文件路径为空,跳过删除");
+ return true;
+ }
+
+ log.info("本地存储删除文件: {}", filePath);
+ boolean result = FileUtil.deleteFile(filePath);
+
+ if (result) {
+ log.info("本地存储删除成功: {}", filePath);
+ } else {
+ log.warn("本地存储删除失败: {}", filePath);
+ }
+
+ return result;
+
+ } catch (Exception e) {
+ log.error("本地存储删除文件异常: {}", filePath, e);
+ return false;
+ }
+ }
+
+ /**
+ * @description [获取文件访问URL]
+ * @author Leocoder
+ */
+ @Override
+ public String getFileUrl(String filePath) {
+ if (!StringUtils.hasText(filePath)) {
+ return "";
+ }
+
+ try {
+ // 如果已经是完整URL,直接返回
+ if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
+ return filePath;
+ }
+
+ // 构建完整URL
+ String protocol = IpUtil.getProtocol(ServletUtil.getRequest());
+ if (!StringUtils.hasText(protocol)) {
+ protocol = "http";
+ }
+ String hostIp = IpUtil.getHostIp(ServletUtil.getRequest());
+ String hostPort = StringUtils.hasText(env.getProperty("server.port")) ?
+ env.getProperty("server.port") : "18099";
+
+ // 处理文件路径,确保以/开头
+ String normalizedPath = filePath.startsWith("/") ? filePath : "/" + filePath;
+
+ return protocol + "://" + hostIp + ":" + hostPort + normalizedPath;
+
+ } catch (Exception e) {
+ log.error("生成文件访问URL失败: {}", filePath, e);
+ return filePath;
+ }
+ }
+
+ /**
+ * @description [检查文件是否存在]
+ * @author Leocoder
+ */
+ @Override
+ public boolean fileExists(String filePath) {
+ if (!StringUtils.hasText(filePath)) {
+ return false;
+ }
+
+ try {
+ File file = new File(filePath);
+ return file.exists() && file.isFile();
+
+ } catch (Exception e) {
+ log.error("检查文件是否存在失败: {}", filePath, e);
+ return false;
+ }
+ }
+
+ /**
+ * @description [获取存储服务类型]
+ * @author Leocoder
+ */
+ @Override
+ public String getStorageType() {
+ // 本地存储对应数据库中的"1"
+ return CoderConstants.ONE_STRING;
+ }
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/MinioStorageService.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/MinioStorageService.java
new file mode 100644
index 0000000..a268031
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/MinioStorageService.java
@@ -0,0 +1,229 @@
+package org.leocoder.heritage.oss.service;
+
+import io.minio.*;
+import io.minio.errors.ErrorResponseException;
+import io.minio.http.Method;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.leocoder.heritage.common.constants.CoderConstants;
+import org.leocoder.heritage.common.exception.BusinessException;
+import org.leocoder.heritage.common.utils.file.FileTypeUtil;
+import org.leocoder.heritage.oss.config.MinioConfig;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * MinIO对象存储服务实现
+ *
+ * @author Leocoder
+ */
+@RequiredArgsConstructor
+@Slf4j
+@Service
+@ConditionalOnProperty(name = "coder.minio.enabled", havingValue = "true")
+public class MinioStorageService implements StorageService {
+
+ private final MinioClient minioClient;
+ private final MinioConfig minioConfig;
+
+ /**
+ * @description [上传文件到MinIO]
+ * @author Leocoder
+ */
+ @Override
+ public Map uploadFile(MultipartFile file, String fileName, String folderPath) {
+ try {
+ // 确保存储桶存在
+ ensureBucketExists();
+
+ // 构建对象名称
+ String objectName = buildObjectName(folderPath, fileName);
+
+ // 上传文件
+ minioClient.putObject(
+ PutObjectArgs.builder()
+ .bucket(minioConfig.getBucketName())
+ .object(objectName)
+ .stream(file.getInputStream(), file.getSize(), -1)
+ .contentType(file.getContentType())
+ .build()
+ );
+
+ // 构建返回结果Map(保持与现有接口兼容)
+ Map fileMap = new HashMap<>();
+ fileMap.put("fileName", file.getOriginalFilename());
+ fileMap.put("newName", fileName);
+ fileMap.put("fileSize", FileTypeUtil.getFileSize(file));
+ fileMap.put("suffixName", FileTypeUtil.getFileType(file.getOriginalFilename()));
+ // MinIO对象名,用于删除操作
+ fileMap.put("filePath", objectName);
+ // 完整的访问URL
+ fileMap.put("fileUploadPath", getFileUrl(objectName));
+
+ log.info("MinIO文件上传成功: {}", fileName);
+ return fileMap;
+
+ } catch (Exception e) {
+ log.error("MinIO文件上传失败", e);
+ throw new BusinessException(500, "文件上传失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * @description [从MinIO删除文件]
+ * @author Leocoder
+ */
+ @Override
+ public boolean deleteFile(String objectName) {
+ try {
+ if (!StringUtils.hasText(objectName)) {
+ log.warn("MinIO对象名为空,跳过删除");
+ return true;
+ }
+
+ log.info("MinIO删除文件: {}", objectName);
+ minioClient.removeObject(
+ RemoveObjectArgs.builder()
+ .bucket(minioConfig.getBucketName())
+ .object(objectName)
+ .build()
+ );
+
+ log.info("MinIO删除成功: {}", objectName);
+ return true;
+
+ } catch (Exception e) {
+ log.error("MinIO文件删除失败: {}", objectName, e);
+ return false;
+ }
+ }
+
+ /**
+ * @description [获取文件访问URL]
+ * @author Leocoder
+ */
+ @Override
+ public String getFileUrl(String objectName) {
+ if (!StringUtils.hasText(objectName)) {
+ return "";
+ }
+
+ try {
+ // 如果配置了自定义域名,构建直接访问URL
+ if (StringUtils.hasText(minioConfig.getDomain())) {
+ String cleanDomain = minioConfig.getDomain().replaceAll("/$", "");
+ String cleanObjectName = objectName.startsWith("/") ? objectName.substring(1) : objectName;
+ String directUrl = cleanDomain + "/" + minioConfig.getBucketName() + "/" + cleanObjectName;
+ return directUrl;
+ }
+
+ // 如果没有自定义域名,使用MinIO的预签名URL(有效期24小时)
+ String presignedUrl = minioClient.getPresignedObjectUrl(
+ GetPresignedObjectUrlArgs.builder()
+ .method(Method.GET)
+ .bucket(minioConfig.getBucketName())
+ .object(objectName)
+ .expiry(24 * 60 * 60)
+ .build()
+ );
+ return presignedUrl;
+
+ } catch (Exception e) {
+ log.error("生成MinIO文件访问URL失败: {}", objectName, e);
+ return "";
+ }
+ }
+
+ /**
+ * @description [检查文件是否存在]
+ * @author Leocoder
+ */
+ @Override
+ public boolean fileExists(String objectName) {
+ try {
+ if (!StringUtils.hasText(objectName)) {
+ return false;
+ }
+
+ minioClient.statObject(
+ StatObjectArgs.builder()
+ .bucket(minioConfig.getBucketName())
+ .object(objectName)
+ .build()
+ );
+ return true;
+
+ } catch (ErrorResponseException e) {
+ if ("NoSuchKey".equals(e.errorResponse().code())) {
+ return false;
+ }
+ log.error("检查MinIO文件是否存在失败: {}", objectName, e);
+ return false;
+ } catch (Exception e) {
+ log.error("检查MinIO文件是否存在失败: {}", objectName, e);
+ return false;
+ }
+ }
+
+ /**
+ * @description [获取存储服务类型]
+ * @author Leocoder
+ */
+ @Override
+ public String getStorageType() {
+ // MinIO存储对应数据库中的"2"
+ return CoderConstants.TWO_STRING;
+ }
+
+ /**
+ * @description [构建MinIO对象名称]
+ * @author Leocoder
+ */
+ private String buildObjectName(String folderPath, String fileName) {
+ StringBuilder nameBuilder = new StringBuilder();
+
+ // 添加路径前缀
+ if (StringUtils.hasText(minioConfig.getPathPrefix())) {
+ nameBuilder.append(minioConfig.getPathPrefix()).append("/");
+ }
+
+ // 添加文件夹路径
+ if (StringUtils.hasText(folderPath)) {
+ // 确保路径以/结尾
+ String normalizedPath = folderPath.endsWith("/") ? folderPath : folderPath + "/";
+ nameBuilder.append(normalizedPath);
+ }
+
+ // 添加文件名
+ nameBuilder.append(fileName);
+
+ return nameBuilder.toString();
+ }
+
+ /**
+ * @description [确保存储桶存在]
+ * @author Leocoder
+ */
+ private void ensureBucketExists() throws Exception {
+ boolean bucketExists = minioClient.bucketExists(
+ BucketExistsArgs.builder()
+ .bucket(minioConfig.getBucketName())
+ .build()
+ );
+
+ if (!bucketExists) {
+ log.info("创建MinIO存储桶: {}", minioConfig.getBucketName());
+ minioClient.makeBucket(
+ MakeBucketArgs.builder()
+ .bucket(minioConfig.getBucketName())
+ .region(minioConfig.getRegion())
+ .build()
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/OssStorageService.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/OssStorageService.java
new file mode 100644
index 0000000..31db250
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/OssStorageService.java
@@ -0,0 +1,189 @@
+package org.leocoder.heritage.oss.service;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.model.ObjectMetadata;
+import com.aliyun.oss.model.PutObjectRequest;
+import com.aliyun.oss.model.PutObjectResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.leocoder.heritage.common.constants.CoderConstants;
+import org.leocoder.heritage.common.exception.BusinessException;
+import org.leocoder.heritage.common.utils.file.FileTypeUtil;
+import org.leocoder.heritage.oss.config.OssConfig;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 阿里云OSS存储服务实现
+ *
+ * @author Leocoder
+ */
+@RequiredArgsConstructor
+@Slf4j
+@ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true")
+public class OssStorageService implements StorageService {
+
+ private final OSS ossClient;
+ private final OssConfig ossConfig;
+
+ /**
+ * @description [上传文件到阿里云OSS]
+ * @author Leocoder
+ */
+ @Override
+ public Map uploadFile(MultipartFile file, String fileName, String folderPath) {
+ try {
+ log.info("OSS上传文件: fileName={}, folderPath={}", fileName, folderPath);
+
+ // 构建对象键
+ String objectKey = buildObjectKey(folderPath, fileName);
+
+ // 设置元数据
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(file.getSize());
+ metadata.setContentType(file.getContentType());
+ metadata.setContentDisposition("inline");
+
+ // 创建上传请求
+ PutObjectRequest putObjectRequest = new PutObjectRequest(
+ ossConfig.getBucketName(),
+ objectKey,
+ file.getInputStream(),
+ metadata
+ );
+
+ // 执行上传
+ PutObjectResult result = ossClient.putObject(putObjectRequest);
+
+ // 构建返回结果Map(保持与现有接口兼容)
+ Map fileMap = new HashMap<>();
+ fileMap.put("fileName", file.getOriginalFilename());
+ fileMap.put("newName", fileName);
+ fileMap.put("fileSize", FileTypeUtil.getFileSize(file));
+ fileMap.put("suffixName", FileTypeUtil.getFileType(file.getOriginalFilename()));
+ // OSS对象键,用于删除操作
+ fileMap.put("filePath", objectKey);
+ // 完整的访问URL
+ fileMap.put("fileUploadPath", getFileUrl(objectKey));
+
+ log.info("OSS上传成功: objectKey={}, url={}", objectKey, fileMap.get("fileUploadPath"));
+ return fileMap;
+
+ } catch (Exception e) {
+ log.error("OSS文件上传失败", e);
+ throw new BusinessException(500, "文件上传失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * @description [从阿里云OSS删除文件]
+ * @author Leocoder
+ */
+ @Override
+ public boolean deleteFile(String objectKey) {
+ try {
+ if (!StringUtils.hasText(objectKey)) {
+ log.warn("OSS对象键为空,跳过删除");
+ return true;
+ }
+
+ log.info("OSS删除文件: {}", objectKey);
+ ossClient.deleteObject(ossConfig.getBucketName(), objectKey);
+
+ log.info("OSS删除成功: {}", objectKey);
+ return true;
+
+ } catch (Exception e) {
+ log.error("OSS文件删除失败: {}", objectKey, e);
+ return false;
+ }
+ }
+
+ /**
+ * @description [获取文件访问URL]
+ * @author Leocoder
+ */
+ @Override
+ public String getFileUrl(String objectKey) {
+ if (!StringUtils.hasText(objectKey)) {
+ return "";
+ }
+
+ try {
+ // 如果配置了自定义域名,使用自定义域名
+ if (StringUtils.hasText(ossConfig.getDomain())) {
+ return ossConfig.getDomain() + "/" + objectKey;
+ }
+
+ // 使用默认的OSS域名
+ String protocol = ossConfig.getHttps() ? "https" : "http";
+ return protocol + "://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint() + "/" + objectKey;
+
+ } catch (Exception e) {
+ log.error("生成OSS文件访问URL失败: {}", objectKey, e);
+ return "";
+ }
+ }
+
+ /**
+ * @description [检查文件是否存在]
+ * @author Leocoder
+ */
+ @Override
+ public boolean fileExists(String objectKey) {
+ try {
+ if (!StringUtils.hasText(objectKey)) {
+ return false;
+ }
+
+ return ossClient.doesObjectExist(ossConfig.getBucketName(), objectKey);
+
+ } catch (Exception e) {
+ log.error("检查OSS文件是否存在失败: {}", objectKey, e);
+ return false;
+ }
+ }
+
+ /**
+ * @description [获取存储服务类型]
+ * @author Leocoder
+ */
+ @Override
+ public String getStorageType() {
+ // OSS存储对应数据库中的"3"
+ return "3";
+ }
+
+ /**
+ * @description [构建OSS对象键]
+ * @author Leocoder
+ */
+ private String buildObjectKey(String folderPath, String fileName) {
+ StringBuilder keyBuilder = new StringBuilder();
+
+ // 添加路径前缀
+ if (StringUtils.hasText(ossConfig.getPathPrefix())) {
+ keyBuilder.append(ossConfig.getPathPrefix()).append("/");
+ }
+
+ // 添加文件夹路径
+ if (StringUtils.hasText(folderPath)) {
+ // 确保路径以/结尾
+ String normalizedPath = folderPath.endsWith("/") ? folderPath : folderPath + "/";
+ keyBuilder.append(normalizedPath);
+ }
+
+ // 添加文件名
+ keyBuilder.append(fileName);
+
+ return keyBuilder.toString();
+ }
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/StorageService.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/StorageService.java
new file mode 100644
index 0000000..50ad28a
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/StorageService.java
@@ -0,0 +1,44 @@
+package org.leocoder.heritage.oss.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Map;
+
+/**
+ * 存储服务接口
+ * 提供统一的文件存储操作接口,支持多种存储类型(本地、OSS等)
+ *
+ * @author Leocoder
+ */
+public interface StorageService {
+
+ /**
+ * @description [上传文件]
+ * @author Leocoder
+ */
+ Map uploadFile(MultipartFile file, String fileName, String folderPath);
+
+ /**
+ * @description [删除文件]
+ * @author Leocoder
+ */
+ boolean deleteFile(String filePath);
+
+ /**
+ * @description [获取文件访问URL]
+ * @author Leocoder
+ */
+ String getFileUrl(String filePath);
+
+ /**
+ * @description [检查文件是否存在]
+ * @author Leocoder
+ */
+ boolean fileExists(String filePath);
+
+ /**
+ * @description [获取存储服务类型]
+ * @author Leocoder
+ */
+ String getStorageType();
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/StorageServiceFactory.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/StorageServiceFactory.java
new file mode 100644
index 0000000..48ea992
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/service/StorageServiceFactory.java
@@ -0,0 +1,93 @@
+package org.leocoder.heritage.oss.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.leocoder.heritage.common.exception.BusinessException;
+import org.springframework.context.ApplicationContext;
+
+import java.util.Map;
+
+/**
+ * 存储服务工厂
+ * 根据配置类型获取对应的存储服务实现
+ *
+ * @author Leocoder
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class StorageServiceFactory {
+
+ private final ApplicationContext applicationContext;
+
+ /**
+ * @description [根据存储类型获取存储服务]
+ * @author Leocoder
+ */
+ public StorageService getStorageService(String storageType) {
+ if (storageType == null || storageType.trim().isEmpty()) {
+ throw new BusinessException(400, "存储类型不能为空");
+ }
+
+ try {
+ // 获取所有StorageService实现
+ Map storageServiceMap = applicationContext.getBeansOfType(StorageService.class);
+
+ for (StorageService service : storageServiceMap.values()) {
+ if (isMatchingStorageType(service, storageType.toLowerCase())) {
+ log.debug("获取存储服务: {} -> {}", storageType, service.getClass().getSimpleName());
+ return service;
+ }
+ }
+
+ throw new BusinessException(404, "未找到对应的存储服务实现: " + storageType);
+
+ } catch (Exception e) {
+ log.error("获取存储服务失败: {}", storageType, e);
+ throw new BusinessException(500, "获取存储服务失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * @description [获取默认存储服务(MinIO优先,然后OSS,最后本地存储)]
+ * @author Leocoder
+ */
+ public StorageService getDefaultStorageService() {
+ try {
+ // 优先尝试获取MinIO服务
+ return getStorageService("minio");
+ } catch (Exception e) {
+ log.warn("MinIO服务不可用,尝试OSS服务", e);
+ try {
+ // 尝试获取OSS服务
+ return getStorageService("oss");
+ } catch (Exception ex) {
+ log.warn("OSS服务不可用,使用本地存储作为降级方案", ex);
+ try {
+ return getStorageService("local");
+ } catch (Exception localEx) {
+ log.error("本地存储服务也不可用", localEx);
+ throw new BusinessException(500, "没有可用的存储服务");
+ }
+ }
+ }
+ }
+
+ /**
+ * @description [检查存储服务是否匹配指定类型]
+ * @author Leocoder
+ */
+ private boolean isMatchingStorageType(StorageService service, String storageType) {
+ String serviceClassName = service.getClass().getSimpleName().toLowerCase();
+
+ switch (storageType) {
+ case "local":
+ return serviceClassName.contains("local");
+ case "oss":
+ return serviceClassName.contains("oss");
+ case "minio":
+ return serviceClassName.contains("minio");
+ default:
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/utils/OssUtil.java b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/utils/OssUtil.java
new file mode 100644
index 0000000..b6a7028
--- /dev/null
+++ b/heritage-plugins/heritage-oss/src/main/java/org/leocoder/heritage/oss/utils/OssUtil.java
@@ -0,0 +1,126 @@
+package org.leocoder.heritage.oss.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.leocoder.heritage.common.utils.date.DateUtil;
+import org.leocoder.heritage.common.utils.file.FileTypeUtil;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * OSS工具类
+ *
+ * @author Leocoder
+ */
+@Slf4j
+public class OssUtil {
+
+ /**
+ * @description [生成唯一文件名]
+ * @author Leocoder
+ */
+ public static String generateUniqueFileName(String originalFilename) {
+ if (!StringUtils.hasText(originalFilename)) {
+ throw new IllegalArgumentException("原始文件名不能为空");
+ }
+
+ // 获取文件扩展名
+ String extension = FileTypeUtil.getFileType(originalFilename);
+
+ // 生成时间戳
+ String timeStamp = DateUtil.getCurrentDate("yyyyMMddHHmmss");
+
+ // 生成6位UUID
+ String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 6);
+
+ // 组合文件名
+ return timeStamp + "-" + uuid + "." + extension;
+ }
+
+ /**
+ * @description [构建文件夹路径]
+ * @author Leocoder
+ */
+ public static String buildFolderPath(String folderName, String username) {
+ LocalDateTime now = LocalDateTime.now();
+
+ StringBuilder pathBuilder = new StringBuilder();
+
+ if (StringUtils.hasText(folderName)) {
+ pathBuilder.append(folderName);
+ }
+
+ if (StringUtils.hasText(username)) {
+ pathBuilder.append("/").append(username);
+ }
+
+ // 添加日期目录结构
+ pathBuilder.append("/")
+ .append(now.getYear())
+ .append("/")
+ .append(String.format("%02d", now.getMonthValue()))
+ .append("/")
+ .append(String.format("%02d", now.getDayOfMonth()));
+
+ return pathBuilder.toString();
+ }
+
+ /**
+ * @description [验证OSS对象键格式]
+ * @author Leocoder
+ */
+ public static boolean isValidObjectKey(String objectKey) {
+ if (!StringUtils.hasText(objectKey)) {
+ return false;
+ }
+
+ // OSS对象名不能以/开头
+ if (objectKey.startsWith("/")) {
+ return false;
+ }
+
+ if (objectKey.contains("//")) {
+ return false;
+ }
+
+ // OSS对象名长度限制
+ if (objectKey.length() > 1023) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @description [规范化对象键]
+ * @author Leocoder
+ */
+ public static String normalizeObjectKey(String objectKey) {
+ if (!StringUtils.hasText(objectKey)) {
+ return objectKey;
+ }
+
+ String normalized = objectKey;
+
+ // 移除开头的/
+ while (normalized.startsWith("/")) {
+ normalized = normalized.substring(1);
+ }
+
+ // 替换连续的//为/
+ while (normalized.contains("//")) {
+ normalized = normalized.replace("//", "/");
+ }
+
+ return normalized;
+ }
+
+ /**
+ * @description [获取文件类型编码]
+ * @author Leocoder
+ */
+ public static String getFileTypeCode(String filename) {
+ return FileTypeUtil.checkFileExtension(FileTypeUtil.getFileType(filename));
+ }
+}
\ No newline at end of file