feat: 新增OSS对象存储插件模块

- 实现阿里云OSS对象存储服务
- 支持MinIO对象存储服务
- 提供本地存储服务作为降级选择
- 实现存储服务工厂模式统一管理
- 新增OSS配置管理和工具类
- 完善插件化架构支持多种存储方式
- 添加详细的使用文档和配置说明
This commit is contained in:
Leo 2025-07-09 01:18:02 +08:00
parent 33b341e9e7
commit 0767c83995
12 changed files with 1360 additions and 0 deletions

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-plugins</artifactId>
<version>${revision}</version>
</parent>
<name>coder-common-thin-oss</name>
<artifactId>coder-common-thin-oss</artifactId>
<description>阿里云OSS对象存储模块</description>
<dependencies>
<!-- 全局公共模块 -->
<dependency>
<groupId>org.leocoder.thin</groupId>
<artifactId>coder-common-thin-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- 阿里云OSS SDK -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<!-- Spring Boot Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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<String, Object> uploadFile(MultipartFile file, String fileName, String folderPath) {
try {
log.info("本地存储上传文件: fileName={}, folderPath={}", fileName, folderPath);
// 构建完整的本地存储路径
String fullPath = getBasePath() + "/" + folderPath;
// 使用现有的上传工具类
Map<String, Object> 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"
}
}

View File

@ -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<String, Object> 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<String, Object> 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();
}
}

View File

@ -0,0 +1,55 @@
package org.leocoder.thin.oss.service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
* 存储服务接口
* 提供统一的文件存储操作接口支持多种存储类型本地MinIOOSS等
*
* @author Leocoder
*/
public interface StorageService {
/**
* 上传文件
*
* @param file 文件对象
* @param fileName 文件名
* @param folderPath 文件夹路径
* @return 文件信息Map包含fileNamenewNamefileSizesuffixNamefilePathfileUploadPath等
*/
Map<String, Object> 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-LOCAL2-MINIO3-OSS
*/
String getStorageType();
}

View File

@ -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 存储类型localminiooss
* @return 存储服务实例
*/
public StorageService getStorageService(String storageType) {
if (storageType == null || storageType.trim().isEmpty()) {
throw new BusinessException(400, "存储类型不能为空");
}
try {
// 获取所有StorageService实现
Map<String, StorageService> 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;
}
}
}

View File

@ -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));
}
}

View File

@ -22,6 +22,7 @@
<module>coder-common-thin-repect</module>
<module>coder-common-thin-limit</module>
<module>coder-common-thin-oper-logs</module>
<module>coder-common-thin-oss</module>
</modules>
</project>

240
doc/oss/setup-env.sh Executable file
View File

@ -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"

View File

@ -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功能就可以正常使用了