# 阿里云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集成、边缘计算) 通过本方案的实施,可以显著提升系统的文件处理能力,为用户提供更好的体验,同时确保系统的安全性和稳定性。