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