1. 数据库表设计
sql
CREATE TABLE `oss_config` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`config_name` varchar(100) NOT NULL COMMENT '配置名称',
`platform` varchar(20) NOT NULL COMMENT '存储平台: minio/qiniu/aliyun/tencent',
`endpoint` varchar(255) NOT NULL COMMENT '端点',
`access_key` varchar(255) NOT NULL COMMENT '访问密钥',
`secret_key` varchar(255) NOT NULL COMMENT '秘密密钥',
`bucket_name` varchar(100) NOT NULL COMMENT '存储桶名称',
`region` varchar(50) DEFAULT NULL COMMENT '区域',
`domain` varchar(255) DEFAULT NULL COMMENT '访问域名',
`is_default` tinyint(1) DEFAULT '0' COMMENT '是否默认配置',
`is_active` tinyint(1) DEFAULT '1' COMMENT '是否启用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OSS存储配置表';
2. Maven依赖
XML
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- OSS SDK -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.2</version>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.11.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.89</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
3. 实体类
java
@Entity
@Table(name = "oss_config")
public class OssConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "config_name", nullable = false, length = 100)
private String configName;
@Column(name = "platform", nullable = false, length = 20)
private String platform;
@Column(name = "endpoint", nullable = false)
private String endpoint;
@Column(name = "access_key", nullable = false)
private String accessKey;
@Column(name = "secret_key", nullable = false)
private String secretKey;
@Column(name = "bucket_name", nullable = false, length = 100)
private String bucketName;
@Column(name = "region", length = 50)
private String region;
@Column(name = "domain")
private String domain;
@Column(name = "is_default")
private Boolean isDefault = false;
@Column(name = "is_active")
private Boolean isActive = true;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
// getter/setter 省略
}
4. 数据访问层
java
@Repository
public interface OssConfigRepository extends JpaRepository<OssConfig, Long> {
/**
* 根据配置名称查找
*/
Optional<OssConfig> findByConfigName(String configName);
/**
* 查找默认配置
*/
Optional<OssConfig> findByIsDefaultTrueAndIsActiveTrue();
/**
* 查找所有启用的配置
*/
List<OssConfig> findByIsActiveTrue();
/**
* 根据平台查找配置
*/
List<OssConfig> findByPlatformAndIsActiveTrue(String platform);
}
5. OSS服务接口
java
public interface OssService {
/**
* 文件上传
*/
String uploadFile(InputStream inputStream, String fileName, long fileSize, String contentType) throws Exception;
/**
* 文件下载
*/
InputStream downloadFile(String fileKey) throws Exception;
/**
* 获取文件访问URL
*/
String getFileUrl(String fileKey, long expireSeconds);
/**
* 删除文件
*/
boolean deleteFile(String fileKey) throws Exception;
/**
* 判断文件是否存在
*/
boolean fileExists(String fileKey) throws Exception;
/**
* 获取存储平台类型
*/
String getPlatform();
}
6. OSS服务实现类
MinIO实现
java
@Service
@Slf4j
public class MinioOssService implements OssService {
private final MinioClient minioClient;
private final OssConfig ossConfig;
public MinioOssService(OssConfig ossConfig) {
this.ossConfig = ossConfig;
this.minioClient = MinioClient.builder()
.endpoint(ossConfig.getEndpoint())
.credentials(ossConfig.getAccessKey(), ossConfig.getSecretKey())
.build();
}
@Override
public String uploadFile(InputStream inputStream, String fileName, long fileSize, String contentType) throws Exception {
String fileKey = generateFileKey(fileName);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(ossConfig.getBucketName())
.object(fileKey)
.stream(inputStream, fileSize, -1)
.contentType(contentType)
.build()
);
return fileKey;
}
@Override
public InputStream downloadFile(String fileKey) throws Exception {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(ossConfig.getBucketName())
.object(fileKey)
.build()
);
}
@Override
public String getFileUrl(String fileKey, long expireSeconds) {
try {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(ossConfig.getBucketName())
.object(fileKey)
.expiry(Math.toIntExact(expireSeconds))
.build()
);
} catch (Exception e) {
log.error("获取文件URL失败", e);
return null;
}
}
@Override
public boolean deleteFile(String fileKey) throws Exception {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(ossConfig.getBucketName())
.object(fileKey)
.build()
);
return true;
}
@Override
public boolean fileExists(String fileKey) throws Exception {
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(ossConfig.getBucketName())
.object(fileKey)
.build()
);
return true;
} catch (ErrorResponseException e) {
return false;
}
}
@Override
public String getPlatform() {
return "minio";
}
private String generateFileKey(String fileName) {
String suffix = fileName.contains(".") ?
fileName.substring(fileName.lastIndexOf(".")) : "";
return UUID.randomUUID().toString().replace("-", "") + suffix;
}
}
七牛云实现
java
@Service
@Slf4j
public class QiniuOssService implements OssService {
private final Auth auth;
private final Configuration cfg;
private final BucketManager bucketManager;
private final UploadManager uploadManager;
private final OssConfig ossConfig;
public QiniuOssService(OssConfig ossConfig) {
this.ossConfig = ossConfig;
this.auth = Auth.create(ossConfig.getAccessKey(), ossConfig.getSecretKey());
this.cfg = new Configuration(Region.autoRegion());
this.bucketManager = new BucketManager(auth, cfg);
this.uploadManager = new UploadManager(cfg);
}
@Override
public String uploadFile(InputStream inputStream, String fileName, long fileSize, String contentType) throws Exception {
String fileKey = generateFileKey(fileName);
byte[] bytes = IOUtils.toByteArray(inputStream);
Response response = uploadManager.put(bytes, fileKey, getUploadToken(fileKey));
if (!response.isOK()) {
throw new RuntimeException("七牛云上传失败: " + response.bodyString());
}
return fileKey;
}
@Override
public InputStream downloadFile(String fileKey) throws Exception {
String downloadUrl = getFileUrl(fileKey, 3600);
return new URL(downloadUrl).openStream();
}
@Override
public String getFileUrl(String fileKey, long expireSeconds) {
String baseUrl = "http://" + ossConfig.getDomain() + "/" + fileKey;
return auth.privateDownloadUrl(baseUrl, expireSeconds);
}
@Override
public boolean deleteFile(String fileKey) throws Exception {
bucketManager.delete(ossConfig.getBucketName(), fileKey);
return true;
}
@Override
public boolean fileExists(String fileKey) throws Exception {
try {
bucketManager.stat(ossConfig.getBucketName(), fileKey);
return true;
} catch (QiniuException e) {
if (e.code() == 612) {
return false;
}
throw e;
}
}
@Override
public String getPlatform() {
return "qiniu";
}
private String getUploadToken(String fileKey) {
return auth.uploadToken(ossConfig.getBucketName(), fileKey, 3600,
new StringMap().put("insertOnly", 1));
}
private String generateFileKey(String fileName) {
String suffix = fileName.contains(".") ?
fileName.substring(fileName.lastIndexOf(".")) : "";
return UUID.randomUUID().toString().replace("-", "") + suffix;
}
}
阿里云OSS实现
java
@Service
@Slf4j
public class AliyunOssService implements OssService {
private final OSS ossClient;
private final OssConfig ossConfig;
public AliyunOssService(OssConfig ossConfig) {
this.ossConfig = ossConfig;
this.ossClient = new OSSClientBuilder().build(
ossConfig.getEndpoint(),
ossConfig.getAccessKey(),
ossConfig.getSecretKey()
);
}
@Override
public String uploadFile(InputStream inputStream, String fileName, long fileSize, String contentType) throws Exception {
String fileKey = generateFileKey(fileName);
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossConfig.getBucketName(), fileKey, inputStream);
if (StringUtils.isNotBlank(contentType)) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
putObjectRequest.setMetadata(metadata);
}
ossClient.putObject(putObjectRequest);
return fileKey;
}
@Override
public InputStream downloadFile(String fileKey) throws Exception {
OSSObject ossObject = ossClient.getObject(ossConfig.getBucketName(), fileKey);
return ossObject.getObjectContent();
}
@Override
public String getFileUrl(String fileKey, long expireSeconds) {
Date expiration = new Date(System.currentTimeMillis() + expireSeconds * 1000);
URL url = ossClient.generatePresignedUrl(ossConfig.getBucketName(), fileKey, expiration);
return url.toString();
}
@Override
public boolean deleteFile(String fileKey) throws Exception {
ossClient.deleteObject(ossConfig.getBucketName(), fileKey);
return true;
}
@Override
public boolean fileExists(String fileKey) throws Exception {
return ossClient.doesObjectExist(ossConfig.getBucketName(), fileKey);
}
@Override
public String getPlatform() {
return "aliyun";
}
private String generateFileKey(String fileName) {
String suffix = fileName.contains(".") ?
fileName.substring(fileName.lastIndexOf(".")) : "";
return UUID.randomUUID().toString().replace("-", "") + suffix;
}
@PreDestroy
public void destroy() {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
腾讯云COS实现
java
@Service
@Slf4j
public class TencentOssService implements OssService {
private final COSClient cosClient;
private final OssConfig ossConfig;
public TencentOssService(OssConfig ossConfig) {
this.ossConfig = ossConfig;
ClientConfig clientConfig = new ClientConfig(new Region(ossConfig.getRegion()));
COSCredentials cred = new BasicCOSCredentials(
ossConfig.getAccessKey(), ossConfig.getSecretKey());
this.cosClient = new COSClient(cred, clientConfig);
}
@Override
public String uploadFile(InputStream inputStream, String fileName, long fileSize, String contentType) throws Exception {
String fileKey = generateFileKey(fileName);
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(fileSize);
if (StringUtils.isNotBlank(contentType)) {
metadata.setContentType(contentType);
}
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossConfig.getBucketName(), fileKey, inputStream, metadata);
cosClient.putObject(putObjectRequest);
return fileKey;
}
@Override
public InputStream downloadFile(String fileKey) throws Exception {
GetObjectRequest getObjectRequest = new GetObjectRequest(
ossConfig.getBucketName(), fileKey);
COSObject cosObject = cosClient.getObject(getObjectRequest);
return cosObject.getObjectContent();
}
@Override
public String getFileUrl(String fileKey, long expireSeconds) {
Date expirationDate = new Date(System.currentTimeMillis() + expireSeconds * 1000);
URL url = cosClient.generatePresignedUrl(
ossConfig.getBucketName(), fileKey, expirationDate);
return url.toString();
}
@Override
public boolean deleteFile(String fileKey) throws Exception {
cosClient.deleteObject(ossConfig.getBucketName(), fileKey);
return true;
}
@Override
public boolean fileExists(String fileKey) throws Exception {
try {
cosClient.getObjectMetadata(ossConfig.getBucketName(), fileKey);
return true;
} catch (CosServiceException e) {
if (e.getStatusCode() == 404) {
return false;
}
throw e;
}
}
@Override
public String getPlatform() {
return "tencent";
}
private String generateFileKey(String fileName) {
String suffix = fileName.contains(".") ?
fileName.substring(fileName.lastIndexOf(".")) : "";
return UUID.randomUUID().toString().replace("-", "") + suffix;
}
@PreDestroy
public void destroy() {
if (cosClient != null) {
cosClient.shutdown();
}
}
}
7. OSS服务工厂
java
@Component
public class OssServiceFactory {
private final Map<String, Function<OssConfig, OssService>> serviceCreators;
private final OssConfigRepository ossConfigRepository;
public OssServiceFactory(OssConfigRepository ossConfigRepository) {
this.ossConfigRepository = ossConfigRepository;
this.serviceCreators = new HashMap<>();
this.serviceCreators.put("minio", MinioOssService::new);
this.serviceCreators.put("qiniu", QiniuOssService::new);
this.serviceCreators.put("aliyun", AliyunOssService::new);
this.serviceCreators.put("tencent", TencentOssService::new);
}
/**
* 根据配置名称获取OSS服务
*/
public OssService getOssService(String configName) {
OssConfig config = ossConfigRepository.findByConfigName(configName)
.orElseThrow(() -> new RuntimeException("OSS配置不存在: " + configName));
if (!config.getIsActive()) {
throw new RuntimeException("OSS配置未启用: " + configName);
}
Function<OssConfig, OssService> creator = serviceCreators.get(config.getPlatform());
if (creator == null) {
throw new RuntimeException("不支持的OSS平台: " + config.getPlatform());
}
return creator.apply(config);
}
/**
* 获取默认的OSS服务
*/
public OssService getDefaultOssService() {
OssConfig config = ossConfigRepository.findByIsDefaultTrueAndIsActiveTrue()
.orElseThrow(() -> new RuntimeException("未找到默认的OSS配置"));
return getOssService(config.getConfigName());
}
/**
* 获取所有可用的OSS服务
*/
public Map<String, OssService> getAllOssServices() {
List<OssConfig> configs = ossConfigRepository.findByIsActiveTrue();
Map<String, OssService> services = new HashMap<>();
for (OssConfig config : configs) {
try {
OssService service = getOssService(config.getConfigName());
services.put(config.getConfigName(), service);
} catch (Exception e) {
log.warn("创建OSS服务失败: {}", config.getConfigName(), e);
}
}
return services;
}
}
8. 统一文件服务
java
@Service
@Slf4j
public class FileStorageService {
private final OssServiceFactory ossServiceFactory;
public FileStorageService(OssServiceFactory ossServiceFactory) {
this.ossServiceFactory = ossServiceFactory;
}
/**
* 上传文件(使用默认配置)
*/
public FileUploadResult uploadFile(MultipartFile file) throws Exception {
return uploadFile(file, null);
}
/**
* 上传文件(指定配置)
*/
public FileUploadResult uploadFile(MultipartFile file, String configName) throws Exception {
OssService ossService = StringUtils.isBlank(configName) ?
ossServiceFactory.getDefaultOssService() :
ossServiceFactory.getOssService(configName);
String originalFileName = file.getOriginalFilename();
String contentType = file.getContentType();
long fileSize = file.getSize();
String fileKey = ossService.uploadFile(
file.getInputStream(), originalFileName, fileSize, contentType);
String fileUrl = ossService.getFileUrl(fileKey, 3600 * 24 * 7); // 7天有效期
return FileUploadResult.builder()
.fileKey(fileKey)
.fileName(originalFileName)
.fileSize(fileSize)
.contentType(contentType)
.fileUrl(fileUrl)
.platform(ossService.getPlatform())
.uploadTime(LocalDateTime.now())
.build();
}
/**
* 下载文件
*/
public FileDownloadResult downloadFile(String fileKey, String configName) throws Exception {
OssService ossService = StringUtils.isBlank(configName) ?
ossServiceFactory.getDefaultOssService() :
ossServiceFactory.getOssService(configName);
InputStream inputStream = ossService.downloadFile(fileKey);
return FileDownloadResult.builder()
.fileKey(fileKey)
.inputStream(inputStream)
.platform(ossService.getPlatform())
.build();
}
/**
* 删除文件
*/
public boolean deleteFile(String fileKey, String configName) throws Exception {
OssService ossService = StringUtils.isBlank(configName) ?
ossServiceFactory.getDefaultOssService() :
ossServiceFactory.getOssService(configName);
return ossService.deleteFile(fileKey);
}
/**
* 获取文件URL
*/
public String getFileUrl(String fileKey, String configName) {
try {
OssService ossService = StringUtils.isBlank(configName) ?
ossServiceFactory.getDefaultOssService() :
ossServiceFactory.getOssService(configName);
return ossService.getFileUrl(fileKey, 3600 * 24 * 7); // 7天有效期
} catch (Exception e) {
log.error("获取文件URL失败", e);
return null;
}
}
}
9. 控制器
java
@RestController
@RequestMapping("/api/file")
@Slf4j
public class FileController {
private final FileStorageService fileStorageService;
public FileController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
/**
* 文件上传
*/
@PostMapping("/upload")
public ResponseEntity<FileUploadResult> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "config", required = false) String configName) {
try {
FileUploadResult result = fileStorageService.uploadFile(file, configName);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("文件上传失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 文件下载
*/
@GetMapping("/download/{fileKey}")
public void downloadFile(
@PathVariable String fileKey,
@RequestParam(value = "config", required = false) String configName,
HttpServletResponse response) {
try {
FileDownloadResult downloadResult = fileStorageService.downloadFile(fileKey, configName);
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + fileKey + "\"");
IOUtils.copy(downloadResult.getInputStream(), response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
log.error("文件下载失败", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
/**
* 删除文件
*/
@DeleteMapping("/{fileKey}")
public ResponseEntity<Void> deleteFile(
@PathVariable String fileKey,
@RequestParam(value = "config", required = false) String configName) {
try {
boolean success = fileStorageService.deleteFile(fileKey, configName);
return success ? ResponseEntity.ok().build() :
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} catch (Exception e) {
log.error("文件删除失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
/**
* 获取文件URL
*/
@GetMapping("/url/{fileKey}")
public ResponseEntity<Map<String, String>> getFileUrl(
@PathVariable String fileKey,
@RequestParam(value = "config", required = false) String configName) {
try {
String fileUrl = fileStorageService.getFileUrl(fileKey, configName);
Map<String, String> result = Collections.singletonMap("url", fileUrl);
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("获取文件URL失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
10. 配置类
java
@Configuration
public class OssAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public OssServiceFactory ossServiceFactory(OssConfigRepository ossConfigRepository) {
return new OssServiceFactory(ossConfigRepository);
}
@Bean
@ConditionalOnMissingBean
public FileStorageService fileStorageService(OssServiceFactory ossServiceFactory) {
return new FileStorageService(ossServiceFactory);
}
}
11. 使用示例
数据库配置示例数据:
sql
INSERT INTO oss_config (config_name, platform, endpoint, access_key, secret_key, bucket_name, region, domain, is_default, is_active) VALUES
('minio-dev', 'minio', 'http://localhost:9000', 'minioadmin', 'minioadmin', 'my-bucket', NULL, NULL, 1, 1),
('qiniu-prod', 'qiniu', 's3-cn-east-1.qiniucs.com', 'your-access-key', 'your-secret-key', 'my-bucket', NULL, 'cdn.example.com', 0, 1),
('aliyun-prod', 'aliyun', 'oss-cn-hangzhou.aliyuncs.com', 'your-access-key', 'your-secret-key', 'my-bucket', 'cn-hangzhou', NULL, 0, 1),
('tencent-prod', 'tencent', 'cos.ap-shanghai.myqcloud.com', 'your-access-key', 'your-secret-key', 'my-bucket', 'ap-shanghai', NULL, 0, 1);
使用方式:
java
@RestController
public class TestController {
@Autowired
private FileStorageService fileStorageService;
@PostMapping("/test-upload")
public String testUpload(@RequestParam("file") MultipartFile file) {
try {
// 使用默认配置上传
FileUploadResult result1 = fileStorageService.uploadFile(file);
// 使用指定配置上传
FileUploadResult result2 = fileStorageService.uploadFile(file, "aliyun-prod");
return "上传成功";
} catch (Exception e) {
return "上传失败: " + e.getMessage();
}
}
}
这个完整的解决方案提供了:
-
多存储平台支持:MinIO、七牛云、阿里云OSS、腾讯云COS
-
数据库配置:所有配置存储在数据库中,支持动态修改
-
统一接口:所有存储平台使用相同的接口
-
工厂模式:根据配置动态创建对应的OSS服务
-
完整的CRUD操作:上传、下载、删除、获取URL等
-
错误处理:完善的异常处理机制
-
扩展性:易于添加新的存储平台支持