@llryiop/avatar-boot-cli 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/docs/exam-question-generate-api.md +163 -0
  2. package/package.json +1 -1
  3. package/src/prompts.js +3 -3
  4. package/src/transform.js +1 -1
  5. package/templates/.claude/skills/avatar-boot-starter-feign/README.md +243 -0
  6. package/templates/.claude/skills/avatar-boot-starter-feign/SKILL.md +47 -219
  7. package/templates/.claude/skills/avatar-boot-starter-feign/references//345/212/237/350/203/275/350/257/246/350/247/243.md +65 -0
  8. package/templates/.claude/skills/avatar-boot-starter-feign/references//345/277/253/351/200/237/346/216/245/345/205/245/346/214/207/345/215/227.md +75 -0
  9. package/templates/.claude/skills/avatar-boot-starter-feign/references//351/205/215/347/275/256/345/217/202/350/200/203.md +70 -0
  10. package/templates/.claude/skills/avatar-boot-starter-job/README.md +437 -0
  11. package/templates/.claude/skills/avatar-boot-starter-job/SKILL.md +35 -414
  12. package/templates/.claude/skills/avatar-boot-starter-job/references//345/270/270/350/247/201/351/227/256/351/242/230.md +55 -0
  13. package/templates/.claude/skills/avatar-boot-starter-job/references//345/277/253/351/200/237/346/216/245/345/205/245/344/270/216/351/205/215/347/275/256.md +124 -0
  14. package/templates/.claude/skills/avatar-boot-starter-job/references//347/233/221/346/216/247/346/214/207/346/240/207.md +72 -0
  15. package/templates/.claude/skills/avatar-boot-starter-kafka/README.md +580 -0
  16. package/templates/.claude/skills/avatar-boot-starter-kafka/SKILL.md +36 -560
  17. package/templates/.claude/skills/avatar-boot-starter-kafka/references//346/234/200/344/275/263/345/256/236/350/267/265.md +43 -0
  18. package/templates/.claude/skills/avatar-boot-starter-kafka/references//346/240/270/345/277/203/345/212/237/350/203/275.md +117 -0
  19. package/templates/.claude/skills/avatar-boot-starter-kafka/references//351/205/215/347/275/256/345/217/202/350/200/203.md +54 -0
  20. package/templates/.claude/skills/avatar-boot-starter-mysql/README.md +572 -0
  21. package/templates/.claude/skills/avatar-boot-starter-mysql/SKILL.md +40 -550
  22. package/templates/.claude/skills/avatar-boot-starter-mysql/references//345/256/236/344/275/223/344/270/216/345/212/237/350/203/275.md +96 -0
  23. package/templates/.claude/skills/avatar-boot-starter-mysql/references//345/277/253/351/200/237/346/216/245/345/205/245/344/270/216/346/225/260/346/215/256/346/272/220.md +91 -0
  24. package/templates/.claude/skills/avatar-boot-starter-mysql/references//351/253/230/347/272/247/347/211/271/346/200/247/344/270/216/351/205/215/347/275/256.md +59 -0
  25. package/templates/.claude/skills/avatar-boot-starter-nacos/README.md +901 -0
  26. package/templates/.claude/skills/avatar-boot-starter-nacos/SKILL.md +40 -879
  27. package/templates/.claude/skills/avatar-boot-starter-nacos/references//345/212/237/350/203/275/344/275/277/347/224/250.md +134 -0
  28. package/templates/.claude/skills/avatar-boot-starter-nacos/references//345/277/253/351/200/237/346/216/245/345/205/245/344/270/216/351/205/215/347/275/256.md +96 -0
  29. package/templates/.claude/skills/avatar-boot-starter-nacos/references//346/225/205/351/232/234/346/216/222/346/237/245.md +64 -0
  30. package/templates/.claude/skills/avatar-boot-starter-oss/README.md +594 -0
  31. package/templates/.claude/skills/avatar-boot-starter-oss/SKILL.md +52 -570
  32. package/templates/.claude/skills/avatar-boot-starter-oss/references//345/277/253/351/200/237/346/216/245/345/205/245/344/270/216/351/205/215/347/275/256.md +77 -0
  33. package/templates/.claude/skills/avatar-boot-starter-oss/references//346/240/270/345/277/203/345/212/237/350/203/275.md +94 -0
  34. package/templates/.claude/skills/avatar-boot-starter-oss/references//350/247/204/350/214/203/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.md +61 -0
  35. package/templates/.claude/skills/avatar-boot-starter-redis/README.md +586 -0
  36. package/templates/.claude/skills/avatar-boot-starter-redis/SKILL.md +42 -566
  37. package/templates/.claude/skills/avatar-boot-starter-redis/references//345/277/253/351/200/237/346/216/245/345/205/245/344/270/216/351/205/215/347/275/256.md +78 -0
  38. package/templates/.claude/skills/avatar-boot-starter-redis/references//346/225/260/346/215/256/346/223/215/344/275/234.md +111 -0
  39. package/templates/.claude/skills/avatar-boot-starter-redis/references//351/253/230/347/272/247/345/212/237/350/203/275.md +90 -0
  40. package/templates/.claude/skills/avatar-boot-starter-rocketmq/README.md +662 -0
  41. package/templates/.claude/skills/avatar-boot-starter-rocketmq/SKILL.md +48 -640
  42. package/templates/.claude/skills/avatar-boot-starter-rocketmq/references//346/240/270/345/277/203/345/212/237/350/203/275.md +101 -0
  43. package/templates/.claude/skills/avatar-boot-starter-rocketmq/references//351/205/215/347/275/256/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.md +44 -0
  44. package/templates/.claude/skills/avatar-boot-starter-rocketmq/references//351/253/230/347/272/247/347/211/271/346/200/247.md +71 -0
  45. package/templates/.claude/skills/avatar-boot-starter-web/README.md +1007 -0
  46. package/templates/.claude/skills/avatar-boot-starter-web/SKILL.md +150 -1003
  47. package/templates/.claude/skills/avatar-boot-starter-web/references//345/212/237/350/203/275-LogInfo/346/263/250/350/247/243.md +75 -0
  48. package/templates/.claude/skills/avatar-boot-starter-web/references//345/212/237/350/203/275-/345/205/250/345/261/200/345/274/202/345/270/270/345/244/204/347/220/206.md +90 -0
  49. package/templates/.claude/skills/avatar-boot-starter-web/references//345/212/237/350/203/275-/346/214/207/346/240/207/347/233/221/346/216/247.md +74 -0
  50. package/templates/.claude/skills/avatar-boot-starter-web/references//345/212/237/350/203/275-/346/227/245/345/277/227/344/275/223/347/263/273.md +73 -0
  51. package/templates/.claude/skills/avatar-boot-starter-web/references//345/212/237/350/203/275-/350/257/267/346/261/202/344/270/212/344/270/213/346/226/207.md +77 -0
  52. package/templates/.claude/skills/avatar-boot-starter-web/references//345/277/253/351/200/237/346/216/245/345/205/245/346/214/207/345/215/227.md +52 -0
  53. package/templates/.claude/skills/avatar-boot-starter-web/references//346/263/250/346/204/217/344/272/213/351/241/271.md +68 -0
  54. package/templates/.claude/skills/avatar-boot-starter-web/references//350/207/252/345/256/232/344/271/211/346/211/251/345/261/225/346/214/207/345/215/227.md +107 -0
  55. package/templates/.claude/skills/avatar-boot-starter-web/references//351/205/215/347/275/256/345/217/202/350/200/203.md +107 -0
  56. package/templates/.claude/skills/crud-generator/SKILL.md +133 -64
  57. package/templates/.claude/skills/database-design/README.md +207 -0
  58. package/templates/.claude/skills/database-design/SKILL.md +469 -82
  59. package/templates/.claude/skills/database-design/references//345/221/275/345/220/215/350/247/204/350/214/203.md +232 -0
  60. package/templates/.claude/skills/database-design/references//345/255/227/346/256/265/347/261/273/345/236/213/350/247/204/350/214/203.md +400 -0
  61. package/templates/.claude/skills/database-design/references//347/264/242/345/274/225/350/247/204/350/214/203.md +506 -0
  62. package/templates/avatar-scaffold-api/pom.xml +0 -5
  63. package/templates/avatar-scaffold-service/pom.xml +25 -87
  64. package/templates/avatar-scaffold-service/src/main/resources/application-dev.yaml +3 -5
  65. package/templates/avatar-scaffold-service/src/main/resources/application-local.yaml +2 -2
  66. package/templates/pom.xml +9 -18
@@ -0,0 +1,594 @@
1
+ ---
2
+ name: avatar-boot-starter-oss
3
+ description: 当涉及 OSS、对象存储、文件上传、文件下载、阿里云存储 相关功能时使用此技能 - 为 Spring Boot 3.5.3 应用提供基于阿里云 OSS SDK 3.18.1 的对象存储能力,包含文件上传下载、分片上传、签名 URL、STS 直传。
4
+ ---
5
+
6
+ Avatar Boot 的阿里云 OSS 对象存储集成模块,基于 aliyun-sdk-oss 3.18.1 提供开箱即用的文件上传、下载和管理能力。
7
+
8
+ ## 功能特性
9
+
10
+ - ✅ **自动配置** - 基于 Spring Boot 自动配置机制,自动装配 OSSClient
11
+ - ✅ **简单上传** - 支持 putObject 方式上传文件和流
12
+ - ✅ **分片上传** - 支持大文件分片上传,自动管理分片生命周期
13
+ - ✅ **签名 URL** - 生成预签名 URL,支持临时授权访问
14
+ - ✅ **STS 直传** - 支持客户端直传模式,减轻服务端带宽压力
15
+ - ✅ **生命周期管理** - 支持配置文件自动过期和存储类型转换
16
+
17
+ ## 快速开始
18
+
19
+ ### 1. 添加依赖
20
+
21
+ 在项目的 `pom.xml` 中添加依赖:
22
+
23
+ ```xml
24
+ <dependency>
25
+ <groupId>com.iflytek.avatar.boot</groupId>
26
+ <artifactId>avatar-boot-starter-oss</artifactId>
27
+ </dependency>
28
+ ```
29
+
30
+ > 版本由 Avatar Boot BOM 统一管理,无需指定 version。内置 aliyun-sdk-oss 3.18.1。
31
+
32
+ ### 2. 配置文件
33
+
34
+ 在 `application.yml` 中添加 OSS 配置:
35
+
36
+ ```yaml
37
+ aliyun:
38
+ oss:
39
+ endpoint: https://oss-cn-shanghai.aliyuncs.com # OSS 端点
40
+ access-key-id: ${OSS_ACCESS_KEY_ID} # AccessKey ID(从环境变量读取)
41
+ access-key-secret: ${OSS_ACCESS_KEY_SECRET} # AccessKey Secret(从环境变量读取)
42
+ bucket-name: your-bucket-name # 默认 Bucket
43
+ ```
44
+
45
+ **安全提醒**:AccessKey 信息禁止硬编码在配置文件中,必须通过环境变量或密钥管理服务注入。
46
+
47
+ ### 3. OSSClient 配置类
48
+
49
+ ```java
50
+ package com.example.config;
51
+
52
+ import com.aliyun.oss.OSS;
53
+ import com.aliyun.oss.OSSClientBuilder;
54
+ import org.springframework.beans.factory.annotation.Value;
55
+ import org.springframework.context.annotation.Bean;
56
+ import org.springframework.context.annotation.Configuration;
57
+
58
+ @Configuration
59
+ public class OssConfig {
60
+
61
+ @Value("${aliyun.oss.endpoint}")
62
+ private String endpoint;
63
+
64
+ @Value("${aliyun.oss.access-key-id}")
65
+ private String accessKeyId;
66
+
67
+ @Value("${aliyun.oss.access-key-secret}")
68
+ private String accessKeySecret;
69
+
70
+ @Bean(destroyMethod = "shutdown")
71
+ public OSS ossClient() {
72
+ return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## 文件上传
78
+
79
+ ### 简单上传(putObject)
80
+
81
+ ```java
82
+ package com.example.service;
83
+
84
+ import com.aliyun.oss.OSS;
85
+ import com.aliyun.oss.model.ObjectMetadata;
86
+ import com.aliyun.oss.model.PutObjectResult;
87
+ import lombok.RequiredArgsConstructor;
88
+ import lombok.extern.slf4j.Slf4j;
89
+ import org.springframework.beans.factory.annotation.Value;
90
+ import org.springframework.stereotype.Service;
91
+ import org.springframework.web.multipart.MultipartFile;
92
+
93
+ import java.io.InputStream;
94
+ import java.time.LocalDate;
95
+ import java.time.format.DateTimeFormatter;
96
+ import java.util.UUID;
97
+
98
+ @Slf4j
99
+ @Service
100
+ @RequiredArgsConstructor
101
+ public class OssUploadService {
102
+
103
+ private final OSS ossClient;
104
+
105
+ @Value("${aliyun.oss.bucket-name}")
106
+ private String bucketName;
107
+
108
+ /**
109
+ * 上传文件
110
+ * @return 文件访问路径
111
+ */
112
+ public String uploadFile(MultipartFile file) {
113
+ // 生成文件路径:日期 + UUID
114
+ String objectKey = generateObjectKey(file.getOriginalFilename());
115
+
116
+ try (InputStream inputStream = file.getInputStream()) {
117
+ ObjectMetadata metadata = new ObjectMetadata();
118
+ metadata.setContentType(file.getContentType());
119
+ metadata.setContentLength(file.getSize());
120
+ // 设置自定义元数据
121
+ metadata.addUserMetadata("original-filename", file.getOriginalFilename());
122
+
123
+ PutObjectResult result = ossClient.putObject(bucketName, objectKey, inputStream, metadata);
124
+ log.info("文件上传成功: objectKey={}, etag={}", objectKey, result.getETag());
125
+
126
+ return objectKey;
127
+ } catch (Exception e) {
128
+ log.error("文件上传失败: filename={}", file.getOriginalFilename(), e);
129
+ throw new RuntimeException("文件上传失败", e);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * 上传字符串内容
135
+ */
136
+ public String uploadContent(String content, String fileName) {
137
+ String objectKey = generateObjectKey(fileName);
138
+ ossClient.putObject(bucketName, objectKey, new java.io.ByteArrayInputStream(content.getBytes()));
139
+ return objectKey;
140
+ }
141
+
142
+ /**
143
+ * 生成文件路径:按日期分目录 + UUID 文件名
144
+ * 格式:upload/2026/03/09/uuid.ext
145
+ */
146
+ private String generateObjectKey(String originalFilename) {
147
+ String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
148
+ String ext = "";
149
+ if (originalFilename != null && originalFilename.contains(".")) {
150
+ ext = originalFilename.substring(originalFilename.lastIndexOf("."));
151
+ }
152
+ return String.format("upload/%s/%s%s", datePath, UUID.randomUUID().toString().replace("-", ""), ext);
153
+ }
154
+ }
155
+ ```
156
+
157
+ ### 分片上传(Multipart Upload)
158
+
159
+ ```java
160
+ @Service
161
+ @RequiredArgsConstructor
162
+ public class MultipartUploadService {
163
+
164
+ private final OSS ossClient;
165
+
166
+ @Value("${aliyun.oss.bucket-name}")
167
+ private String bucketName;
168
+
169
+ /**
170
+ * 分片上传大文件
171
+ * @param file 文件
172
+ * @param partSize 分片大小(字节),建议 5MB-100MB
173
+ */
174
+ public String multipartUpload(MultipartFile file, long partSize) {
175
+ String objectKey = "upload/" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))
176
+ + "/" + UUID.randomUUID().toString().replace("-", "")
177
+ + getExtension(file.getOriginalFilename());
178
+
179
+ // 1. 初始化分片上传
180
+ InitiateMultipartUploadRequest initRequest =
181
+ new InitiateMultipartUploadRequest(bucketName, objectKey);
182
+ InitiateMultipartUploadResult initResult = ossClient.initiateMultipartUpload(initRequest);
183
+ String uploadId = initResult.getUploadId();
184
+ log.info("分片上传初始化: uploadId={}, objectKey={}", uploadId, objectKey);
185
+
186
+ try {
187
+ long fileSize = file.getSize();
188
+ int partCount = (int) Math.ceil((double) fileSize / partSize);
189
+ List<PartETag> partETags = new ArrayList<>();
190
+
191
+ InputStream inputStream = file.getInputStream();
192
+ for (int i = 0; i < partCount; i++) {
193
+ long startPos = i * partSize;
194
+ long curPartSize = Math.min(partSize, fileSize - startPos);
195
+
196
+ // 2. 上传分片
197
+ UploadPartRequest uploadPartRequest = new UploadPartRequest();
198
+ uploadPartRequest.setBucketName(bucketName);
199
+ uploadPartRequest.setKey(objectKey);
200
+ uploadPartRequest.setUploadId(uploadId);
201
+ uploadPartRequest.setInputStream(inputStream);
202
+ uploadPartRequest.setPartSize(curPartSize);
203
+ uploadPartRequest.setPartNumber(i + 1);
204
+
205
+ UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
206
+ partETags.add(uploadPartResult.getPartETag());
207
+ log.info("分片 {}/{} 上传成功", i + 1, partCount);
208
+ }
209
+
210
+ // 3. 完成分片上传
211
+ CompleteMultipartUploadRequest completeRequest =
212
+ new CompleteMultipartUploadRequest(bucketName, objectKey, uploadId, partETags);
213
+ ossClient.completeMultipartUpload(completeRequest);
214
+ log.info("分片上传完成: objectKey={}", objectKey);
215
+
216
+ return objectKey;
217
+ } catch (Exception e) {
218
+ // 取消分片上传,清理已上传的分片
219
+ ossClient.abortMultipartUpload(
220
+ new AbortMultipartUploadRequest(bucketName, objectKey, uploadId));
221
+ log.error("分片上传失败,已取消: uploadId={}", uploadId, e);
222
+ throw new RuntimeException("分片上传失败", e);
223
+ }
224
+ }
225
+
226
+ private String getExtension(String filename) {
227
+ if (filename != null && filename.contains(".")) {
228
+ return filename.substring(filename.lastIndexOf("."));
229
+ }
230
+ return "";
231
+ }
232
+ }
233
+ ```
234
+
235
+ ## 文件下载
236
+
237
+ ### 服务端下载
238
+
239
+ ```java
240
+ @Service
241
+ @RequiredArgsConstructor
242
+ public class OssDownloadService {
243
+
244
+ private final OSS ossClient;
245
+
246
+ @Value("${aliyun.oss.bucket-name}")
247
+ private String bucketName;
248
+
249
+ /**
250
+ * 下载文件到字节数组
251
+ */
252
+ public byte[] downloadFile(String objectKey) {
253
+ try {
254
+ OSSObject ossObject = ossClient.getObject(bucketName, objectKey);
255
+ try (InputStream inputStream = ossObject.getObjectContent()) {
256
+ return inputStream.readAllBytes();
257
+ }
258
+ } catch (Exception e) {
259
+ log.error("文件下载失败: objectKey={}", objectKey, e);
260
+ throw new RuntimeException("文件下载失败", e);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * 下载文件到 HttpServletResponse(浏览器下载)
266
+ */
267
+ public void downloadToResponse(String objectKey, String fileName,
268
+ HttpServletResponse response) {
269
+ try {
270
+ OSSObject ossObject = ossClient.getObject(bucketName, objectKey);
271
+ ObjectMetadata metadata = ossObject.getObjectMetadata();
272
+
273
+ response.setContentType(metadata.getContentType());
274
+ response.setContentLengthLong(metadata.getContentLength());
275
+ response.setHeader("Content-Disposition",
276
+ "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
277
+
278
+ try (InputStream in = ossObject.getObjectContent();
279
+ OutputStream out = response.getOutputStream()) {
280
+ in.transferTo(out);
281
+ out.flush();
282
+ }
283
+ } catch (Exception e) {
284
+ log.error("文件下载失败: objectKey={}", objectKey, e);
285
+ throw new RuntimeException("文件下载失败", e);
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+ ### 生成预签名 URL
292
+
293
+ ```java
294
+ @Service
295
+ @RequiredArgsConstructor
296
+ public class OssPresignedUrlService {
297
+
298
+ private final OSS ossClient;
299
+
300
+ @Value("${aliyun.oss.bucket-name}")
301
+ private String bucketName;
302
+
303
+ /**
304
+ * 生成下载预签名 URL(有效期 1 小时)
305
+ */
306
+ public String generateDownloadUrl(String objectKey) {
307
+ return generatePresignedUrl(objectKey, 3600, HttpMethod.GET);
308
+ }
309
+
310
+ /**
311
+ * 生成上传预签名 URL(有效期 10 分钟)
312
+ */
313
+ public String generateUploadUrl(String objectKey) {
314
+ return generatePresignedUrl(objectKey, 600, HttpMethod.PUT);
315
+ }
316
+
317
+ /**
318
+ * 生成预签名 URL
319
+ * @param expireSeconds 过期时间(秒)
320
+ */
321
+ public String generatePresignedUrl(String objectKey, int expireSeconds, HttpMethod method) {
322
+ Date expiration = new Date(System.currentTimeMillis() + expireSeconds * 1000L);
323
+
324
+ GeneratePresignedUrlRequest request =
325
+ new GeneratePresignedUrlRequest(bucketName, objectKey, method);
326
+ request.setExpiration(expiration);
327
+
328
+ URL url = ossClient.generatePresignedUrl(request);
329
+ log.info("生成预签名 URL: objectKey={}, expireSeconds={}", objectKey, expireSeconds);
330
+ return url.toString();
331
+ }
332
+ }
333
+ ```
334
+
335
+ ## 文件命名策略
336
+
337
+ **推荐的文件命名规则:**
338
+
339
+ ```
340
+ {业务类型}/{年}/{月}/{日}/{UUID}.{扩展名}
341
+ ```
342
+
343
+ | 场景 | 路径格式 | 示例 |
344
+ |------|---------|------|
345
+ | 用户头像 | `avatar/{yyyy}/{MM}/{dd}/{uuid}.jpg` | `avatar/2026/03/09/a1b2c3d4.jpg` |
346
+ | 文档上传 | `document/{yyyy}/{MM}/{dd}/{uuid}.pdf` | `document/2026/03/09/e5f6g7h8.pdf` |
347
+ | 临时文件 | `temp/{yyyy}/{MM}/{dd}/{uuid}.tmp` | `temp/2026/03/09/i9j0k1l2.tmp` |
348
+ | 导出文件 | `export/{yyyy}/{MM}/{dd}/{uuid}.xlsx` | `export/2026/03/09/m3n4o5p6.xlsx` |
349
+
350
+ **命名规范:**
351
+
352
+ 1. **使用 UUID 作为文件名**:避免文件名冲突和中文编码问题
353
+ 2. **按日期分目录**:便于管理和生命周期策略配置
354
+ 3. **保留原始扩展名**:便于浏览器识别文件类型
355
+ 4. **不使用原始文件名**:防止路径遍历攻击和特殊字符问题
356
+
357
+ ## 客户端直传(STS Token)
358
+
359
+ ### 获取 STS 临时凭证
360
+
361
+ ```java
362
+ package com.example.service;
363
+
364
+ import com.aliyuncs.DefaultAcsClient;
365
+ import com.aliyuncs.auth.sts.AssumeRoleRequest;
366
+ import com.aliyuncs.auth.sts.AssumeRoleResponse;
367
+ import com.aliyuncs.profile.DefaultProfile;
368
+ import lombok.extern.slf4j.Slf4j;
369
+ import org.springframework.beans.factory.annotation.Value;
370
+ import org.springframework.stereotype.Service;
371
+
372
+ @Slf4j
373
+ @Service
374
+ public class StsTokenService {
375
+
376
+ @Value("${aliyun.oss.access-key-id}")
377
+ private String accessKeyId;
378
+
379
+ @Value("${aliyun.oss.access-key-secret}")
380
+ private String accessKeySecret;
381
+
382
+ @Value("${aliyun.oss.sts.role-arn}")
383
+ private String roleArn;
384
+
385
+ @Value("${aliyun.oss.sts.region-id:cn-shanghai}")
386
+ private String regionId;
387
+
388
+ /**
389
+ * 获取 STS 临时凭证
390
+ * @param sessionName 会话名称
391
+ * @param durationSeconds 有效期(秒),最短 900,最长 3600
392
+ */
393
+ public StsCredentials getStsToken(String sessionName, long durationSeconds) {
394
+ try {
395
+ DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
396
+ DefaultAcsClient client = new DefaultAcsClient(profile);
397
+
398
+ AssumeRoleRequest request = new AssumeRoleRequest();
399
+ request.setRoleArn(roleArn);
400
+ request.setRoleSessionName(sessionName);
401
+ request.setDurationSeconds(durationSeconds);
402
+
403
+ // 限制上传路径的策略
404
+ String policy = """
405
+ {
406
+ "Version": "1",
407
+ "Statement": [
408
+ {
409
+ "Effect": "Allow",
410
+ "Action": ["oss:PutObject"],
411
+ "Resource": ["acs:oss:*:*:your-bucket-name/upload/*"]
412
+ }
413
+ ]
414
+ }
415
+ """;
416
+ request.setPolicy(policy);
417
+
418
+ AssumeRoleResponse response = client.getAcsResponse(request);
419
+ AssumeRoleResponse.Credentials credentials = response.getCredentials();
420
+
421
+ return new StsCredentials(
422
+ credentials.getAccessKeyId(),
423
+ credentials.getAccessKeySecret(),
424
+ credentials.getSecurityToken(),
425
+ credentials.getExpiration()
426
+ );
427
+ } catch (Exception e) {
428
+ log.error("获取 STS Token 失败", e);
429
+ throw new RuntimeException("获取 STS Token 失败", e);
430
+ }
431
+ }
432
+ }
433
+ ```
434
+
435
+ ### 前端直传接口
436
+
437
+ ```java
438
+ @RestController
439
+ @RequestMapping("/api/oss")
440
+ @RequiredArgsConstructor
441
+ public class OssController {
442
+
443
+ private final StsTokenService stsTokenService;
444
+
445
+ @Value("${aliyun.oss.endpoint}")
446
+ private String endpoint;
447
+
448
+ @Value("${aliyun.oss.bucket-name}")
449
+ private String bucketName;
450
+
451
+ /**
452
+ * 获取前端直传凭证
453
+ */
454
+ @GetMapping("/sts-token")
455
+ public Result<Map<String, Object>> getStsToken() {
456
+ StsCredentials credentials = stsTokenService.getStsToken("web-upload", 900);
457
+
458
+ Map<String, Object> data = new HashMap<>();
459
+ data.put("accessKeyId", credentials.getAccessKeyId());
460
+ data.put("accessKeySecret", credentials.getAccessKeySecret());
461
+ data.put("securityToken", credentials.getSecurityToken());
462
+ data.put("expiration", credentials.getExpiration());
463
+ data.put("endpoint", endpoint);
464
+ data.put("bucket", bucketName);
465
+ data.put("dir", "upload/" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/");
466
+
467
+ return Result.success(data);
468
+ }
469
+ }
470
+ ```
471
+
472
+ ## 生命周期管理
473
+
474
+ ### 配置文件自动过期
475
+
476
+ ```java
477
+ @Service
478
+ @RequiredArgsConstructor
479
+ public class OssLifecycleService {
480
+
481
+ private final OSS ossClient;
482
+
483
+ @Value("${aliyun.oss.bucket-name}")
484
+ private String bucketName;
485
+
486
+ /**
487
+ * 配置临时文件 7 天后自动删除
488
+ */
489
+ public void setupTempFileLifecycle() {
490
+ SetBucketLifecycleRequest request = new SetBucketLifecycleRequest(bucketName);
491
+
492
+ LifecycleRule rule = new LifecycleRule();
493
+ rule.setId("auto-delete-temp");
494
+ rule.setPrefix("temp/");
495
+ rule.setStatus(LifecycleRule.RuleStatus.Enabled);
496
+ rule.setExpirationDays(7); // 7 天后删除
497
+
498
+ request.setLifecycleRules(List.of(rule));
499
+ ossClient.setBucketLifecycle(request);
500
+ log.info("临时文件生命周期规则已配置:7 天后自动删除");
501
+ }
502
+ }
503
+ ```
504
+
505
+ ## 最佳实践
506
+
507
+ ### 1. 安全
508
+
509
+ - **AccessKey 安全**:禁止硬编码,使用环境变量或密钥管理服务
510
+ - **最小权限**:STS Token 限制具体的 Bucket 和路径
511
+ - **Bucket 权限**:设为私有(private),通过签名 URL 访问
512
+ - **上传校验**:校验文件类型和大小,防止恶意文件上传
513
+
514
+ ### 2. 性能
515
+
516
+ - **大文件分片上传**:超过 100MB 的文件使用分片上传
517
+ - **客户端直传**:使用 STS Token 方式,减轻服务端带宽压力
518
+ - **CDN 加速**:静态资源配合 CDN 使用,降低 OSS 流量成本
519
+ - **OSSClient 复用**:OSSClient 是线程安全的,全局复用,不要每次 new
520
+
521
+ ### 3. 可靠性
522
+
523
+ - **异常处理**:捕获 OSSException 和 ClientException 分别处理
524
+ - **分片上传清理**:失败时调用 abortMultipartUpload 清理碎片
525
+ - **签名 URL 有效期**:下载 URL 1 小时,上传 URL 10 分钟
526
+ - **文件存在性检查**:下载前使用 `doesObjectExist()` 检查
527
+
528
+ ### 4. 成本
529
+
530
+ - **生命周期管理**:临时文件和日志设置自动过期
531
+ - **存储类型**:不常访问的文件转为低频或归档存储
532
+ - **签名 URL 替代公开访问**:避免 Bucket 设为 public-read
533
+
534
+ ## 常见问题
535
+
536
+ ### 1. AccessDenied(权限拒绝)
537
+
538
+ **原因**:AccessKey 权限不足或 Bucket 策略限制
539
+
540
+ **解决**:
541
+ - 检查 AccessKey 是否有 OSS 操作权限
542
+ - 检查 Bucket Policy 和 ACL 配置
543
+ - STS Token 检查 Policy 中的 Resource 和 Action 是否匹配
544
+ - 确认 AccessKey 未被禁用或过期
545
+
546
+ ### 2. 上传超时
547
+
548
+ **原因**:文件过大或网络不稳定
549
+
550
+ **解决**:
551
+ - 大文件(>100MB)使用分片上传
552
+ - 增大 OSSClient 超时配置
553
+ - 检查网络带宽和稳定性
554
+ - 使用客户端直传减轻服务端压力
555
+
556
+ ### 3. 签名 URL 无法访问
557
+
558
+ **原因**:URL 已过期或签名方式不匹配
559
+
560
+ **解决**:
561
+ - 检查 URL 过期时间设置
562
+ - 确保生成 URL 时的 endpoint 与实际访问一致
563
+ - 检查是否使用了 HTTPS(endpoint 和访问方式要一致)
564
+ - 验证 Bucket 名称和 objectKey 是否正确
565
+
566
+ ### 4. 分片上传碎片清理
567
+
568
+ **原因**:分片上传未完成或失败后碎片未清理
569
+
570
+ **解决**:
571
+ - 上传失败时调用 `abortMultipartUpload` 清理
572
+ - 配置 Bucket 生命周期规则自动清理碎片
573
+ - 定期检查未完成的分片上传任务并清理
574
+
575
+ ### 5. 文件名中文乱码
576
+
577
+ **原因**:文件名编码问题
578
+
579
+ **解决**:
580
+ - 使用 UUID 作为 OSS objectKey,不使用中文文件名
581
+ - 原始文件名存储在数据库或 OSS 元数据中
582
+ - 下载时通过 Content-Disposition 设置文件名,注意 URL 编码
583
+
584
+ ## 依赖版本
585
+
586
+ - aliyun-sdk-oss: 3.18.1
587
+ - Spring Boot: 3.5.3
588
+ - Java: 21
589
+
590
+ ## 参考文档
591
+
592
+ - [阿里云 OSS Java SDK 文档](https://help.aliyun.com/document_detail/32008.html)
593
+ - [阿里云 OSS 最佳实践](https://help.aliyun.com/document_detail/31850.html)
594
+ - [STS 临时授权访问](https://help.aliyun.com/document_detail/100624.html)