feat: 新增OSS对象存储插件模块
- 实现阿里云OSS对象存储服务 - 支持MinIO对象存储服务 - 提供本地存储服务作为降级选择 - 实现存储服务工厂模式统一管理 - 新增OSS配置管理和工具类 - 完善插件化架构支持多种存储方式 - 添加详细的使用文档和配置说明
This commit is contained in:
parent
33b341e9e7
commit
0767c83995
51
coder-common-thin-plugins/coder-common-thin-oss/pom.xml
Normal file
51
coder-common-thin-plugins/coder-common-thin-oss/pom.xml
Normal 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>
|
||||||
@ -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 {
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<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-LOCAL,2-MINIO,3-OSS)
|
||||||
|
*/
|
||||||
|
String getStorageType();
|
||||||
|
}
|
||||||
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@
|
|||||||
<module>coder-common-thin-repect</module>
|
<module>coder-common-thin-repect</module>
|
||||||
<module>coder-common-thin-limit</module>
|
<module>coder-common-thin-limit</module>
|
||||||
<module>coder-common-thin-oper-logs</module>
|
<module>coder-common-thin-oper-logs</module>
|
||||||
|
<module>coder-common-thin-oss</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
240
doc/oss/setup-env.sh
Executable file
240
doc/oss/setup-env.sh
Executable 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"
|
||||||
271
doc/oss/环境变量设置指南.md
Normal file
271
doc/oss/环境变量设置指南.md
Normal 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功能就可以正常使用了!
|
||||||
Loading…
Reference in New Issue
Block a user