添加OSS文件上传系统的详细设计文档,包含: - 系统架构设计和技术选型 - 核心功能模块说明 - 配置参数和使用示例 - 最佳实践和注意事项 为OSS插件模块提供完整的技术文档支持。
1855 lines
56 KiB
Markdown
1855 lines
56 KiB
Markdown
# 阿里云OSS文件上传系统设计方案
|
||
|
||
## 1. 项目概述
|
||
|
||
### 1.1 背景
|
||
基于现有的本地文件上传系统,需要扩展支持阿里云OSS对象存储服务,实现文件的云端存储和管理。现有系统已经具备良好的架构设计,支持多种存储服务类型,需要在不破坏现有功能的基础上,集成阿里云OSS功能。
|
||
|
||
### 1.2 目标
|
||
- 实现阿里云OSS文件上传功能
|
||
- 保持与现有本地存储系统的兼容性
|
||
- 实现文件删除时的OSS文件同步删除
|
||
- 提供统一的文件管理接口
|
||
- 支持图片和文档文件上传到OSS
|
||
- 确保系统的高可用性和安全性
|
||
|
||
### 1.3 技术栈
|
||
- **后端框架**: Spring Boot 3.5.0
|
||
- **ORM框架**: MyBatis Plus 3.5.12
|
||
- **OSS SDK**: 阿里云Java SDK
|
||
- **认证框架**: Sa-Token 1.43.0
|
||
- **数据库**: MySQL 9.3.0
|
||
- **缓存**: Redis
|
||
- **工具库**: Hutool 5.8.38
|
||
|
||
## 2. 现有系统分析
|
||
|
||
### 2.1 前端文件上传实现
|
||
|
||
#### 2.1.1 核心组件
|
||
- **文件管理页面**: `/src/views/system/file/index.vue`
|
||
- **图片管理页面**: `/src/views/system/picture/index.vue`
|
||
- **上传组件**: Naive UI的`NUpload`组件
|
||
|
||
#### 2.1.2 API接口
|
||
```typescript
|
||
// 文件上传API
|
||
export function uploadFile(file: File, folderName: string, fileSize = 2, fileParam = '-1') {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
return request.Post<FileUploadResult>(`/coder/file/uploadFile/${fileSize}/${folderName}/${fileParam}`, formData)
|
||
}
|
||
|
||
// 图片上传API
|
||
export function uploadPicture(file: File, pictureType = '9', fileSize = 2) {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
return request.Post<PictureUploadResult>(`/coder/file/uploadFile/${fileSize}/pictures/${pictureType}`, formData)
|
||
}
|
||
```
|
||
|
||
#### 2.1.3 文件验证机制
|
||
- **文件大小限制**: 2MB
|
||
- **支持格式**: 图片格式(jpg、png、gif等)和文档格式(doc、pdf、excel等)
|
||
- **前端验证**: 文件类型、大小验证
|
||
- **上传进度**: 实时显示上传进度
|
||
|
||
### 2.2 后端文件上传实现
|
||
|
||
#### 2.2.1 核心Controller
|
||
- **FileController**: 文件上传核心控制器
|
||
- **SysFileController**: 文件管理控制器
|
||
- **SysPictureController**: 图片管理控制器
|
||
|
||
#### 2.2.2 数据模型
|
||
```sql
|
||
-- 文件表
|
||
CREATE TABLE `sys_file` (
|
||
`file_id` bigint NOT NULL AUTO_INCREMENT,
|
||
`file_name` text NOT NULL COMMENT '文件原始名称',
|
||
`new_name` text NOT NULL COMMENT '文件新名称',
|
||
`file_type` char(1) NOT NULL DEFAULT '1' COMMENT '文件类型[1-图片 2-文档 3-音频 4-视频 5-压缩包 6-应用程序 7-其他]',
|
||
`file_size` varchar(32) DEFAULT NULL COMMENT '文件大小',
|
||
`file_suffix` varchar(32) DEFAULT '' COMMENT '文件后缀',
|
||
`file_upload` text COMMENT '文件上传路径',
|
||
`file_path` text COMMENT '文件回显路径',
|
||
`file_service` char(1) NOT NULL DEFAULT '1' COMMENT '文件服务类型[1-LOCAL,2-MINIO,3-OSS]',
|
||
`create_time` datetime DEFAULT NULL,
|
||
`create_by` varchar(32) DEFAULT '',
|
||
`update_time` datetime DEFAULT NULL,
|
||
`update_by` varchar(32) DEFAULT '',
|
||
PRIMARY KEY (`file_id`)
|
||
);
|
||
|
||
-- 图片表
|
||
CREATE TABLE `sys_picture` (
|
||
`picture_id` bigint NOT NULL AUTO_INCREMENT,
|
||
`picture_name` text NOT NULL COMMENT '图片原始名称',
|
||
`new_name` text NOT NULL COMMENT '图片新名称',
|
||
`picture_type` char(1) NOT NULL DEFAULT '1' COMMENT '图片类型',
|
||
`picture_size` varchar(32) DEFAULT NULL COMMENT '图片大小',
|
||
`picture_suffix` varchar(32) DEFAULT '' COMMENT '图片后缀',
|
||
`picture_upload` text COMMENT '图片上传路径',
|
||
`picture_path` text COMMENT '图片回显路径',
|
||
`picture_service` char(1) NOT NULL DEFAULT '1' COMMENT '图片服务类型[1-LOCAL,2-MINIO,3-OSS]',
|
||
`create_time` datetime DEFAULT NULL,
|
||
`create_by` varchar(32) DEFAULT '',
|
||
`update_time` datetime DEFAULT NULL,
|
||
`update_by` varchar(32) DEFAULT '',
|
||
PRIMARY KEY (`picture_id`)
|
||
);
|
||
```
|
||
|
||
#### 2.2.3 文件删除机制
|
||
- **SysFileController**: 删除时同时删除数据库记录和物理文件
|
||
- **SysPictureController**: 仅删除数据库记录(存在问题)
|
||
- **FileUtil**: 提供文件删除工具方法
|
||
|
||
### 2.3 存在的问题
|
||
1. **图片删除不一致**: SysPictureController删除时未删除物理文件
|
||
2. **存储服务扩展**: 目前只实现了本地存储,OSS功能未实现
|
||
3. **配置管理**: 缺少OSS相关配置管理
|
||
|
||
## 3. 阿里云OSS架构设计
|
||
|
||
### 3.1 整体架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 前端应用层 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ 文件管理页面 │ │ 图片管理页面 │ │ 上传组件 │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 后端应用层 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ FileController │ │ SysFileController│ │SysPictureController││
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 业务服务层 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ FileService │ │ StorageService │ │ OssService │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 存储适配层 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ LocalStorage │ │ MinioStorage │ │ OssStorage │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 存储基础设施 │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ 本地磁盘 │ │ MinIO服务 │ │ 阿里云OSS │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 核心组件设计
|
||
|
||
#### 3.2.1 存储服务接口
|
||
```java
|
||
public interface StorageService {
|
||
/**
|
||
* 上传文件
|
||
* @param file 文件对象
|
||
* @param fileName 文件名
|
||
* @param folderPath 文件夹路径
|
||
* @return 文件信息
|
||
*/
|
||
FileUploadResult 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);
|
||
}
|
||
```
|
||
|
||
#### 3.2.2 OSS存储实现
|
||
```java
|
||
@Service
|
||
@ConditionalOnProperty(name = "coder.storage.type", havingValue = "oss")
|
||
public class OssStorageService implements StorageService {
|
||
|
||
private final OSS ossClient;
|
||
private final OssConfig ossConfig;
|
||
|
||
@Override
|
||
public FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath) {
|
||
// OSS文件上传实现
|
||
}
|
||
|
||
@Override
|
||
public boolean deleteFile(String filePath) {
|
||
// OSS文件删除实现
|
||
}
|
||
|
||
@Override
|
||
public String getFileUrl(String filePath) {
|
||
// 生成OSS访问URL
|
||
}
|
||
|
||
@Override
|
||
public boolean fileExists(String filePath) {
|
||
// 检查OSS文件是否存在
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.2.3 存储工厂模式
|
||
```java
|
||
@Component
|
||
public class StorageServiceFactory {
|
||
|
||
private final Map<String, StorageService> storageServiceMap;
|
||
|
||
public StorageService getStorageService(String storageType) {
|
||
return storageServiceMap.get(storageType);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 插件化设计
|
||
|
||
#### 3.3.1 OSS插件模块
|
||
```
|
||
coder-common-thin-plugins/
|
||
└── coder-common-thin-oss/
|
||
├── pom.xml
|
||
├── src/main/java/org/leocoder/thin/oss/
|
||
│ ├── annotation/
|
||
│ │ └── EnableCoderOss.java
|
||
│ ├── config/
|
||
│ │ ├── OssConfig.java
|
||
│ │ └── OssAutoConfiguration.java
|
||
│ ├── service/
|
||
│ │ ├── OssService.java
|
||
│ │ └── OssServiceImpl.java
|
||
│ └── util/
|
||
│ └── OssUtil.java
|
||
└── src/main/resources/
|
||
└── META-INF/
|
||
└── spring.factories
|
||
```
|
||
|
||
#### 3.3.2 启用注解
|
||
```java
|
||
@Target(ElementType.TYPE)
|
||
@Retention(RetentionPolicy.RUNTIME)
|
||
@Documented
|
||
@Import(OssAutoConfiguration.class)
|
||
public @interface EnableCoderOss {
|
||
// OSS插件启用注解
|
||
}
|
||
```
|
||
|
||
## 4. 技术实现方案
|
||
|
||
### 4.1 依赖管理
|
||
|
||
#### 4.1.1 Maven依赖
|
||
```xml
|
||
<!-- OSS插件模块 -->
|
||
<dependency>
|
||
<groupId>org.leocoder.thin</groupId>
|
||
<artifactId>coder-common-thin-oss</artifactId>
|
||
<version>${project.version}</version>
|
||
</dependency>
|
||
|
||
<!-- 阿里云OSS SDK -->
|
||
<dependency>
|
||
<groupId>com.aliyun.oss</groupId>
|
||
<artifactId>aliyun-sdk-oss</artifactId>
|
||
<version>3.17.4</version>
|
||
</dependency>
|
||
```
|
||
|
||
#### 4.1.2 主启动类配置
|
||
```java
|
||
@EnableCoderOss
|
||
@SpringBootApplication
|
||
public class CoderCommonThinApplication {
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(CoderCommonThinApplication.class, args);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 配置管理
|
||
|
||
#### 4.2.1 OSS配置类
|
||
```java
|
||
@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;
|
||
|
||
/**
|
||
* 是否启用HTTPS
|
||
*/
|
||
private Boolean https = true;
|
||
|
||
/**
|
||
* 连接超时时间
|
||
*/
|
||
private Long connectTimeout = 10000L;
|
||
|
||
/**
|
||
* 读取超时时间
|
||
*/
|
||
private Long readTimeout = 10000L;
|
||
}
|
||
```
|
||
|
||
#### 4.2.2 应用配置
|
||
```yaml
|
||
# application-dev.yml
|
||
coder:
|
||
# 存储服务类型:local、minio、oss
|
||
storage:
|
||
type: oss
|
||
|
||
# OSS配置
|
||
oss:
|
||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||
access-key-id: ${OSS_ACCESS_KEY_ID:}
|
||
access-key-secret: ${OSS_ACCESS_KEY_SECRET:}
|
||
bucket-name: coder-file-storage
|
||
domain: https://coder-file-storage.oss-cn-hangzhou.aliyuncs.com
|
||
path-prefix: coder-files
|
||
https: true
|
||
connect-timeout: 10000
|
||
read-timeout: 10000
|
||
```
|
||
|
||
### 4.3 核心服务实现
|
||
|
||
#### 4.3.1 OSS客户端配置
|
||
```java
|
||
@Configuration
|
||
@EnableConfigurationProperties(OssConfig.class)
|
||
@ConditionalOnProperty(name = "coder.storage.type", havingValue = "oss")
|
||
public class OssAutoConfiguration {
|
||
|
||
@Bean
|
||
@ConditionalOnMissingBean
|
||
public OSS ossClient(OssConfig ossConfig) {
|
||
return new OSSClientBuilder().build(
|
||
ossConfig.getEndpoint(),
|
||
ossConfig.getAccessKeyId(),
|
||
ossConfig.getAccessKeySecret()
|
||
);
|
||
}
|
||
|
||
@Bean
|
||
@ConditionalOnMissingBean
|
||
public OssService ossService(OSS ossClient, OssConfig ossConfig) {
|
||
return new OssServiceImpl(ossClient, ossConfig);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.3.2 OSS服务实现
|
||
```java
|
||
@Service
|
||
@Slf4j
|
||
@RequiredArgsConstructor
|
||
public class OssServiceImpl implements OssService {
|
||
|
||
private final OSS ossClient;
|
||
private final OssConfig ossConfig;
|
||
|
||
@Override
|
||
public FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath) {
|
||
try {
|
||
// 构建对象键
|
||
String objectKey = buildObjectKey(folderPath, fileName);
|
||
|
||
// 上传文件
|
||
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||
ossConfig.getBucketName(),
|
||
objectKey,
|
||
file.getInputStream()
|
||
);
|
||
|
||
// 设置元数据
|
||
ObjectMetadata metadata = new ObjectMetadata();
|
||
metadata.setContentLength(file.getSize());
|
||
metadata.setContentType(file.getContentType());
|
||
metadata.setContentDisposition("inline");
|
||
putObjectRequest.setMetadata(metadata);
|
||
|
||
// 执行上传
|
||
PutObjectResult result = ossClient.putObject(putObjectRequest);
|
||
|
||
// 构建返回结果
|
||
return FileUploadResult.builder()
|
||
.fileName(file.getOriginalFilename())
|
||
.newName(fileName)
|
||
.fileSize(FileTypeUtil.formatFileSize(file.getSize()))
|
||
.suffixName(FileTypeUtil.getFileExtension(file.getOriginalFilename()))
|
||
.filePath(objectKey)
|
||
.fileUploadPath(getFileUrl(objectKey))
|
||
.build();
|
||
|
||
} catch (Exception e) {
|
||
log.error("OSS文件上传失败", e);
|
||
throw new BusinessException("文件上传失败");
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean deleteFile(String objectKey) {
|
||
try {
|
||
ossClient.deleteObject(ossConfig.getBucketName(), objectKey);
|
||
return true;
|
||
} catch (Exception e) {
|
||
log.error("OSS文件删除失败: {}", objectKey, e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public String getFileUrl(String objectKey) {
|
||
if (StringUtils.isBlank(objectKey)) {
|
||
return "";
|
||
}
|
||
|
||
if (StringUtils.isNotBlank(ossConfig.getDomain())) {
|
||
return ossConfig.getDomain() + "/" + objectKey;
|
||
}
|
||
|
||
String protocol = ossConfig.getHttps() ? "https" : "http";
|
||
return protocol + "://" + ossConfig.getBucketName() + "." + ossConfig.getEndpoint() + "/" + objectKey;
|
||
}
|
||
|
||
@Override
|
||
public boolean fileExists(String objectKey) {
|
||
try {
|
||
return ossClient.doesObjectExist(ossConfig.getBucketName(), objectKey);
|
||
} catch (Exception e) {
|
||
log.error("检查OSS文件是否存在失败: {}", objectKey, e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private String buildObjectKey(String folderPath, String fileName) {
|
||
StringBuilder keyBuilder = new StringBuilder();
|
||
|
||
if (StringUtils.isNotBlank(ossConfig.getPathPrefix())) {
|
||
keyBuilder.append(ossConfig.getPathPrefix()).append("/");
|
||
}
|
||
|
||
if (StringUtils.isNotBlank(folderPath)) {
|
||
keyBuilder.append(folderPath);
|
||
if (!folderPath.endsWith("/")) {
|
||
keyBuilder.append("/");
|
||
}
|
||
}
|
||
|
||
keyBuilder.append(fileName);
|
||
|
||
return keyBuilder.toString();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.4 文件上传控制器改造
|
||
|
||
#### 4.4.1 存储服务工厂
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
public class StorageServiceFactory {
|
||
|
||
private final ApplicationContext applicationContext;
|
||
|
||
@Value("${coder.storage.type:local}")
|
||
private String defaultStorageType;
|
||
|
||
public StorageService getStorageService() {
|
||
return getStorageService(defaultStorageType);
|
||
}
|
||
|
||
public StorageService getStorageService(String storageType) {
|
||
switch (storageType.toLowerCase()) {
|
||
case "local":
|
||
return applicationContext.getBean(LocalStorageService.class);
|
||
case "minio":
|
||
return applicationContext.getBean(MinioStorageService.class);
|
||
case "oss":
|
||
return applicationContext.getBean(OssStorageService.class);
|
||
default:
|
||
throw new BusinessException("不支持的存储类型: " + storageType);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.4.2 文件上传控制器改造
|
||
```java
|
||
@RestController
|
||
@RequestMapping("/coder")
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class FileController {
|
||
|
||
private final StorageServiceFactory storageServiceFactory;
|
||
private final SysFileService sysFileService;
|
||
private final SysPictureService sysPictureService;
|
||
|
||
@PostMapping("/file/uploadFile/{fileSize}/{folderName}/{fileParam}")
|
||
public Map<String, Object> uploadSingleFile(
|
||
@RequestParam("file") MultipartFile file,
|
||
@PathVariable("fileSize") Integer fileSize,
|
||
@PathVariable("folderName") String folderName,
|
||
@PathVariable("fileParam") String fileParam) {
|
||
|
||
// 文件预检查
|
||
validateUploadFile(file, fileSize, folderName);
|
||
|
||
// 获取存储服务
|
||
StorageService storageService = storageServiceFactory.getStorageService();
|
||
|
||
// 生成文件名
|
||
String fileName = generateFileName(file.getOriginalFilename());
|
||
|
||
// 构建文件夹路径
|
||
String folderPath = buildFolderPath(folderName, CoderLoginUtil.getLoginName());
|
||
|
||
// 上传文件
|
||
FileUploadResult uploadResult = storageService.uploadFile(file, fileName, folderPath);
|
||
|
||
// 转换为Map格式(保持兼容性)
|
||
Map<String, Object> fileMap = convertToMap(uploadResult);
|
||
|
||
// 保存文件信息到数据库
|
||
saveUploadFilesInformation(fileMap, true);
|
||
|
||
// 如果是图片,同时保存到图库表
|
||
if (CoderConstants.PICTURES.equals(folderName)) {
|
||
saveUploadPicturesInformation(fileMap, fileParam, true);
|
||
}
|
||
|
||
return fileMap;
|
||
}
|
||
|
||
private String generateFileName(String originalFilename) {
|
||
String extension = FileTypeUtil.getFileExtension(originalFilename);
|
||
String timeStamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
|
||
String uuid = IdUtil.fastSimpleUUID().substring(0, 6);
|
||
return timeStamp + "-" + uuid + "." + extension;
|
||
}
|
||
|
||
private String buildFolderPath(String folderName, String username) {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
return String.format("%s/%s/%d/%02d/%02d",
|
||
folderName, username, now.getYear(), now.getMonthValue(), now.getDayOfMonth());
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.5 文件删除机制改造
|
||
|
||
#### 4.5.1 文件删除服务
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class FileDeleteService {
|
||
|
||
private final StorageServiceFactory storageServiceFactory;
|
||
|
||
/**
|
||
* 删除文件(支持多种存储类型)
|
||
*/
|
||
public boolean deleteFile(String filePath, String fileService) {
|
||
try {
|
||
// 根据文件服务类型选择删除策略
|
||
switch (fileService) {
|
||
case "1": // LOCAL
|
||
return FileUtil.deleteFile(filePath);
|
||
case "2": // MINIO
|
||
StorageService minioService = storageServiceFactory.getStorageService("minio");
|
||
return minioService.deleteFile(filePath);
|
||
case "3": // OSS
|
||
StorageService ossService = storageServiceFactory.getStorageService("oss");
|
||
return ossService.deleteFile(filePath);
|
||
default:
|
||
log.warn("未知的文件服务类型: {}", fileService);
|
||
return false;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("删除文件失败: filePath={}, fileService={}", filePath, fileService, e);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.5.2 文件管理控制器改造
|
||
```java
|
||
@RestController
|
||
@RequestMapping("/coder")
|
||
@RequiredArgsConstructor
|
||
public class SysFileController {
|
||
|
||
private final SysFileService sysFileService;
|
||
private final FileDeleteService fileDeleteService;
|
||
|
||
@PostMapping("/sysFile/deleteById/{id}")
|
||
@SaCheckPermission("system:file:delete")
|
||
@OperLog(value = "删除文件资源", operType = OperType.DELETE)
|
||
public void delete(@PathVariable("id") Long id) {
|
||
SysFile sysFile = sysFileService.getById(id);
|
||
if (sysFile != null) {
|
||
// 删除物理文件
|
||
if (StringUtils.isNotBlank(sysFile.getFileUpload())) {
|
||
fileDeleteService.deleteFile(sysFile.getFileUpload(), sysFile.getFileService());
|
||
}
|
||
|
||
// 删除数据库记录
|
||
YUtil.isTrue(!sysFileService.removeById(id), "删除失败,请稍后重试");
|
||
}
|
||
}
|
||
|
||
@PostMapping("/sysFile/batchDelete")
|
||
@SaCheckPermission("system:file:delete")
|
||
@Transactional(rollbackFor = Exception.class)
|
||
@OperLog(value = "批量删除文件资源", operType = OperType.DELETE)
|
||
public void batchDelete(@RequestBody List<Long> ids) {
|
||
List<SysFile> sysFileList = sysFileService.listByIds(ids);
|
||
|
||
// 批量删除物理文件
|
||
for (SysFile sysFile : sysFileList) {
|
||
if (StringUtils.isNotBlank(sysFile.getFileUpload())) {
|
||
fileDeleteService.deleteFile(sysFile.getFileUpload(), sysFile.getFileService());
|
||
}
|
||
}
|
||
|
||
// 批量删除数据库记录
|
||
YUtil.isTrue(!sysFileService.removeBatchByIds(ids), "删除失败,请稍后重试");
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 4.5.3 图片管理控制器改造
|
||
```java
|
||
@RestController
|
||
@RequestMapping("/coder")
|
||
@RequiredArgsConstructor
|
||
public class SysPictureController {
|
||
|
||
private final SysPictureService sysPictureService;
|
||
private final FileDeleteService fileDeleteService;
|
||
|
||
@PostMapping("/sysPicture/deleteById/{id}")
|
||
@SaCheckPermission("system:sysPicture:delete")
|
||
@OperLog(value = "删除图库", operType = OperType.DELETE)
|
||
public void delete(@PathVariable("id") Long id) {
|
||
SysPicture sysPicture = sysPictureService.getById(id);
|
||
if (sysPicture != null) {
|
||
// 删除物理文件
|
||
if (StringUtils.isNotBlank(sysPicture.getPictureUpload())) {
|
||
fileDeleteService.deleteFile(sysPicture.getPictureUpload(), sysPicture.getPictureService());
|
||
}
|
||
|
||
// 删除数据库记录
|
||
YUtil.isTrue(!sysPictureService.removeById(id), "删除失败,请稍后重试");
|
||
}
|
||
}
|
||
|
||
@PostMapping("/sysPicture/batchDelete")
|
||
@SaCheckPermission("system:sysPicture:delete")
|
||
@Transactional(rollbackFor = Exception.class)
|
||
@OperLog(value = "批量删除图库", operType = OperType.DELETE)
|
||
public void batchDelete(@RequestBody List<Long> ids) {
|
||
List<SysPicture> sysPictureList = sysPictureService.listByIds(ids);
|
||
|
||
// 批量删除物理文件
|
||
for (SysPicture sysPicture : sysPictureList) {
|
||
if (StringUtils.isNotBlank(sysPicture.getPictureUpload())) {
|
||
fileDeleteService.deleteFile(sysPicture.getPictureUpload(), sysPicture.getPictureService());
|
||
}
|
||
}
|
||
|
||
// 批量删除数据库记录
|
||
YUtil.isTrue(!sysPictureService.removeBatchByIds(ids), "删除失败,请稍后重试");
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 数据库设计
|
||
|
||
### 5.1 现有表结构分析
|
||
现有的`sys_file`和`sys_picture`表已经包含了`file_service`和`picture_service`字段,用于标识存储服务类型:
|
||
- `1` - LOCAL(本地存储)
|
||
- `2` - MINIO(MinIO存储)
|
||
- `3` - OSS(阿里云OSS)
|
||
|
||
### 5.2 字段含义更新
|
||
|
||
#### 5.2.1 OSS存储字段说明
|
||
对于OSS存储,字段含义如下:
|
||
|
||
- **file_upload/picture_upload**: 存储OSS对象键(Object Key)
|
||
- 格式:`coder-files/files/username/2025/07/08/20250708183651-44b5b3.png`
|
||
- 用于OSS文件操作(上传、删除、检查存在)
|
||
|
||
- **file_path/picture_path**: 存储完整的访问URL
|
||
- 格式:`https://coder-file-storage.oss-cn-hangzhou.aliyuncs.com/coder-files/files/username/2025/07/08/20250708183651-44b5b3.png`
|
||
- 用于前端显示和文件下载
|
||
|
||
- **file_service/picture_service**: 存储服务类型标识
|
||
- `3` - 表示使用阿里云OSS存储
|
||
|
||
### 5.3 数据迁移方案
|
||
|
||
#### 5.3.1 存储服务升级
|
||
对于现有的本地存储文件,可以提供迁移工具:
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class StorageMigrationService {
|
||
|
||
private final SysFileService sysFileService;
|
||
private final SysPictureService sysPictureService;
|
||
private final StorageServiceFactory storageServiceFactory;
|
||
|
||
/**
|
||
* 从本地存储迁移到OSS
|
||
*/
|
||
public void migrateFromLocalToOss() {
|
||
// 迁移文件
|
||
migrateFiles();
|
||
|
||
// 迁移图片
|
||
migratePictures();
|
||
}
|
||
|
||
private void migrateFiles() {
|
||
// 查询本地存储的文件
|
||
LambdaQueryWrapper<SysFile> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(SysFile::getFileService, "1");
|
||
List<SysFile> localFiles = sysFileService.list(wrapper);
|
||
|
||
StorageService ossService = storageServiceFactory.getStorageService("oss");
|
||
|
||
for (SysFile sysFile : localFiles) {
|
||
try {
|
||
// 读取本地文件
|
||
File localFile = new File(sysFile.getFileUpload());
|
||
if (!localFile.exists()) {
|
||
log.warn("本地文件不存在: {}", sysFile.getFileUpload());
|
||
continue;
|
||
}
|
||
|
||
// 上传到OSS
|
||
MockMultipartFile multipartFile = new MockMultipartFile(
|
||
"file",
|
||
sysFile.getFileName(),
|
||
Files.probeContentType(localFile.toPath()),
|
||
Files.readAllBytes(localFile.toPath())
|
||
);
|
||
|
||
String folderPath = extractFolderPath(sysFile.getFileUpload());
|
||
FileUploadResult uploadResult = ossService.uploadFile(multipartFile, sysFile.getNewName(), folderPath);
|
||
|
||
// 更新数据库记录
|
||
sysFile.setFileUpload(uploadResult.getFilePath());
|
||
sysFile.setFilePath(uploadResult.getFileUploadPath());
|
||
sysFile.setFileService("3");
|
||
sysFileService.updateById(sysFile);
|
||
|
||
log.info("文件迁移成功: {} -> {}", localFile.getPath(), uploadResult.getFilePath());
|
||
|
||
} catch (Exception e) {
|
||
log.error("文件迁移失败: {}", sysFile.getFileUpload(), e);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void migratePictures() {
|
||
// 类似实现图片迁移
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 安全机制
|
||
|
||
### 6.1 访问控制
|
||
|
||
#### 6.1.1 OSS访问权限
|
||
```java
|
||
@Configuration
|
||
public class OssSecurityConfig {
|
||
|
||
/**
|
||
* OSS访问策略配置
|
||
*/
|
||
@Bean
|
||
public OssAccessPolicy ossAccessPolicy() {
|
||
return OssAccessPolicy.builder()
|
||
.allowedOrigins("https://yourdomian.com")
|
||
.allowedMethods("GET", "POST", "PUT", "DELETE")
|
||
.allowedHeaders("*")
|
||
.maxAge(3600)
|
||
.build();
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 6.1.2 文件上传权限验证
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
public class FileUploadSecurityChecker {
|
||
|
||
/**
|
||
* 检查文件上传权限
|
||
*/
|
||
public void checkUploadPermission(String folderName, String username) {
|
||
// 检查用户是否有上传权限
|
||
if (!CoderLoginUtil.hasPermission("system:file:upload")) {
|
||
throw new BusinessException("没有文件上传权限");
|
||
}
|
||
|
||
// 检查文件夹访问权限
|
||
if (!isAllowedFolder(folderName, username)) {
|
||
throw new BusinessException("无权限访问该文件夹");
|
||
}
|
||
}
|
||
|
||
private boolean isAllowedFolder(String folderName, String username) {
|
||
// 实现文件夹权限检查逻辑
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.2 文件安全
|
||
|
||
#### 6.2.1 文件类型检查
|
||
```java
|
||
@Component
|
||
public class FileSecurityChecker {
|
||
|
||
// 危险文件类型黑名单
|
||
private static final List<String> DANGEROUS_EXTENSIONS = Arrays.asList(
|
||
"exe", "bat", "cmd", "com", "pif", "scr", "vbs", "js", "jar", "class"
|
||
);
|
||
|
||
/**
|
||
* 检查文件安全性
|
||
*/
|
||
public void checkFileSecurity(MultipartFile file) {
|
||
String filename = file.getOriginalFilename();
|
||
if (StringUtils.isBlank(filename)) {
|
||
throw new BusinessException("文件名不能为空");
|
||
}
|
||
|
||
String extension = FileTypeUtil.getFileExtension(filename).toLowerCase();
|
||
|
||
// 检查危险文件类型
|
||
if (DANGEROUS_EXTENSIONS.contains(extension)) {
|
||
throw new BusinessException("不允许上传该类型的文件");
|
||
}
|
||
|
||
// 检查文件头信息
|
||
checkFileHeader(file);
|
||
}
|
||
|
||
private void checkFileHeader(MultipartFile file) {
|
||
// 实现文件头检查逻辑
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 6.2.2 文件大小和数量限制
|
||
```java
|
||
@Component
|
||
@ConfigurationProperties(prefix = "coder.file.limit")
|
||
@Data
|
||
public class FileUploadLimitConfig {
|
||
|
||
/**
|
||
* 单个文件大小限制(MB)
|
||
*/
|
||
private Integer maxFileSize = 10;
|
||
|
||
/**
|
||
* 用户总存储空间限制(MB)
|
||
*/
|
||
private Integer maxUserStorage = 1000;
|
||
|
||
/**
|
||
* 用户每日上传数量限制
|
||
*/
|
||
private Integer maxDailyUploads = 100;
|
||
|
||
/**
|
||
* 用户每日上传大小限制(MB)
|
||
*/
|
||
private Integer maxDailySize = 500;
|
||
}
|
||
```
|
||
|
||
### 6.3 配置安全
|
||
|
||
#### 6.3.1 敏感信息加密
|
||
```yaml
|
||
# application-dev.yml
|
||
coder:
|
||
oss:
|
||
# 使用环境变量或配置中心管理敏感信息
|
||
access-key-id: ${OSS_ACCESS_KEY_ID}
|
||
access-key-secret: ${OSS_ACCESS_KEY_SECRET}
|
||
```
|
||
|
||
#### 6.3.2 OSS Bucket安全配置
|
||
```java
|
||
@Configuration
|
||
@RequiredArgsConstructor
|
||
public class OssBucketSecurityConfig {
|
||
|
||
private final OSS ossClient;
|
||
private final OssConfig ossConfig;
|
||
|
||
@PostConstruct
|
||
public void configureBucketSecurity() {
|
||
try {
|
||
// 设置Bucket访问权限
|
||
ossClient.setBucketAcl(ossConfig.getBucketName(), CannedAccessControlList.Private);
|
||
|
||
// 配置跨域规则
|
||
SetBucketCORSRequest request = new SetBucketCORSRequest(ossConfig.getBucketName());
|
||
CORSRule corsRule = new CORSRule();
|
||
corsRule.setAllowedOrigins(Arrays.asList("https://yourdomian.com"));
|
||
corsRule.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
|
||
corsRule.setAllowedHeaders(Arrays.asList("*"));
|
||
corsRule.setMaxAge(3600);
|
||
request.setCorsRules(Arrays.asList(corsRule));
|
||
|
||
ossClient.setBucketCORS(request);
|
||
|
||
} catch (Exception e) {
|
||
log.error("OSS Bucket安全配置失败", e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 7. 性能优化
|
||
|
||
### 7.1 连接池配置
|
||
|
||
#### 7.1.1 OSS连接池
|
||
```java
|
||
@Configuration
|
||
@EnableConfigurationProperties(OssConfig.class)
|
||
public class OssClientConfig {
|
||
|
||
@Bean
|
||
public OSS ossClient(OssConfig ossConfig) {
|
||
// 创建ClientConfiguration
|
||
ClientConfiguration clientConfiguration = new ClientConfiguration();
|
||
|
||
// 设置连接超时时间
|
||
clientConfiguration.setConnectionTimeout(ossConfig.getConnectTimeout().intValue());
|
||
|
||
// 设置读取超时时间
|
||
clientConfiguration.setSocketTimeout(ossConfig.getReadTimeout().intValue());
|
||
|
||
// 设置连接池大小
|
||
clientConfiguration.setMaxConnections(200);
|
||
|
||
// 设置请求超时时间
|
||
clientConfiguration.setRequestTimeout(30000);
|
||
|
||
// 设置失败重试次数
|
||
clientConfiguration.setMaxErrorRetry(3);
|
||
|
||
return new OSSClientBuilder().build(
|
||
ossConfig.getEndpoint(),
|
||
ossConfig.getAccessKeyId(),
|
||
ossConfig.getAccessKeySecret(),
|
||
clientConfiguration
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 缓存策略
|
||
|
||
#### 7.2.1 文件信息缓存
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class FileInfoCacheService {
|
||
|
||
private final RedisTemplate<String, Object> redisTemplate;
|
||
|
||
private static final String FILE_INFO_KEY = "file:info:";
|
||
private static final Duration CACHE_TIMEOUT = Duration.ofHours(1);
|
||
|
||
/**
|
||
* 缓存文件信息
|
||
*/
|
||
public void cacheFileInfo(String fileId, SysFile fileInfo) {
|
||
redisTemplate.opsForValue().set(FILE_INFO_KEY + fileId, fileInfo, CACHE_TIMEOUT);
|
||
}
|
||
|
||
/**
|
||
* 获取缓存的文件信息
|
||
*/
|
||
public SysFile getCachedFileInfo(String fileId) {
|
||
return (SysFile) redisTemplate.opsForValue().get(FILE_INFO_KEY + fileId);
|
||
}
|
||
|
||
/**
|
||
* 删除文件信息缓存
|
||
*/
|
||
public void removeFileInfoCache(String fileId) {
|
||
redisTemplate.delete(FILE_INFO_KEY + fileId);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.3 异步处理
|
||
|
||
#### 7.3.1 异步文件上传
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class AsyncFileUploadService {
|
||
|
||
private final StorageServiceFactory storageServiceFactory;
|
||
private final SysFileService sysFileService;
|
||
|
||
@Async("fileUploadExecutor")
|
||
public CompletableFuture<FileUploadResult> uploadFileAsync(
|
||
MultipartFile file, String fileName, String folderPath) {
|
||
|
||
try {
|
||
StorageService storageService = storageServiceFactory.getStorageService();
|
||
FileUploadResult result = storageService.uploadFile(file, fileName, folderPath);
|
||
|
||
log.info("异步文件上传成功: {}", result.getFilePath());
|
||
return CompletableFuture.completedFuture(result);
|
||
|
||
} catch (Exception e) {
|
||
log.error("异步文件上传失败", e);
|
||
return CompletableFuture.failedFuture(e);
|
||
}
|
||
}
|
||
}
|
||
|
||
@Configuration
|
||
@EnableAsync
|
||
public class AsyncConfig {
|
||
|
||
@Bean("fileUploadExecutor")
|
||
public ThreadPoolTaskExecutor fileUploadExecutor() {
|
||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||
executor.setCorePoolSize(10);
|
||
executor.setMaxPoolSize(50);
|
||
executor.setQueueCapacity(200);
|
||
executor.setThreadNamePrefix("FileUpload-");
|
||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||
executor.initialize();
|
||
return executor;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 8. 监控和日志
|
||
|
||
### 8.1 操作日志
|
||
|
||
#### 8.1.1 文件操作日志
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class FileOperationLogger {
|
||
|
||
private final SysOperLogService operLogService;
|
||
|
||
/**
|
||
* 记录文件上传日志
|
||
*/
|
||
public void logFileUpload(String fileName, String fileSize, String storageType, String username) {
|
||
SysOperLog operLog = new SysOperLog();
|
||
operLog.setOperName(username);
|
||
operLog.setOperType(OperType.UPLOAD.name());
|
||
operLog.setBusinessType("文件上传");
|
||
operLog.setMethod("uploadFile");
|
||
operLog.setOperDesc("上传文件: " + fileName + ", 大小: " + fileSize + ", 存储类型: " + storageType);
|
||
operLog.setOperTime(new Date());
|
||
|
||
operLogService.save(operLog);
|
||
}
|
||
|
||
/**
|
||
* 记录文件删除日志
|
||
*/
|
||
public void logFileDelete(String fileName, String storageType, String username) {
|
||
SysOperLog operLog = new SysOperLog();
|
||
operLog.setOperName(username);
|
||
operLog.setOperType(OperType.DELETE.name());
|
||
operLog.setBusinessType("文件删除");
|
||
operLog.setMethod("deleteFile");
|
||
operLog.setOperDesc("删除文件: " + fileName + ", 存储类型: " + storageType);
|
||
operLog.setOperTime(new Date());
|
||
|
||
operLogService.save(operLog);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 性能监控
|
||
|
||
#### 8.2.1 文件上传性能监控
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
public class FileUploadMonitor {
|
||
|
||
private final MeterRegistry meterRegistry;
|
||
|
||
/**
|
||
* 记录文件上传指标
|
||
*/
|
||
public void recordUploadMetrics(String storageType, long fileSize, long uploadTime) {
|
||
// 记录上传次数
|
||
Counter.builder("file.upload.count")
|
||
.tag("storage.type", storageType)
|
||
.register(meterRegistry)
|
||
.increment();
|
||
|
||
// 记录上传大小
|
||
Counter.builder("file.upload.size")
|
||
.tag("storage.type", storageType)
|
||
.register(meterRegistry)
|
||
.increment(fileSize);
|
||
|
||
// 记录上传时间
|
||
Timer.builder("file.upload.duration")
|
||
.tag("storage.type", storageType)
|
||
.register(meterRegistry)
|
||
.record(uploadTime, TimeUnit.MILLISECONDS);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.3 告警机制
|
||
|
||
#### 8.3.1 文件上传异常告警
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class FileUploadAlertService {
|
||
|
||
private final AlarmService alarmService;
|
||
|
||
/**
|
||
* 文件上传失败告警
|
||
*/
|
||
public void alertUploadFailure(String fileName, String errorMessage, String username) {
|
||
AlarmMessage alarm = AlarmMessage.builder()
|
||
.title("文件上传失败告警")
|
||
.content(String.format("用户%s上传文件%s失败,错误信息:%s", username, fileName, errorMessage))
|
||
.level(AlarmLevel.ERROR)
|
||
.timestamp(new Date())
|
||
.build();
|
||
|
||
alarmService.sendAlarm(alarm);
|
||
}
|
||
|
||
/**
|
||
* 存储空间不足告警
|
||
*/
|
||
public void alertStorageSpaceInsufficient(String storageType, long remainingSpace) {
|
||
AlarmMessage alarm = AlarmMessage.builder()
|
||
.title("存储空间不足告警")
|
||
.content(String.format("%s存储空间不足,剩余空间:%s", storageType, FileTypeUtil.formatFileSize(remainingSpace)))
|
||
.level(AlarmLevel.WARNING)
|
||
.timestamp(new Date())
|
||
.build();
|
||
|
||
alarmService.sendAlarm(alarm);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 9. 部署和运维
|
||
|
||
### 9.1 环境配置
|
||
|
||
#### 9.1.1 开发环境配置
|
||
```yaml
|
||
# application-dev.yml
|
||
coder:
|
||
storage:
|
||
type: oss
|
||
oss:
|
||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||
access-key-id: ${OSS_ACCESS_KEY_ID}
|
||
access-key-secret: ${OSS_ACCESS_KEY_SECRET}
|
||
bucket-name: coder-file-storage-dev
|
||
domain: https://coder-file-storage-dev.oss-cn-hangzhou.aliyuncs.com
|
||
path-prefix: coder-files
|
||
https: true
|
||
connect-timeout: 10000
|
||
read-timeout: 10000
|
||
|
||
# 日志配置
|
||
logging:
|
||
level:
|
||
org.leocoder.thin.oss: DEBUG
|
||
com.aliyun.oss: INFO
|
||
```
|
||
|
||
#### 9.1.2 生产环境配置
|
||
```yaml
|
||
# application-prod.yml
|
||
coder:
|
||
storage:
|
||
type: oss
|
||
oss:
|
||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||
access-key-id: ${OSS_ACCESS_KEY_ID}
|
||
access-key-secret: ${OSS_ACCESS_KEY_SECRET}
|
||
bucket-name: coder-file-storage-prod
|
||
domain: https://files.yourcompany.com
|
||
path-prefix: coder-files
|
||
https: true
|
||
connect-timeout: 10000
|
||
read-timeout: 10000
|
||
|
||
# 日志配置
|
||
logging:
|
||
level:
|
||
org.leocoder.thin.oss: INFO
|
||
com.aliyun.oss: WARN
|
||
```
|
||
|
||
### 9.2 Docker部署
|
||
|
||
#### 9.2.1 Dockerfile
|
||
|
||

|
||
|
||
```dockerfile
|
||
FROM openjdk:17-jdk-slim
|
||
|
||
MAINTAINER Leocoder
|
||
|
||
VOLUME /tmp
|
||
|
||
ADD coder-common-thin-web.jar app.jar
|
||
|
||
EXPOSE 18099
|
||
|
||
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]
|
||
```
|
||
|
||
#### 9.2.2 docker-compose.yml
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
coder-app:
|
||
build: .
|
||
ports:
|
||
- "18099:18099"
|
||
environment:
|
||
- SPRING_PROFILES_ACTIVE=prod
|
||
- OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID}
|
||
- OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET}
|
||
- MYSQL_HOST=mysql
|
||
- REDIS_HOST=redis
|
||
depends_on:
|
||
- mysql
|
||
- redis
|
||
volumes:
|
||
- ./logs:/app/logs
|
||
networks:
|
||
- coder-network
|
||
|
||
mysql:
|
||
image: mysql:8.0
|
||
environment:
|
||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
|
||
- MYSQL_DATABASE=coder_common_thin
|
||
ports:
|
||
- "3306:3306"
|
||
volumes:
|
||
- mysql_data:/var/lib/mysql
|
||
networks:
|
||
- coder-network
|
||
|
||
redis:
|
||
image: redis:7.0
|
||
ports:
|
||
- "6379:6379"
|
||
volumes:
|
||
- redis_data:/data
|
||
networks:
|
||
- coder-network
|
||
|
||
volumes:
|
||
mysql_data:
|
||
redis_data:
|
||
|
||
networks:
|
||
coder-network:
|
||
driver: bridge
|
||
```
|
||
|
||
### 9.3 健康检查
|
||
|
||
#### 9.3.1 OSS连接健康检查
|
||
```java
|
||
@Component
|
||
@RequiredArgsConstructor
|
||
public class OssHealthIndicator implements HealthIndicator {
|
||
|
||
private final OSS ossClient;
|
||
private final OssConfig ossConfig;
|
||
|
||
@Override
|
||
public Health health() {
|
||
try {
|
||
// 检查OSS连接
|
||
boolean exists = ossClient.doesBucketExist(ossConfig.getBucketName());
|
||
|
||
if (exists) {
|
||
return Health.up()
|
||
.withDetail("oss.bucket", ossConfig.getBucketName())
|
||
.withDetail("oss.endpoint", ossConfig.getEndpoint())
|
||
.withDetail("status", "connected")
|
||
.build();
|
||
} else {
|
||
return Health.down()
|
||
.withDetail("oss.bucket", ossConfig.getBucketName())
|
||
.withDetail("error", "Bucket not found")
|
||
.build();
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
return Health.down()
|
||
.withDetail("error", e.getMessage())
|
||
.build();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.4 数据备份
|
||
|
||
#### 9.4.1 数据库备份脚本
|
||
```bash
|
||
#!/bin/bash
|
||
|
||
# 数据库备份脚本
|
||
BACKUP_DIR="/backup/mysql"
|
||
DATE=$(date +%Y%m%d_%H%M%S)
|
||
DB_NAME="coder_common_thin"
|
||
|
||
# 创建备份目录
|
||
mkdir -p $BACKUP_DIR
|
||
|
||
# 备份数据库
|
||
mysqldump -h localhost -u root -p$MYSQL_ROOT_PASSWORD $DB_NAME > $BACKUP_DIR/backup_$DATE.sql
|
||
|
||
# 删除7天前的备份
|
||
find $BACKUP_DIR -name "backup_*.sql" -mtime +7 -delete
|
||
|
||
echo "数据库备份完成: $BACKUP_DIR/backup_$DATE.sql"
|
||
```
|
||
|
||
#### 9.4.2 OSS文件备份
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
@Slf4j
|
||
public class OssBackupService {
|
||
|
||
private final OSS ossClient;
|
||
private final OssConfig ossConfig;
|
||
|
||
/**
|
||
* 备份OSS文件到另一个Bucket
|
||
*/
|
||
public void backupToSecondaryBucket(String backupBucketName) {
|
||
try {
|
||
// 列出所有对象
|
||
ObjectListing objectListing = ossClient.listObjects(ossConfig.getBucketName());
|
||
|
||
for (OSSObjectSummary objectSummary : objectListing.getObjectSummaries()) {
|
||
String objectKey = objectSummary.getKey();
|
||
|
||
// 复制到备份Bucket
|
||
CopyObjectRequest copyRequest = new CopyObjectRequest(
|
||
ossConfig.getBucketName(),
|
||
objectKey,
|
||
backupBucketName,
|
||
objectKey
|
||
);
|
||
|
||
ossClient.copyObject(copyRequest);
|
||
|
||
log.info("文件备份成功: {}", objectKey);
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("OSS文件备份失败", e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 10. 风险评估和处理
|
||
|
||
### 10.1 技术风险
|
||
|
||
#### 10.1.1 OSS服务不可用
|
||
**风险等级**: 高
|
||
**影响**: 文件上传和访问服务中断
|
||
**处理方案**:
|
||
1. 实现多存储策略,支持本地存储降级
|
||
2. 配置OSS多地域容灾
|
||
3. 实现缓存机制,缓存常用文件
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class FallbackStorageService implements StorageService {
|
||
|
||
private final StorageServiceFactory storageServiceFactory;
|
||
private final LocalStorageService localStorageService;
|
||
|
||
@Override
|
||
public FileUploadResult uploadFile(MultipartFile file, String fileName, String folderPath) {
|
||
try {
|
||
// 优先使用OSS
|
||
StorageService ossService = storageServiceFactory.getStorageService("oss");
|
||
return ossService.uploadFile(file, fileName, folderPath);
|
||
|
||
} catch (Exception e) {
|
||
log.warn("OSS上传失败,降级使用本地存储", e);
|
||
// 降级到本地存储
|
||
return localStorageService.uploadFile(file, fileName, folderPath);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 10.1.2 访问密钥泄露
|
||
**风险等级**: 高
|
||
**影响**: 安全风险,可能导致数据泄露
|
||
**处理方案**:
|
||
1. 使用RAM子账号,最小权限原则
|
||
2. 定期轮换访问密钥
|
||
3. 使用STS临时凭证
|
||
4. 配置访问控制策略
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class OssSecurityService {
|
||
|
||
private final OSS ossClient;
|
||
|
||
/**
|
||
* 获取临时访问凭证
|
||
*/
|
||
public STSCredentials getTemporaryCredentials() {
|
||
// 实现STS临时凭证获取
|
||
// 返回临时的AccessKeyId、AccessKeySecret、SecurityToken
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 轮换访问密钥
|
||
*/
|
||
public void rotateAccessKey() {
|
||
// 实现访问密钥轮换逻辑
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.2 业务风险
|
||
|
||
#### 10.2.1 文件丢失
|
||
**风险等级**: 中
|
||
**影响**: 业务数据丢失
|
||
**处理方案**:
|
||
1. 开启OSS版本控制
|
||
2. 实现文件备份策略
|
||
3. 定期数据校验
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class FileIntegrityService {
|
||
|
||
/**
|
||
* 文件完整性校验
|
||
*/
|
||
public boolean verifyFileIntegrity(String objectKey, String expectedMD5) {
|
||
try {
|
||
ObjectMetadata metadata = ossClient.getObjectMetadata(bucketName, objectKey);
|
||
String actualMD5 = metadata.getETag();
|
||
|
||
return Objects.equals(expectedMD5, actualMD5);
|
||
|
||
} catch (Exception e) {
|
||
log.error("文件完整性校验失败: {}", objectKey, e);
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 10.2.2 存储成本过高
|
||
**风险等级**: 中
|
||
**影响**: 运营成本增加
|
||
**处理方案**:
|
||
1. 配置生命周期管理
|
||
2. 实现文件压缩
|
||
3. 定期清理无用文件
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class StorageCostOptimization {
|
||
|
||
/**
|
||
* 设置文件生命周期
|
||
*/
|
||
public void setLifecycleRule() {
|
||
SetBucketLifecycleRequest request = new SetBucketLifecycleRequest(bucketName);
|
||
|
||
LifecycleRule rule = new LifecycleRule();
|
||
rule.setId("DeleteOldFiles");
|
||
rule.setStatus(RuleStatus.Enabled);
|
||
rule.setPrefix("temp/");
|
||
|
||
// 30天后删除
|
||
rule.setExpirationDays(30);
|
||
|
||
request.setLifecycleRules(Arrays.asList(rule));
|
||
ossClient.setBucketLifecycle(request);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.3 性能风险
|
||
|
||
#### 10.3.1 上传性能瓶颈
|
||
**风险等级**: 中
|
||
**影响**: 用户体验下降
|
||
**处理方案**:
|
||
1. 实现分片上传
|
||
2. 使用CDN加速
|
||
3. 优化连接池配置
|
||
|
||
```java
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class LargeFileUploadService {
|
||
|
||
/**
|
||
* 分片上传大文件
|
||
*/
|
||
public String uploadLargeFile(MultipartFile file, String objectKey) {
|
||
try {
|
||
// 初始化分片上传
|
||
InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(bucketName, objectKey);
|
||
InitiateMultipartUploadResult initResult = ossClient.initiateMultipartUpload(initRequest);
|
||
|
||
String uploadId = initResult.getUploadId();
|
||
|
||
// 分片上传逻辑
|
||
List<PartETag> partETags = new ArrayList<>();
|
||
|
||
// 完成分片上传
|
||
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
|
||
bucketName, objectKey, uploadId, partETags);
|
||
|
||
ossClient.completeMultipartUpload(completeRequest);
|
||
|
||
return objectKey;
|
||
|
||
} catch (Exception e) {
|
||
log.error("大文件上传失败", e);
|
||
throw new BusinessException("大文件上传失败");
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 11. 测试方案
|
||
|
||
### 11.1 单元测试
|
||
|
||
#### 11.1.1 OSS服务测试
|
||
```java
|
||
@SpringBootTest
|
||
@TestPropertySource(properties = {
|
||
"coder.storage.type=oss",
|
||
"coder.oss.endpoint=oss-cn-hangzhou.aliyuncs.com",
|
||
"coder.oss.bucket-name=test-bucket"
|
||
})
|
||
class OssServiceTest {
|
||
|
||
@Autowired
|
||
private OssService ossService;
|
||
|
||
@Test
|
||
void testUploadFile() {
|
||
// 创建测试文件
|
||
MockMultipartFile mockFile = new MockMultipartFile(
|
||
"test.txt",
|
||
"test.txt",
|
||
"text/plain",
|
||
"test content".getBytes()
|
||
);
|
||
|
||
// 执行上传
|
||
FileUploadResult result = ossService.uploadFile(mockFile, "test.txt", "test/");
|
||
|
||
// 验证结果
|
||
assertNotNull(result);
|
||
assertNotNull(result.getFilePath());
|
||
assertNotNull(result.getFileUploadPath());
|
||
}
|
||
|
||
@Test
|
||
void testDeleteFile() {
|
||
// 测试文件删除
|
||
boolean result = ossService.deleteFile("test/test.txt");
|
||
assertTrue(result);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 11.2 集成测试
|
||
|
||
#### 11.2.1 文件上传接口测试
|
||
```java
|
||
@SpringBootTest
|
||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||
@TestPropertySource(locations = "classpath:application-test.yml")
|
||
class FileUploadIntegrationTest {
|
||
|
||
@Autowired
|
||
private TestRestTemplate restTemplate;
|
||
|
||
@Test
|
||
void testFileUpload() {
|
||
// 创建测试文件
|
||
FileSystemResource resource = new FileSystemResource("test.txt");
|
||
|
||
// 构建请求
|
||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||
body.add("file", resource);
|
||
|
||
HttpHeaders headers = new HttpHeaders();
|
||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||
|
||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||
|
||
// 发送请求
|
||
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||
"/coder/file/uploadFile/2/files/-1",
|
||
requestEntity,
|
||
Map.class
|
||
);
|
||
|
||
// 验证响应
|
||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||
assertNotNull(response.getBody());
|
||
}
|
||
}
|
||
```
|
||
|
||
### 11.3 性能测试
|
||
|
||
#### 11.3.1 并发上传测试
|
||
```java
|
||
@Test
|
||
void testConcurrentUpload() throws InterruptedException {
|
||
int threadCount = 10;
|
||
int filesPerThread = 5;
|
||
CountDownLatch latch = new CountDownLatch(threadCount);
|
||
|
||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||
|
||
for (int i = 0; i < threadCount; i++) {
|
||
executor.submit(() -> {
|
||
try {
|
||
for (int j = 0; j < filesPerThread; j++) {
|
||
// 模拟文件上传
|
||
uploadTestFile();
|
||
}
|
||
} finally {
|
||
latch.countDown();
|
||
}
|
||
});
|
||
}
|
||
|
||
latch.await(30, TimeUnit.SECONDS);
|
||
executor.shutdown();
|
||
}
|
||
```
|
||
|
||
## 12. 实施计划
|
||
|
||
### 12.1 开发阶段
|
||
|
||
#### 第一阶段:基础架构搭建(1-2周)
|
||
1. 创建OSS插件模块
|
||
2. 实现存储服务接口
|
||
3. 配置OSS客户端
|
||
4. 实现基础的上传和删除功能
|
||
|
||
#### 第二阶段:功能完善(2-3周)
|
||
1. 完善文件上传控制器
|
||
2. 实现文件删除机制
|
||
3. 添加安全验证
|
||
4. 实现错误处理和日志记录
|
||
|
||
#### 第三阶段:优化和测试(1-2周)
|
||
1. 性能优化
|
||
2. 单元测试和集成测试
|
||
3. 安全测试
|
||
4. 性能测试
|
||
|
||
### 12.2 部署计划
|
||
|
||
#### 12.2.1 预发布环境
|
||
1. 部署到测试环境
|
||
2. 功能验证
|
||
3. 性能测试
|
||
4. 安全测试
|
||
|
||
#### 12.2.2 生产环境
|
||
1. 灰度发布
|
||
2. 监控告警
|
||
3. 回滚准备
|
||
4. 全量发布
|
||
|
||
### 12.3 验收标准
|
||
|
||
#### 12.3.1 功能验收
|
||
- [ ] 文件上传功能正常
|
||
- [ ] 图片上传功能正常
|
||
- [ ] 文件删除功能正常
|
||
- [ ] 批量删除功能正常
|
||
- [ ] 文件访问URL正常
|
||
- [ ] 前端页面显示正常
|
||
|
||
#### 12.3.2 性能验收
|
||
- [ ] 单文件上传响应时间 < 5秒
|
||
- [ ] 并发上传支持 > 100个请求/秒
|
||
- [ ] 文件删除响应时间 < 2秒
|
||
- [ ] 系统可用性 > 99.9%
|
||
|
||
#### 12.3.3 安全验收
|
||
- [ ] 文件类型验证正常
|
||
- [ ] 文件大小限制正常
|
||
- [ ] 权限验证正常
|
||
- [ ] 敏感信息加密存储
|
||
- [ ] 访问日志记录完整
|
||
|
||
## 13. 总结
|
||
|
||
本设计方案基于现有的文件上传系统,通过插件化架构实现了阿里云OSS的集成,具有以下特点:
|
||
|
||
### 13.1 优势
|
||
1. **架构清晰**: 采用分层架构和插件化设计,便于扩展和维护
|
||
2. **兼容性好**: 保持与现有系统的完全兼容
|
||
3. **功能完善**: 支持文件上传、删除、访问等完整功能
|
||
4. **安全性高**: 实现了完善的安全机制
|
||
5. **性能优化**: 通过连接池、缓存等技术提升性能
|
||
6. **易于运维**: 提供了完善的监控和告警机制
|
||
|
||
### 13.2 创新点
|
||
1. **存储服务抽象**: 通过接口抽象实现多种存储服务的统一管理
|
||
2. **插件化设计**: 通过@Enable注解实现功能的可插拔配置
|
||
3. **降级策略**: 实现了OSS不可用时的本地存储降级
|
||
4. **文件完整性校验**: 确保文件上传和存储的完整性
|
||
5. **成本优化**: 通过生命周期管理控制存储成本
|
||
|
||
### 13.3 扩展性
|
||
本方案具有良好的扩展性,可以很容易地:
|
||
1. 添加新的存储服务(如华为云OBS、腾讯云COS)
|
||
2. 实现更多的文件处理功能(如图片压缩、文档转换)
|
||
3. 集成更多的安全机制(如数据加密、访问控制)
|
||
4. 优化性能(如CDN集成、边缘计算)
|
||
|
||
通过本方案的实施,可以显著提升系统的文件处理能力,为用户提供更好的体验,同时确保系统的安全性和稳定性。 |