添加OSS文件上传系统的详细设计文档,包含: - 系统架构设计和技术选型 - 核心功能模块说明 - 配置参数和使用示例 - 最佳实践和注意事项 为OSS插件模块提供完整的技术文档支持。
56 KiB
56 KiB
阿里云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接口
// 文件上传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 数据模型
-- 文件表
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 存在的问题
- 图片删除不一致: SysPictureController删除时未删除物理文件
- 存储服务扩展: 目前只实现了本地存储,OSS功能未实现
- 配置管理: 缺少OSS相关配置管理
3. 阿里云OSS架构设计
3.1 整体架构图
┌─────────────────────────────────────────────────────────────────┐
│ 前端应用层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 文件管理页面 │ │ 图片管理页面 │ │ 上传组件 │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 后端应用层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ FileController │ │ SysFileController│ │SysPictureController││
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 业务服务层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ FileService │ │ StorageService │ │ OssService │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 存储适配层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ LocalStorage │ │ MinioStorage │ │ OssStorage │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 存储基础设施 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 本地磁盘 │ │ MinIO服务 │ │ 阿里云OSS │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
3.2 核心组件设计
3.2.1 存储服务接口
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存储实现
@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 存储工厂模式
@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 启用注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(OssAutoConfiguration.class)
public @interface EnableCoderOss {
// OSS插件启用注解
}
4. 技术实现方案
4.1 依赖管理
4.1.1 Maven依赖
<!-- 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 主启动类配置
@EnableCoderOss
@SpringBootApplication
public class CoderCommonThinApplication {
public static void main(String[] args) {
SpringApplication.run(CoderCommonThinApplication.class, args);
}
}
4.2 配置管理
4.2.1 OSS配置类
@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 应用配置
# 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客户端配置
@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服务实现
@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 存储服务工厂
@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 文件上传控制器改造
@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 文件删除服务
@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 文件管理控制器改造
@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 图片管理控制器改造
@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 存储服务升级
对于现有的本地存储文件,可以提供迁移工具:
@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访问权限
@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 文件上传权限验证
@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 文件类型检查
@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 文件大小和数量限制
@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 敏感信息加密
# application-dev.yml
coder:
oss:
# 使用环境变量或配置中心管理敏感信息
access-key-id: ${OSS_ACCESS_KEY_ID}
access-key-secret: ${OSS_ACCESS_KEY_SECRET}
6.3.2 OSS Bucket安全配置
@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连接池
@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 文件信息缓存
@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 异步文件上传
@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 文件操作日志
@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 文件上传性能监控
@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 文件上传异常告警
@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 开发环境配置
# 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 生产环境配置
# 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
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
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连接健康检查
@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 数据库备份脚本
#!/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文件备份
@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服务不可用
风险等级: 高 影响: 文件上传和访问服务中断 处理方案:
- 实现多存储策略,支持本地存储降级
- 配置OSS多地域容灾
- 实现缓存机制,缓存常用文件
@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 访问密钥泄露
风险等级: 高 影响: 安全风险,可能导致数据泄露 处理方案:
- 使用RAM子账号,最小权限原则
- 定期轮换访问密钥
- 使用STS临时凭证
- 配置访问控制策略
@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 文件丢失
风险等级: 中 影响: 业务数据丢失 处理方案:
- 开启OSS版本控制
- 实现文件备份策略
- 定期数据校验
@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 存储成本过高
风险等级: 中 影响: 运营成本增加 处理方案:
- 配置生命周期管理
- 实现文件压缩
- 定期清理无用文件
@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 上传性能瓶颈
风险等级: 中 影响: 用户体验下降 处理方案:
- 实现分片上传
- 使用CDN加速
- 优化连接池配置
@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服务测试
@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 文件上传接口测试
@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 并发上传测试
@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周)
- 创建OSS插件模块
- 实现存储服务接口
- 配置OSS客户端
- 实现基础的上传和删除功能
第二阶段:功能完善(2-3周)
- 完善文件上传控制器
- 实现文件删除机制
- 添加安全验证
- 实现错误处理和日志记录
第三阶段:优化和测试(1-2周)
- 性能优化
- 单元测试和集成测试
- 安全测试
- 性能测试
12.2 部署计划
12.2.1 预发布环境
- 部署到测试环境
- 功能验证
- 性能测试
- 安全测试
12.2.2 生产环境
- 灰度发布
- 监控告警
- 回滚准备
- 全量发布
12.3 验收标准
12.3.1 功能验收
- 文件上传功能正常
- 图片上传功能正常
- 文件删除功能正常
- 批量删除功能正常
- 文件访问URL正常
- 前端页面显示正常
12.3.2 性能验收
- 单文件上传响应时间 < 5秒
- 并发上传支持 > 100个请求/秒
- 文件删除响应时间 < 2秒
- 系统可用性 > 99.9%
12.3.3 安全验收
- 文件类型验证正常
- 文件大小限制正常
- 权限验证正常
- 敏感信息加密存储
- 访问日志记录完整
13. 总结
本设计方案基于现有的文件上传系统,通过插件化架构实现了阿里云OSS的集成,具有以下特点:
13.1 优势
- 架构清晰: 采用分层架构和插件化设计,便于扩展和维护
- 兼容性好: 保持与现有系统的完全兼容
- 功能完善: 支持文件上传、删除、访问等完整功能
- 安全性高: 实现了完善的安全机制
- 性能优化: 通过连接池、缓存等技术提升性能
- 易于运维: 提供了完善的监控和告警机制
13.2 创新点
- 存储服务抽象: 通过接口抽象实现多种存储服务的统一管理
- 插件化设计: 通过@Enable注解实现功能的可插拔配置
- 降级策略: 实现了OSS不可用时的本地存储降级
- 文件完整性校验: 确保文件上传和存储的完整性
- 成本优化: 通过生命周期管理控制存储成本
13.3 扩展性
本方案具有良好的扩展性,可以很容易地:
- 添加新的存储服务(如华为云OBS、腾讯云COS)
- 实现更多的文件处理功能(如图片压缩、文档转换)
- 集成更多的安全机制(如数据加密、访问控制)
- 优化性能(如CDN集成、边缘计算)
通过本方案的实施,可以显著提升系统的文件处理能力,为用户提供更好的体验,同时确保系统的安全性和稳定性。