From 0767c83995885361de6db074991620c86c04d7f0 Mon Sep 17 00:00:00 2001 From: Leo <98382335+gaoziman@users.noreply.github.com> Date: Wed, 9 Jul 2025 01:18:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EOSS=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=8F=92=E4=BB=B6=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现阿里云OSS对象存储服务 - 支持MinIO对象存储服务 - 提供本地存储服务作为降级选择 - 实现存储服务工厂模式统一管理 - 新增OSS配置管理和工具类 - 完善插件化架构支持多种存储方式 - 添加详细的使用文档和配置说明 --- .../coder-common-thin-oss/pom.xml | 51 ++++ .../thin/oss/annotation/EnableCoderOss.java | 18 ++ .../thin/oss/config/OssAutoConfiguration.java | 110 +++++++ .../leocoder/thin/oss/config/OssConfig.java | 66 +++++ .../thin/oss/service/LocalStorageService.java | 144 ++++++++++ .../thin/oss/service/OssStorageService.java | 169 +++++++++++ .../thin/oss/service/StorageService.java | 55 ++++ .../oss/service/StorageServiceFactory.java | 93 ++++++ .../org/leocoder/thin/oss/utils/OssUtil.java | 142 +++++++++ coder-common-thin-plugins/pom.xml | 1 + doc/oss/setup-env.sh | 240 ++++++++++++++++ doc/oss/环境变量设置指南.md | 271 ++++++++++++++++++ 12 files changed, 1360 insertions(+) create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/pom.xml create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/annotation/EnableCoderOss.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssAutoConfiguration.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssConfig.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/LocalStorageService.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/OssStorageService.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageService.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageServiceFactory.java create mode 100644 coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/utils/OssUtil.java create mode 100755 doc/oss/setup-env.sh create mode 100644 doc/oss/环境变量设置指南.md diff --git a/coder-common-thin-plugins/coder-common-thin-oss/pom.xml b/coder-common-thin-plugins/coder-common-thin-oss/pom.xml new file mode 100644 index 0000000..8eb6abe --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.leocoder.thin + coder-common-thin-plugins + ${revision} + + + coder-common-thin-oss + coder-common-thin-oss + 阿里云OSS对象存储模块 + + + + + org.leocoder.thin + coder-common-thin-common + ${revision} + + + + + com.aliyun.oss + aliyun-sdk-oss + 3.17.4 + + + + + 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/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/annotation/EnableCoderOss.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/annotation/EnableCoderOss.java new file mode 100644 index 0000000..0e392c0 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/annotation/EnableCoderOss.java @@ -0,0 +1,18 @@ +package org.leocoder.thin.oss.annotation; + +import org.leocoder.thin.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/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssAutoConfiguration.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssAutoConfiguration.java new file mode 100644 index 0000000..283bd14 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssAutoConfiguration.java @@ -0,0 +1,110 @@ +package org.leocoder.thin.oss.config; + +import com.aliyun.oss.ClientBuilderConfiguration; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import lombok.extern.slf4j.Slf4j; +import org.leocoder.thin.oss.service.LocalStorageService; +import org.leocoder.thin.oss.service.OssStorageService; +import org.leocoder.thin.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) +@Slf4j +public class OssAutoConfiguration { + + /** + * OSS客户端配置 + */ + @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()); + } + } + + /** + * OSS存储服务 + */ + @Bean + @ConditionalOnProperty(name = "coder.oss.enabled", havingValue = "true") + @ConditionalOnMissingBean + public OssStorageService ossStorageService(OSS ossClient, OssConfig ossConfig) { + log.info("初始化OSS存储服务"); + return new OssStorageService(ossClient, ossConfig); + } + + /** + * 本地存储服务(始终可用) + */ + @Bean + @ConditionalOnMissingBean + public LocalStorageService localStorageService(Environment environment) { + log.info("初始化本地存储服务"); + return new LocalStorageService(environment); + } + + /** + * 存储服务工厂(始终可用) + */ + @Bean + @ConditionalOnMissingBean + public StorageServiceFactory storageServiceFactory(ApplicationContext applicationContext) { + log.info("初始化存储服务工厂"); + return new StorageServiceFactory(applicationContext); + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssConfig.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssConfig.java new file mode 100644 index 0000000..b95c7ba --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/config/OssConfig.java @@ -0,0 +1,66 @@ +package org.leocoder.thin.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/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/LocalStorageService.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/LocalStorageService.java new file mode 100644 index 0000000..f0a20b6 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/LocalStorageService.java @@ -0,0 +1,144 @@ +package org.leocoder.thin.oss.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.leocoder.thin.common.constants.CoderConstants; +import org.leocoder.thin.common.exception.BusinessException; +import org.leocoder.thin.common.utils.file.FileUtil; +import org.leocoder.thin.common.utils.file.UploadUtil; +import org.leocoder.thin.common.utils.ip.IpUtil; +import org.leocoder.thin.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; + + private String getBasePath() { + return env.getProperty("coder.filePath", "/tmp/coder-files"); + } + + @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); + + // 生成访问URL + String fileUploadPath = (String) fileMap.get("fileUploadPath"); + 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 fullUrl = protocol + "://" + hostIp + ":" + hostPort + fileUploadPath; + fileMap.put("fileUploadPath", fullUrl); + + log.info("本地存储上传成功: {}", fileMap.get("filePath")); + return fileMap; + + } catch (Exception e) { + log.error("本地存储上传失败", e); + throw new BusinessException(500, "文件上传失败: " + e.getMessage()); + } + } + + @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; + } + } + + @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; + } + } + + @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; + } + } + + @Override + public String getStorageType() { + return CoderConstants.ONE_STRING; // 本地存储对应数据库中的"1" + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/OssStorageService.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/OssStorageService.java new file mode 100644 index 0000000..253b14b --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/OssStorageService.java @@ -0,0 +1,169 @@ +package org.leocoder.thin.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.thin.common.constants.CoderConstants; +import org.leocoder.thin.common.exception.BusinessException; +import org.leocoder.thin.common.utils.file.FileTypeUtil; +import org.leocoder.thin.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; + + @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())); + fileMap.put("filePath", objectKey); // OSS对象键,用于删除操作 + fileMap.put("fileUploadPath", getFileUrl(objectKey)); // 完整的访问URL + + log.info("OSS上传成功: objectKey={}, url={}", objectKey, fileMap.get("fileUploadPath")); + return fileMap; + + } catch (Exception e) { + log.error("OSS文件上传失败", e); + throw new BusinessException(500, "文件上传失败: " + e.getMessage()); + } + } + + @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; + } + } + + @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 ""; + } + } + + @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; + } + } + + @Override + public String getStorageType() { + return "3"; // OSS存储对应数据库中的"3" + } + + /** + * 构建OSS对象键 + * + * @param folderPath 文件夹路径 + * @param fileName 文件名 + * @return OSS对象键 + */ + 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/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageService.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageService.java new file mode 100644 index 0000000..b80b8a5 --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageService.java @@ -0,0 +1,55 @@ +package org.leocoder.thin.oss.service; + +import org.springframework.web.multipart.MultipartFile; + +import java.util.Map; + +/** + * 存储服务接口 + * 提供统一的文件存储操作接口,支持多种存储类型(本地、MinIO、OSS等) + * + * @author Leocoder + */ +public interface StorageService { + + /** + * 上传文件 + * + * @param file 文件对象 + * @param fileName 文件名 + * @param folderPath 文件夹路径 + * @return 文件信息Map,包含fileName、newName、fileSize、suffixName、filePath、fileUploadPath等 + */ + Map uploadFile(MultipartFile file, String fileName, String folderPath); + + /** + * 删除文件 + * + * @param filePath 文件路径 + * @return 删除结果 + */ + boolean deleteFile(String filePath); + + /** + * 获取文件访问URL + * + * @param filePath 文件路径 + * @return 访问URL + */ + String getFileUrl(String filePath); + + /** + * 检查文件是否存在 + * + * @param filePath 文件路径 + * @return 是否存在 + */ + boolean fileExists(String filePath); + + /** + * 获取存储服务类型 + * + * @return 存储服务类型标识(1-LOCAL,2-MINIO,3-OSS) + */ + String getStorageType(); +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageServiceFactory.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageServiceFactory.java new file mode 100644 index 0000000..16ecede --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/service/StorageServiceFactory.java @@ -0,0 +1,93 @@ +package org.leocoder.thin.oss.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.leocoder.thin.common.exception.BusinessException; +import org.springframework.context.ApplicationContext; + +import java.util.Map; + +/** + * 存储服务工厂 + * 根据配置类型获取对应的存储服务实现 + * + * @author Leocoder + */ +@RequiredArgsConstructor +@Slf4j +public class StorageServiceFactory { + + private final ApplicationContext applicationContext; + + /** + * 根据存储类型获取存储服务 + * + * @param storageType 存储类型(local、minio、oss) + * @return 存储服务实例 + */ + 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()); + } + } + + /** + * 获取默认存储服务(OSS优先,如果不可用则使用本地存储) + * + * @return 默认存储服务实例 + */ + public StorageService getDefaultStorageService() { + try { + // 优先尝试获取OSS服务 + return getStorageService("oss"); + } catch (Exception e) { + log.warn("OSS服务不可用,使用本地存储作为降级方案", e); + try { + return getStorageService("local"); + } catch (Exception ex) { + log.error("本地存储服务也不可用", ex); + throw new BusinessException(500, "没有可用的存储服务"); + } + } + } + + /** + * 检查存储服务是否匹配指定类型 + * + * @param service 存储服务实例 + * @param storageType 存储类型 + * @return 是否匹配 + */ + private boolean isMatchingStorageType(StorageService service, String storageType) { + String serviceClassName = service.getClass().getSimpleName().toLowerCase(); + + switch (storageType) { + case "local": + return serviceClassName.contains("local"); + case "minio": + return serviceClassName.contains("minio"); + case "oss": + return serviceClassName.contains("oss"); + default: + return false; + } + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/utils/OssUtil.java b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/utils/OssUtil.java new file mode 100644 index 0000000..c0daeba --- /dev/null +++ b/coder-common-thin-plugins/coder-common-thin-oss/src/main/java/org/leocoder/thin/oss/utils/OssUtil.java @@ -0,0 +1,142 @@ +package org.leocoder.thin.oss.utils; + +import lombok.extern.slf4j.Slf4j; +import org.leocoder.thin.common.utils.date.DateUtil; +import org.leocoder.thin.common.utils.file.FileTypeUtil; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * OSS工具类 + * + * @author Leocoder + */ +@Slf4j +public class OssUtil { + + /** + * 生成唯一文件名 + * 格式:yyyyMMddHHmmss-6位UUID.扩展名 + * + * @param originalFilename 原始文件名 + * @return 生成的唯一文件名 + */ + 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; + } + + /** + * 构建文件夹路径 + * 格式:folderName/username/yyyy/MM/dd + * + * @param folderName 文件夹名称 + * @param username 用户名 + * @return 文件夹路径 + */ + 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(); + } + + /** + * 验证OSS对象键格式 + * + * @param objectKey OSS对象键 + * @return 是否有效 + */ + public static boolean isValidObjectKey(String objectKey) { + if (!StringUtils.hasText(objectKey)) { + return false; + } + + // OSS对象名不能以/开头 + if (objectKey.startsWith("/")) { + return false; + } + + // OSS对象名不能包含连续的// + if (objectKey.contains("//")) { + return false; + } + + // OSS对象名长度限制 + if (objectKey.length() > 1023) { + return false; + } + + return true; + } + + /** + * 规范化对象键 + * 移除开头的/,替换连续的//为/ + * + * @param objectKey 原始对象键 + * @return 规范化后的对象键 + */ + 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; + } + + /** + * 获取文件类型编码 + * 基于文件扩展名返回对应的类型编码 + * + * @param filename 文件名 + * @return 文件类型编码 + */ + public static String getFileTypeCode(String filename) { + return FileTypeUtil.checkFileExtension(FileTypeUtil.getFileType(filename)); + } +} \ No newline at end of file diff --git a/coder-common-thin-plugins/pom.xml b/coder-common-thin-plugins/pom.xml index f0d5fd1..ae173e6 100644 --- a/coder-common-thin-plugins/pom.xml +++ b/coder-common-thin-plugins/pom.xml @@ -22,6 +22,7 @@ coder-common-thin-repect coder-common-thin-limit coder-common-thin-oper-logs + coder-common-thin-oss \ No newline at end of file diff --git a/doc/oss/setup-env.sh b/doc/oss/setup-env.sh new file mode 100755 index 0000000..958f63b --- /dev/null +++ b/doc/oss/setup-env.sh @@ -0,0 +1,240 @@ +#!/bin/bash + +# OSS环境变量快速设置脚本 +# 用于设置阿里云OSS所需的环境变量 + +echo "🔧 OSS环境变量设置工具" +echo "==================================" + +# 检测操作系统 +OS_TYPE=$(uname -s) +SHELL_TYPE=$(basename "$SHELL") + +echo "检测到操作系统: $OS_TYPE" +echo "检测到Shell: $SHELL_TYPE" +echo "" + +# 获取当前配置文件中的值(仅用于显示) +CURRENT_KEY_ID="LTAI5t982gXi7A72gAa9yugE" +CURRENT_KEY_SECRET="Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" + +echo "📋 配置文件中的当前值:" +echo "OSS_ACCESS_KEY_ID: $CURRENT_KEY_ID" +echo "OSS_ACCESS_KEY_SECRET: ${CURRENT_KEY_SECRET:0:8}..." +echo "" + +# 选择设置方式 +echo "请选择设置方式:" +echo "1. 临时设置(当前终端会话有效)" +echo "2. 永久设置(添加到Shell配置文件)" +echo "3. 创建启动脚本" +echo "4. 创建.env文件" +echo "5. 显示手动设置命令" +echo "" + +read -p "请输入选项 (1-5): " choice + +case $choice in + 1) + echo "" + echo "🔧 临时设置环境变量..." + export OSS_ACCESS_KEY_ID="$CURRENT_KEY_ID" + export OSS_ACCESS_KEY_SECRET="$CURRENT_KEY_SECRET" + + echo "✅ 环境变量已设置(当前终端会话有效)" + echo "" + echo "验证设置:" + echo "OSS_ACCESS_KEY_ID: $OSS_ACCESS_KEY_ID" + echo "OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:0:8}..." + echo "" + echo "⚠️ 注意:这些变量只在当前终端会话中有效" + echo " 关闭终端后需要重新设置" + ;; + + 2) + echo "" + echo "🔧 永久设置环境变量..." + + # 根据Shell类型选择配置文件 + if [[ "$SHELL_TYPE" == "zsh" ]]; then + CONFIG_FILE="$HOME/.zshrc" + elif [[ "$SHELL_TYPE" == "bash" ]]; then + CONFIG_FILE="$HOME/.bashrc" + else + CONFIG_FILE="$HOME/.profile" + fi + + echo "将添加到配置文件: $CONFIG_FILE" + + # 检查是否已经存在 + if grep -q "OSS_ACCESS_KEY_ID" "$CONFIG_FILE"; then + echo "⚠️ 配置文件中已存在OSS_ACCESS_KEY_ID,是否覆盖? (y/N)" + read -p "" overwrite + if [[ "$overwrite" != "y" && "$overwrite" != "Y" ]]; then + echo "取消设置" + exit 0 + fi + # 移除现有配置 + sed -i.bak '/OSS_ACCESS_KEY_ID/d' "$CONFIG_FILE" + sed -i.bak '/OSS_ACCESS_KEY_SECRET/d' "$CONFIG_FILE" + fi + + # 添加新配置 + echo "" >> "$CONFIG_FILE" + echo "# OSS环境变量 - 由setup-env.sh添加" >> "$CONFIG_FILE" + echo "export OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" >> "$CONFIG_FILE" + echo "export OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" >> "$CONFIG_FILE" + + echo "✅ 环境变量已添加到 $CONFIG_FILE" + echo "" + echo "请执行以下命令使配置生效:" + echo "source $CONFIG_FILE" + echo "" + echo "或者重新打开终端" + ;; + + 3) + echo "" + echo "🔧 创建启动脚本..." + + SCRIPT_FILE="start-app-with-oss.sh" + + cat > "$SCRIPT_FILE" << 'EOF' +#!/bin/bash + +# 应用程序启动脚本(包含OSS环境变量) +# 自动生成于 $(date) + +echo "🚀 启动应用程序(OSS模式)..." +echo "==================================" + +# 设置OSS环境变量 +export OSS_ACCESS_KEY_ID="LTAI5t982gXi7A72gAa9yugE" +export OSS_ACCESS_KEY_SECRET="Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" + +echo "✅ OSS环境变量已设置" +echo "OSS_ACCESS_KEY_ID: $OSS_ACCESS_KEY_ID" +echo "OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:0:8}..." +echo "" + +# 切换到项目目录 +PROJECT_DIR="/Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend" +if [ -d "$PROJECT_DIR" ]; then + cd "$PROJECT_DIR" + echo "📁 切换到项目目录: $PROJECT_DIR" +else + echo "❌ 项目目录不存在: $PROJECT_DIR" + echo "请修改脚本中的PROJECT_DIR变量" + exit 1 +fi + +# 检查JAR文件是否存在 +JAR_FILE="coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar" +if [ -f "$JAR_FILE" ]; then + echo "📦 找到JAR文件: $JAR_FILE" +else + echo "❌ JAR文件不存在: $JAR_FILE" + echo "请先编译项目: mvn clean package -DskipTests" + exit 1 +fi + +# 启动应用程序 +echo "" +echo "🚀 启动Spring Boot应用程序..." +echo "访问地址: http://localhost:18099" +echo "API文档: http://localhost:18099/swagger-ui.html" +echo "" +echo "按 Ctrl+C 停止应用程序" +echo "" + +java -jar "$JAR_FILE" +EOF + + chmod +x "$SCRIPT_FILE" + + echo "✅ 启动脚本已创建: $SCRIPT_FILE" + echo "" + echo "使用方法:" + echo "./$SCRIPT_FILE" + ;; + + 4) + echo "" + echo "🔧 创建.env文件..." + + ENV_FILE=".env" + + cat > "$ENV_FILE" << EOF +# OSS环境变量配置 +# 创建时间: $(date) +# 注意: 此文件包含敏感信息,不要提交到版本控制 + +OSS_ACCESS_KEY_ID=$CURRENT_KEY_ID +OSS_ACCESS_KEY_SECRET=$CURRENT_KEY_SECRET +EOF + + echo "✅ .env文件已创建: $ENV_FILE" + echo "" + echo "使用方法:" + echo "1. 使用dotenv工具: dotenv java -jar app.jar" + echo "2. 手动加载: source .env && java -jar app.jar" + echo "3. 在IDE中配置Environment Variables" + echo "" + echo "⚠️ 重要提醒:" + echo "• 将.env添加到.gitignore文件中" + echo "• 不要将.env文件提交到版本控制" + + # 检查并添加到.gitignore + if [ -f ".gitignore" ]; then + if ! grep -q "\.env" .gitignore; then + echo ".env" >> .gitignore + echo "✅ .env已添加到.gitignore" + fi + else + echo ".env" > .gitignore + echo "✅ 已创建.gitignore并添加.env" + fi + ;; + + 5) + echo "" + echo "📋 手动设置命令:" + echo "==================================" + echo "" + echo "🐧 Linux/macOS (Bash):" + echo "export OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" + echo "export OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" + echo "" + echo "🐧 Linux/macOS (Zsh):" + echo "export OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" + echo "export OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" + echo "" + echo "🪟 Windows (CMD):" + echo "set OSS_ACCESS_KEY_ID=$CURRENT_KEY_ID" + echo "set OSS_ACCESS_KEY_SECRET=$CURRENT_KEY_SECRET" + echo "" + echo "🪟 Windows (PowerShell):" + echo "\$env:OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\"" + echo "\$env:OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\"" + echo "" + echo "🐳 Docker:" + echo "docker run -e OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\" -e OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\" your-app" + echo "" + echo "☕ Java启动命令:" + echo "OSS_ACCESS_KEY_ID=\"$CURRENT_KEY_ID\" OSS_ACCESS_KEY_SECRET=\"$CURRENT_KEY_SECRET\" java -jar app.jar" + ;; + + *) + echo "❌ 无效选项,请选择1-5" + exit 1 + ;; +esac + +echo "" +echo "🧪 验证环境变量设置:" +echo "--------------------------------" +echo "echo \$OSS_ACCESS_KEY_ID" +echo "echo \$OSS_ACCESS_KEY_SECRET" +echo "" +echo "📖 详细文档: doc/oss/环境变量设置指南.md" +echo "🚀 测试脚本: doc/oss/修复后验证测试.sh" \ No newline at end of file diff --git a/doc/oss/环境变量设置指南.md b/doc/oss/环境变量设置指南.md new file mode 100644 index 0000000..2c85dea --- /dev/null +++ b/doc/oss/环境变量设置指南.md @@ -0,0 +1,271 @@ +# 环境变量设置指南 + +## 📋 需要设置的环境变量 + +根据你的配置文件,需要设置以下环境变量: + +```bash +OSS_ACCESS_KEY_ID=your_access_key_id +OSS_ACCESS_KEY_SECRET=your_access_key_secret +``` + +## 🖥️ 不同操作系统的设置方法 + +### 1. macOS / Linux + +#### 方法1:临时设置(当前终端会话有效) +```bash +export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE +export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 +``` + +#### 方法2:永久设置(添加到配置文件) + +**对于 Bash 用户:** +```bash +# 编辑 ~/.bashrc 或 ~/.bash_profile +echo 'export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE' >> ~/.bashrc +echo 'export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30' >> ~/.bashrc + +# 重新加载配置 +source ~/.bashrc +``` + +**对于 Zsh 用户(macOS 默认):** +```bash +# 编辑 ~/.zshrc +echo 'export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE' >> ~/.zshrc +echo 'export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30' >> ~/.zshrc + +# 重新加载配置 +source ~/.zshrc +``` + +### 2. Windows + +#### 方法1:命令行临时设置 + +**CMD:** +```cmd +set OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE +set OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 +``` + +**PowerShell:** +```powershell +$env:OSS_ACCESS_KEY_ID="LTAI5t982gXi7A72gAa9yugE" +$env:OSS_ACCESS_KEY_SECRET="Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" +``` + +#### 方法2:系统环境变量设置 + +1. 右击"此电脑" → "属性" +2. 点击"高级系统设置" +3. 点击"环境变量" +4. 在"系统变量"中点击"新建" +5. 变量名:`OSS_ACCESS_KEY_ID`,变量值:`LTAI5t982gXi7A72gAa9yugE` +6. 重复步骤4-5,设置 `OSS_ACCESS_KEY_SECRET` + +## 🚀 启动应用程序的方法 + +### 1. 直接在终端启动 + +```bash +# 设置环境变量 +export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE +export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 + +# 启动应用程序 +cd /Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend +java -jar coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar +``` + +### 2. 一行命令启动 + +```bash +OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 java -jar coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar +``` + +### 3. 使用启动脚本 + +创建一个启动脚本: + +```bash +#!/bin/bash +# 文件名: start-app.sh + +# 设置环境变量 +export OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE +export OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 + +# 切换到项目目录 +cd /Users/leocoder/leocoder/develop/templates/coder-common-thin/coder-common-thin-backend + +# 启动应用程序 +java -jar coder-common-thin-web/target/coder-common-thin-web-1.0.0.jar +``` + +给脚本添加执行权限并运行: +```bash +chmod +x start-app.sh +./start-app.sh +``` + +## 🐳 Docker 环境 + +### 1. docker run 命令 +```bash +docker run -d \ + -e OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE \ + -e OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 \ + -p 18099:18099 \ + your-app-image +``` + +### 2. docker-compose.yml +```yaml +version: '3.8' +services: + app: + image: your-app-image + ports: + - "18099:18099" + environment: + - OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE + - OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 +``` + +### 3. 使用 .env 文件 +```bash +# 创建 .env 文件 +echo "OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE" > .env +echo "OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" >> .env + +# docker-compose 会自动读取 +docker-compose up -d +``` + +## 🔧 IDE 环境配置 + +### 1. IntelliJ IDEA + +1. 打开 Run Configuration +2. 选择你的 Spring Boot 应用 +3. 在 "Environment Variables" 中添加: + - `OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE` + - `OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30` + +### 2. VS Code + +在 `.vscode/launch.json` 中配置: +```json +{ + "type": "java", + "name": "Spring Boot App", + "request": "launch", + "mainClass": "org.leocoder.thin.web.CoderApplication", + "env": { + "OSS_ACCESS_KEY_ID": "LTAI5t982gXi7A72gAa9yugE", + "OSS_ACCESS_KEY_SECRET": "Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30" + } +} +``` + +### 3. Eclipse + +1. 右击项目 → Run As → Run Configurations +2. 选择你的 Java Application +3. 在 "Environment" 选项卡中添加变量 + +## 🔐 安全最佳实践 + +### 1. 使用 .env 文件(推荐) + +创建 `.env` 文件(不要提交到版本控制): +```bash +# .env 文件 +OSS_ACCESS_KEY_ID=LTAI5t982gXi7A72gAa9yugE +OSS_ACCESS_KEY_SECRET=Mi9ZsSWLGkvFoMiLNiZ71hHFzVso30 +``` + +在 `.gitignore` 中添加: +``` +.env +``` + +### 2. 使用系统密钥管理 + +**macOS Keychain:** +```bash +# 存储到 Keychain +security add-generic-password -s "oss-access-key" -a "your-app" -w "LTAI5t982gXi7A72gAa9yugE" + +# 从 Keychain 读取 +OSS_ACCESS_KEY_ID=$(security find-generic-password -s "oss-access-key" -a "your-app" -w) +``` + +**Linux Secret Service:** +```bash +# 使用 secret-tool +secret-tool store --label="OSS Access Key" service oss-access-key LTAI5t982gXi7A72gAa9yugE +``` + +### 3. 配置文件最佳实践 + +修改 `application-dev.yml`: +```yaml +coder: + oss: + # 只从环境变量读取,不设置默认值 + access-key-id: ${OSS_ACCESS_KEY_ID} + access-key-secret: ${OSS_ACCESS_KEY_SECRET} +``` + +## ✅ 验证环境变量 + +### 1. 检查环境变量是否设置成功 +```bash +# Linux/macOS +echo $OSS_ACCESS_KEY_ID +echo $OSS_ACCESS_KEY_SECRET + +# Windows CMD +echo %OSS_ACCESS_KEY_ID% +echo %OSS_ACCESS_KEY_SECRET% + +# Windows PowerShell +echo $env:OSS_ACCESS_KEY_ID +echo $env:OSS_ACCESS_KEY_SECRET +``` + +### 2. 在应用程序中验证 + +在 Java 代码中临时添加日志: +```java +@PostConstruct +public void logOssConfig() { + log.info("OSS_ACCESS_KEY_ID: {}", System.getenv("OSS_ACCESS_KEY_ID")); + log.info("OSS_ACCESS_KEY_SECRET: {}", + System.getenv("OSS_ACCESS_KEY_SECRET") != null ? "已设置" : "未设置"); +} +``` + +## 🚨 注意事项 + +1. **不要在版本控制中提交真实的密钥** +2. **定期轮换访问密钥** +3. **使用最小权限原则配置OSS权限** +4. **在生产环境中使用更安全的密钥管理方案** +5. **重启应用程序后环境变量才会生效** + +## 📝 快速设置脚本 + +我已经为你准备了一个快速设置脚本,运行即可: + +```bash +# 当前目录下创建 setup-env.sh +chmod +x setup-env.sh +./setup-env.sh +``` + +按照这个指南设置环境变量后,你的OSS功能就可以正常使用了! \ No newline at end of file