diff --git a/doc/oss/阿里云OSS文件上传系统设计方案.md b/doc/oss/阿里云OSS文件上传系统设计方案.md new file mode 100644 index 0000000..ca4ef85 --- /dev/null +++ b/doc/oss/阿里云OSS文件上传系统设计方案.md @@ -0,0 +1,1855 @@ +# 阿里云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(`/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(`/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 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 + + + org.leocoder.thin + coder-common-thin-oss + ${project.version} + + + + + com.aliyun.oss + aliyun-sdk-oss + 3.17.4 + +``` + +#### 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 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 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 ids) { + List 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 ids) { + List 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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysFile::getFileService, "1"); + List 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 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 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 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 + +![田园犬](https://gaoziman.oss-cn-hangzhou.aliyuncs.com/uPic/2025-07-09-%E7%94%B0%E5%9B%AD%E7%8A%AC.svg) + +```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 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 body = new LinkedMultiValueMap<>(); + body.add("file", resource); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + + // 发送请求 + ResponseEntity 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集成、边缘计算) + +通过本方案的实施,可以显著提升系统的文件处理能力,为用户提供更好的体验,同时确保系统的安全性和稳定性。 \ No newline at end of file