1. OSS自动转链上传概述

阿里云对象存储服务(OSS)是企业级云存储服务,通过自动转链上传可以实现文件的高效存储、CDN加速访问和自动化管理。本文将详细介绍OSS的配置、文件上传、自动转链和CDN加速的完整解决方案。

1.1 核心功能

  1. 文件上传: 支持多种文件格式和上传方式
  2. 自动转链: 自动生成CDN加速链接
  3. 权限控制: 灵活的访问权限管理
  4. 批量处理: 支持批量文件操作
  5. 监控告警: 文件上传和访问监控

1.2 技术架构

1
2
3
4
5
文件上传 → OSS存储 → CDN加速 → 自动转链 → 访问链接
↓ ↓ ↓ ↓ ↓
本地文件 → 云存储 → 全球加速 → 链接生成 → 用户访问
↓ ↓ ↓ ↓ ↓
文件管理 → 权限控制 → 缓存策略 → 链接管理 → 访问统计

2. OSS配置与初始化

2.1 OSS配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* OSS配置类
*/
@Configuration
public class OSSConfig {

@Value("${oss.endpoint}")
private String endpoint;

@Value("${oss.access-key-id}")
private String accessKeyId;

@Value("${oss.access-key-secret}")
private String accessKeySecret;

@Value("${oss.bucket-name}")
private String bucketName;

@Value("${oss.cdn-domain}")
private String cdnDomain;

@Value("${oss.default-path}")
private String defaultPath;

/**
* OSS客户端配置
*/
@Bean
public OSS ossClient() {
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}

/**
* OSS配置属性
*/
@Bean
public OSSProperties ossProperties() {
return OSSProperties.builder()
.endpoint(endpoint)
.accessKeyId(accessKeyId)
.accessKeySecret(accessKeySecret)
.bucketName(bucketName)
.cdnDomain(cdnDomain)
.defaultPath(defaultPath)
.build();
}
}

/**
* OSS配置属性
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String cdnDomain;
private String defaultPath;
}

2.2 应用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# application.yml
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
access-key-id: ${OSS_ACCESS_KEY_ID}
access-key-secret: ${OSS_ACCESS_KEY_SECRET}
bucket-name: my-bucket
cdn-domain: https://cdn.example.com
default-path: uploads/

# 文件上传配置
file:
upload:
max-size: 100MB
allowed-types: jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,ppt,pptx
temp-path: /tmp/uploads/

3. 文件上传服务

3.1 OSS文件上传服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/**
* OSS文件上传服务
*/
@Service
public class OSSFileUploadService {

@Autowired
private OSS ossClient;

@Autowired
private OSSProperties ossProperties;

@Autowired
private FileMetadataService fileMetadataService;

/**
* 上传文件
* @param file 文件
* @param path 存储路径
* @return 上传结果
*/
public FileUploadResult uploadFile(MultipartFile file, String path) {
try {
// 1. 验证文件
validateFile(file);

// 2. 生成文件名
String fileName = generateFileName(file.getOriginalFilename());
String objectKey = buildObjectKey(path, fileName);

// 3. 上传到OSS
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossProperties.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);

// 4. 生成访问链接
String accessUrl = generateAccessUrl(objectKey);
String cdnUrl = generateCDNUrl(objectKey);

// 5. 保存文件元数据
FileMetadata fileMetadata = FileMetadata.builder()
.fileName(fileName)
.originalName(file.getOriginalFilename())
.objectKey(objectKey)
.fileSize(file.getSize())
.contentType(file.getContentType())
.accessUrl(accessUrl)
.cdnUrl(cdnUrl)
.uploadTime(LocalDateTime.now())
.status("UPLOADED")
.build();

fileMetadataService.saveFileMetadata(fileMetadata);

log.info("文件上传成功: fileName={}, objectKey={}, size={}",
fileName, objectKey, file.getSize());

return FileUploadResult.success(fileMetadata);

} catch (Exception e) {
log.error("文件上传失败: fileName={}", file.getOriginalFilename(), e);
return FileUploadResult.error("文件上传失败: " + e.getMessage());
}
}

/**
* 批量上传文件
* @param files 文件列表
* @param path 存储路径
* @return 上传结果列表
*/
public List<FileUploadResult> uploadFiles(List<MultipartFile> files, String path) {
List<FileUploadResult> results = new ArrayList<>();

for (MultipartFile file : files) {
FileUploadResult result = uploadFile(file, path);
results.add(result);
}

return results;
}

/**
* 上传文件(Base64)
* @param base64Data Base64数据
* @param fileName 文件名
* @param path 存储路径
* @return 上传结果
*/
public FileUploadResult uploadBase64File(String base64Data, String fileName, String path) {
try {
// 解码Base64数据
byte[] fileData = Base64.getDecoder().decode(base64Data);

// 生成文件名
String generatedFileName = generateFileName(fileName);
String objectKey = buildObjectKey(path, generatedFileName);

// 上传到OSS
PutObjectRequest putObjectRequest = new PutObjectRequest(
ossProperties.getBucketName(),
objectKey,
new ByteArrayInputStream(fileData)
);

// 设置元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(fileData.length);
metadata.setContentType(getContentType(fileName));

putObjectRequest.setMetadata(metadata);

// 执行上传
ossClient.putObject(putObjectRequest);

// 生成访问链接
String accessUrl = generateAccessUrl(objectKey);
String cdnUrl = generateCDNUrl(objectKey);

// 保存文件元数据
FileMetadata fileMetadata = FileMetadata.builder()
.fileName(generatedFileName)
.originalName(fileName)
.objectKey(objectKey)
.fileSize((long) fileData.length)
.contentType(getContentType(fileName))
.accessUrl(accessUrl)
.cdnUrl(cdnUrl)
.uploadTime(LocalDateTime.now())
.status("UPLOADED")
.build();

fileMetadataService.saveFileMetadata(fileMetadata);

return FileUploadResult.success(fileMetadata);

} catch (Exception e) {
log.error("Base64文件上传失败: fileName={}", fileName, e);
return FileUploadResult.error("Base64文件上传失败: " + e.getMessage());
}
}

/**
* 验证文件
* @param file 文件
*/
private void validateFile(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("文件不能为空");
}

// 检查文件大小
long maxSize = 100 * 1024 * 1024; // 100MB
if (file.getSize() > maxSize) {
throw new IllegalArgumentException("文件大小不能超过100MB");
}

// 检查文件类型
String originalName = file.getOriginalFilename();
if (originalName == null || !isAllowedFileType(originalName)) {
throw new IllegalArgumentException("不支持的文件类型");
}
}

/**
* 检查文件类型是否允许
* @param fileName 文件名
* @return 是否允许
*/
private boolean isAllowedFileType(String fileName) {
String[] allowedTypes = {"jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx"};
String extension = getFileExtension(fileName).toLowerCase();

return Arrays.asList(allowedTypes).contains(extension);
}

/**
* 获取文件扩展名
* @param fileName 文件名
* @return 扩展名
*/
private String getFileExtension(String fileName) {
int lastDotIndex = fileName.lastIndexOf(".");
return lastDotIndex > 0 ? fileName.substring(lastDotIndex + 1) : "";
}

/**
* 生成文件名
* @param originalName 原始文件名
* @return 生成的文件名
*/
private String generateFileName(String originalName) {
String extension = getFileExtension(originalName);
String timestamp = String.valueOf(System.currentTimeMillis());
String uuid = UUID.randomUUID().toString().substring(0, 8);

return timestamp + "_" + uuid + "." + extension;
}

/**
* 构建对象键
* @param path 路径
* @param fileName 文件名
* @return 对象键
*/
private String buildObjectKey(String path, String fileName) {
if (StringUtils.hasText(path)) {
return path.endsWith("/") ? path + fileName : path + "/" + fileName;
} else {
return ossProperties.getDefaultPath() + fileName;
}
}

/**
* 生成访问链接
* @param objectKey 对象键
* @return 访问链接
*/
private String generateAccessUrl(String objectKey) {
return "https://" + ossProperties.getBucketName() + "." +
ossProperties.getEndpoint().replace("https://", "") + "/" + objectKey;
}

/**
* 生成CDN链接
* @param objectKey 对象键
* @return CDN链接
*/
private String generateCDNUrl(String objectKey) {
if (StringUtils.hasText(ossProperties.getCdnDomain())) {
return ossProperties.getCdnDomain() + "/" + objectKey;
}
return generateAccessUrl(objectKey);
}

/**
* 获取内容类型
* @param fileName 文件名
* @return 内容类型
*/
private String getContentType(String fileName) {
String extension = getFileExtension(fileName).toLowerCase();

switch (extension) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "pdf":
return "application/pdf";
case "doc":
return "application/msword";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "xls":
return "application/vnd.ms-excel";
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "ppt":
return "application/vnd.ms-powerpoint";
case "pptx":
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
default:
return "application/octet-stream";
}
}
}

/**
* 文件上传结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FileUploadResult {
private boolean success;
private FileMetadata fileMetadata;
private String errorMessage;

public static FileUploadResult success(FileMetadata fileMetadata) {
return new FileUploadResult(true, fileMetadata, null);
}

public static FileUploadResult error(String errorMessage) {
return new FileUploadResult(false, null, errorMessage);
}
}

/**
* 文件元数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileMetadata {
private Long id;
private String fileName;
private String originalName;
private String objectKey;
private Long fileSize;
private String contentType;
private String accessUrl;
private String cdnUrl;
private LocalDateTime uploadTime;
private String status;
private String description;
}

4. 自动转链服务

4.1 自动转链服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* 自动转链服务
*/
@Service
public class AutoLinkService {

@Autowired
private OSS ossClient;

@Autowired
private OSSProperties ossProperties;

@Autowired
private FileMetadataService fileMetadataService;

/**
* 生成预签名URL
* @param objectKey 对象键
* @param expiration 过期时间(小时)
* @return 预签名URL
*/
public String generatePresignedUrl(String objectKey, int expiration) {
try {
// 设置过期时间
Date expirationDate = new Date(System.currentTimeMillis() + expiration * 3600 * 1000);

// 生成预签名URL
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(
ossProperties.getBucketName(),
objectKey,
HttpMethod.GET
);
request.setExpiration(expirationDate);

URL presignedUrl = ossClient.generatePresignedUrl(request);

log.info("生成预签名URL成功: objectKey={}, expiration={}h", objectKey, expiration);

return presignedUrl.toString();

} catch (Exception e) {
log.error("生成预签名URL失败: objectKey={}", objectKey, e);
throw new RuntimeException("生成预签名URL失败", e);
}
}

/**
* 生成CDN加速链接
* @param objectKey 对象键
* @return CDN链接
*/
public String generateCDNLink(String objectKey) {
try {
if (!StringUtils.hasText(ossProperties.getCdnDomain())) {
return generateDirectLink(objectKey);
}

String cdnUrl = ossProperties.getCdnDomain() + "/" + objectKey;

log.info("生成CDN链接成功: objectKey={}, cdnUrl={}", objectKey, cdnUrl);

return cdnUrl;

} catch (Exception e) {
log.error("生成CDN链接失败: objectKey={}", objectKey, e);
throw new RuntimeException("生成CDN链接失败", e);
}
}

/**
* 生成直链
* @param objectKey 对象键
* @return 直链
*/
public String generateDirectLink(String objectKey) {
try {
String directUrl = "https://" + ossProperties.getBucketName() + "." +
ossProperties.getEndpoint().replace("https://", "") + "/" + objectKey;

log.info("生成直链成功: objectKey={}, directUrl={}", objectKey, directUrl);

return directUrl;

} catch (Exception e) {
log.error("生成直链失败: objectKey={}", objectKey, e);
throw new RuntimeException("生成直链失败", e);
}
}

/**
* 批量生成链接
* @param objectKeys 对象键列表
* @param linkType 链接类型
* @return 链接映射
*/
public Map<String, String> generateBatchLinks(List<String> objectKeys, String linkType) {
Map<String, String> linkMap = new HashMap<>();

for (String objectKey : objectKeys) {
try {
String link = generateLinkByType(objectKey, linkType);
linkMap.put(objectKey, link);
} catch (Exception e) {
log.error("生成链接失败: objectKey={}, linkType={}", objectKey, linkType, e);
}
}

return linkMap;
}

/**
* 根据类型生成链接
* @param objectKey 对象键
* @param linkType 链接类型
* @return 链接
*/
private String generateLinkByType(String objectKey, String linkType) {
switch (linkType) {
case "CDN":
return generateCDNLink(objectKey);
case "DIRECT":
return generateDirectLink(objectKey);
case "PRESIGNED":
return generatePresignedUrl(objectKey, 24);
default:
return generateCDNLink(objectKey);
}
}

/**
* 更新文件链接
* @param fileId 文件ID
* @param linkType 链接类型
* @return 更新结果
*/
public boolean updateFileLink(Long fileId, String linkType) {
try {
FileMetadata fileMetadata = fileMetadataService.getFileMetadata(fileId);
if (fileMetadata == null) {
log.warn("文件不存在: fileId={}", fileId);
return false;
}

String newLink = generateLinkByType(fileMetadata.getObjectKey(), linkType);

// 更新文件元数据
fileMetadata.setCdnUrl(newLink);
fileMetadataService.updateFileMetadata(fileMetadata);

log.info("更新文件链接成功: fileId={}, linkType={}", fileId, linkType);

return true;

} catch (Exception e) {
log.error("更新文件链接失败: fileId={}, linkType={}", fileId, linkType, e);
return false;
}
}
}

4.2 文件元数据服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
* 文件元数据服务
*/
@Service
public class FileMetadataService {

@Autowired
private FileMetadataRepository fileMetadataRepository;

/**
* 保存文件元数据
* @param fileMetadata 文件元数据
* @return 保存结果
*/
public FileMetadata saveFileMetadata(FileMetadata fileMetadata) {
try {
return fileMetadataRepository.save(fileMetadata);
} catch (Exception e) {
log.error("保存文件元数据失败: fileName={}", fileMetadata.getFileName(), e);
throw new RuntimeException("保存文件元数据失败", e);
}
}

/**
* 获取文件元数据
* @param fileId 文件ID
* @return 文件元数据
*/
public FileMetadata getFileMetadata(Long fileId) {
try {
return fileMetadataRepository.findById(fileId).orElse(null);
} catch (Exception e) {
log.error("获取文件元数据失败: fileId={}", fileId, e);
return null;
}
}

/**
* 根据对象键获取文件元数据
* @param objectKey 对象键
* @return 文件元数据
*/
public FileMetadata getFileMetadataByObjectKey(String objectKey) {
try {
return fileMetadataRepository.findByObjectKey(objectKey);
} catch (Exception e) {
log.error("根据对象键获取文件元数据失败: objectKey={}", objectKey, e);
return null;
}
}

/**
* 更新文件元数据
* @param fileMetadata 文件元数据
* @return 更新结果
*/
public FileMetadata updateFileMetadata(FileMetadata fileMetadata) {
try {
return fileMetadataRepository.save(fileMetadata);
} catch (Exception e) {
log.error("更新文件元数据失败: fileId={}", fileMetadata.getId(), e);
throw new RuntimeException("更新文件元数据失败", e);
}
}

/**
* 删除文件元数据
* @param fileId 文件ID
* @return 删除结果
*/
public boolean deleteFileMetadata(Long fileId) {
try {
fileMetadataRepository.deleteById(fileId);
return true;
} catch (Exception e) {
log.error("删除文件元数据失败: fileId={}", fileId, e);
return false;
}
}

/**
* 获取文件列表
* @param pageable 分页参数
* @return 文件列表
*/
public Page<FileMetadata> getFileList(Pageable pageable) {
try {
return fileMetadataRepository.findAll(pageable);
} catch (Exception e) {
log.error("获取文件列表失败", e);
return Page.empty();
}
}

/**
* 根据状态获取文件列表
* @param status 状态
* @param pageable 分页参数
* @return 文件列表
*/
public Page<FileMetadata> getFileListByStatus(String status, Pageable pageable) {
try {
return fileMetadataRepository.findByStatus(status, pageable);
} catch (Exception e) {
log.error("根据状态获取文件列表失败: status={}", status, e);
return Page.empty();
}
}
}

/**
* 文件元数据仓库
*/
@Repository
public interface FileMetadataRepository extends JpaRepository<FileMetadata, Long> {

/**
* 根据对象键查找文件元数据
* @param objectKey 对象键
* @return 文件元数据
*/
FileMetadata findByObjectKey(String objectKey);

/**
* 根据状态查找文件列表
* @param status 状态
* @param pageable 分页参数
* @return 文件列表
*/
Page<FileMetadata> findByStatus(String status, Pageable pageable);

/**
* 根据文件名模糊查找
* @param fileName 文件名
* @param pageable 分页参数
* @return 文件列表
*/
Page<FileMetadata> findByFileNameContaining(String fileName, Pageable pageable);

/**
* 根据上传时间范围查找
* @param startTime 开始时间
* @param endTime 结束时间
* @param pageable 分页参数
* @return 文件列表
*/
Page<FileMetadata> findByUploadTimeBetween(LocalDateTime startTime, LocalDateTime endTime, Pageable pageable);
}

5. 文件管理控制器

5.1 文件上传控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/**
* 文件上传控制器
*/
@RestController
@RequestMapping("/api/files")
public class FileUploadController {

@Autowired
private OSSFileUploadService ossFileUploadService;

@Autowired
private AutoLinkService autoLinkService;

/**
* 上传单个文件
*/
@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "path", required = false) String path) {

try {
FileUploadResult result = ossFileUploadService.uploadFile(file, path);

if (result.isSuccess()) {
FileUploadResponse response = FileUploadResponse.builder()
.success(true)
.fileId(result.getFileMetadata().getId())
.fileName(result.getFileMetadata().getFileName())
.originalName(result.getFileMetadata().getOriginalName())
.fileSize(result.getFileMetadata().getFileSize())
.contentType(result.getFileMetadata().getContentType())
.accessUrl(result.getFileMetadata().getAccessUrl())
.cdnUrl(result.getFileMetadata().getCdnUrl())
.uploadTime(result.getFileMetadata().getUploadTime())
.build();

return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(FileUploadResponse.error(result.getErrorMessage()));
}

} catch (Exception e) {
log.error("文件上传失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(FileUploadResponse.error("文件上传失败: " + e.getMessage()));
}
}

/**
* 批量上传文件
*/
@PostMapping("/upload/batch")
public ResponseEntity<List<FileUploadResponse>> uploadFiles(
@RequestParam("files") List<MultipartFile> files,
@RequestParam(value = "path", required = false) String path) {

try {
List<FileUploadResult> results = ossFileUploadService.uploadFiles(files, path);

List<FileUploadResponse> responses = results.stream()
.map(result -> {
if (result.isSuccess()) {
return FileUploadResponse.builder()
.success(true)
.fileId(result.getFileMetadata().getId())
.fileName(result.getFileMetadata().getFileName())
.originalName(result.getFileMetadata().getOriginalName())
.fileSize(result.getFileMetadata().getFileSize())
.contentType(result.getFileMetadata().getContentType())
.accessUrl(result.getFileMetadata().getAccessUrl())
.cdnUrl(result.getFileMetadata().getCdnUrl())
.uploadTime(result.getFileMetadata().getUploadTime())
.build();
} else {
return FileUploadResponse.error(result.getErrorMessage());
}
})
.collect(Collectors.toList());

return ResponseEntity.ok(responses);

} catch (Exception e) {
log.error("批量文件上传失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Collections.singletonList(FileUploadResponse.error("批量文件上传失败: " + e.getMessage())));
}
}

/**
* 上传Base64文件
*/
@PostMapping("/upload/base64")
public ResponseEntity<FileUploadResponse> uploadBase64File(
@RequestBody Base64UploadRequest request) {

try {
FileUploadResult result = ossFileUploadService.uploadBase64File(
request.getBase64Data(),
request.getFileName(),
request.getPath()
);

if (result.isSuccess()) {
FileUploadResponse response = FileUploadResponse.builder()
.success(true)
.fileId(result.getFileMetadata().getId())
.fileName(result.getFileMetadata().getFileName())
.originalName(result.getFileMetadata().getOriginalName())
.fileSize(result.getFileMetadata().getFileSize())
.contentType(result.getFileMetadata().getContentType())
.accessUrl(result.getFileMetadata().getAccessUrl())
.cdnUrl(result.getFileMetadata().getCdnUrl())
.uploadTime(result.getFileMetadata().getUploadTime())
.build();

return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(FileUploadResponse.error(result.getErrorMessage()));
}

} catch (Exception e) {
log.error("Base64文件上传失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(FileUploadResponse.error("Base64文件上传失败: " + e.getMessage()));
}
}

/**
* 生成文件链接
*/
@PostMapping("/generate-link")
public ResponseEntity<LinkGenerationResponse> generateLink(
@RequestBody LinkGenerationRequest request) {

try {
String link = autoLinkService.generateLinkByType(request.getObjectKey(), request.getLinkType());

LinkGenerationResponse response = LinkGenerationResponse.builder()
.success(true)
.objectKey(request.getObjectKey())
.linkType(request.getLinkType())
.link(link)
.generatedTime(LocalDateTime.now())
.build();

return ResponseEntity.ok(response);

} catch (Exception e) {
log.error("生成文件链接失败: objectKey={}, linkType={}",
request.getObjectKey(), request.getLinkType(), e);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(LinkGenerationResponse.error("生成文件链接失败: " + e.getMessage()));
}
}
}

/**
* 文件上传响应
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileUploadResponse {
private boolean success;
private Long fileId;
private String fileName;
private String originalName;
private Long fileSize;
private String contentType;
private String accessUrl;
private String cdnUrl;
private LocalDateTime uploadTime;
private String errorMessage;

public static FileUploadResponse error(String errorMessage) {
return FileUploadResponse.builder()
.success(false)
.errorMessage(errorMessage)
.build();
}
}

/**
* Base64上传请求
*/
@Data
public class Base64UploadRequest {
private String base64Data;
private String fileName;
private String path;
}

/**
* 链接生成请求
*/
@Data
public class LinkGenerationRequest {
private String objectKey;
private String linkType;
}

/**
* 链接生成响应
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LinkGenerationResponse {
private boolean success;
private String objectKey;
private String linkType;
private String link;
private LocalDateTime generatedTime;
private String errorMessage;

public static LinkGenerationResponse error(String errorMessage) {
return LinkGenerationResponse.builder()
.success(false)
.errorMessage(errorMessage)
.build();
}
}

6. 总结

通过OSS自动转链上传的实现,我们成功构建了一个完整的文件存储和管理系统。关键特性包括:

6.1 核心优势

  1. 文件上传: 支持多种文件格式和上传方式
  2. 自动转链: 自动生成CDN加速链接
  3. 权限控制: 灵活的访问权限管理
  4. 批量处理: 支持批量文件操作
  5. 监控告警: 文件上传和访问监控

6.2 最佳实践

  1. 文件管理: 完善的文件元数据管理
  2. 链接生成: 多种链接类型支持
  3. 错误处理: 完善的异常处理机制
  4. 性能优化: CDN加速和缓存策略
  5. 安全控制: 文件类型验证和大小限制

这套OSS自动转链方案不仅能够提供高效的文件存储服务,还为文件访问提供了CDN加速支持,是现代Web应用的重要基础设施。