@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.
- package/docs/exam-question-generate-api.md +163 -0
- package/package.json +1 -1
- package/src/prompts.js +3 -3
- package/src/transform.js +1 -1
- package/templates/.claude/skills/avatar-boot-starter-feign/README.md +243 -0
- package/templates/.claude/skills/avatar-boot-starter-feign/SKILL.md +47 -219
- package/templates/.claude/skills/avatar-boot-starter-feign/references//345/212/237/350/203/275/350/257/246/350/247/243.md +65 -0
- 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
- package/templates/.claude/skills/avatar-boot-starter-feign/references//351/205/215/347/275/256/345/217/202/350/200/203.md +70 -0
- package/templates/.claude/skills/avatar-boot-starter-job/README.md +437 -0
- package/templates/.claude/skills/avatar-boot-starter-job/SKILL.md +35 -414
- package/templates/.claude/skills/avatar-boot-starter-job/references//345/270/270/350/247/201/351/227/256/351/242/230.md +55 -0
- 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
- package/templates/.claude/skills/avatar-boot-starter-job/references//347/233/221/346/216/247/346/214/207/346/240/207.md +72 -0
- package/templates/.claude/skills/avatar-boot-starter-kafka/README.md +580 -0
- package/templates/.claude/skills/avatar-boot-starter-kafka/SKILL.md +36 -560
- package/templates/.claude/skills/avatar-boot-starter-kafka/references//346/234/200/344/275/263/345/256/236/350/267/265.md +43 -0
- package/templates/.claude/skills/avatar-boot-starter-kafka/references//346/240/270/345/277/203/345/212/237/350/203/275.md +117 -0
- package/templates/.claude/skills/avatar-boot-starter-kafka/references//351/205/215/347/275/256/345/217/202/350/200/203.md +54 -0
- package/templates/.claude/skills/avatar-boot-starter-mysql/README.md +572 -0
- package/templates/.claude/skills/avatar-boot-starter-mysql/SKILL.md +40 -550
- 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
- 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
- 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
- package/templates/.claude/skills/avatar-boot-starter-nacos/README.md +901 -0
- package/templates/.claude/skills/avatar-boot-starter-nacos/SKILL.md +40 -879
- package/templates/.claude/skills/avatar-boot-starter-nacos/references//345/212/237/350/203/275/344/275/277/347/224/250.md +134 -0
- 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
- package/templates/.claude/skills/avatar-boot-starter-nacos/references//346/225/205/351/232/234/346/216/222/346/237/245.md +64 -0
- package/templates/.claude/skills/avatar-boot-starter-oss/README.md +594 -0
- package/templates/.claude/skills/avatar-boot-starter-oss/SKILL.md +52 -570
- 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
- package/templates/.claude/skills/avatar-boot-starter-oss/references//346/240/270/345/277/203/345/212/237/350/203/275.md +94 -0
- 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
- package/templates/.claude/skills/avatar-boot-starter-redis/README.md +586 -0
- package/templates/.claude/skills/avatar-boot-starter-redis/SKILL.md +42 -566
- 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
- package/templates/.claude/skills/avatar-boot-starter-redis/references//346/225/260/346/215/256/346/223/215/344/275/234.md +111 -0
- package/templates/.claude/skills/avatar-boot-starter-redis/references//351/253/230/347/272/247/345/212/237/350/203/275.md +90 -0
- package/templates/.claude/skills/avatar-boot-starter-rocketmq/README.md +662 -0
- package/templates/.claude/skills/avatar-boot-starter-rocketmq/SKILL.md +48 -640
- package/templates/.claude/skills/avatar-boot-starter-rocketmq/references//346/240/270/345/277/203/345/212/237/350/203/275.md +101 -0
- 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
- package/templates/.claude/skills/avatar-boot-starter-rocketmq/references//351/253/230/347/272/247/347/211/271/346/200/247.md +71 -0
- package/templates/.claude/skills/avatar-boot-starter-web/README.md +1007 -0
- package/templates/.claude/skills/avatar-boot-starter-web/SKILL.md +150 -1003
- 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
- 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
- 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
- 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
- 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
- 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
- package/templates/.claude/skills/avatar-boot-starter-web/references//346/263/250/346/204/217/344/272/213/351/241/271.md +68 -0
- 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
- package/templates/.claude/skills/avatar-boot-starter-web/references//351/205/215/347/275/256/345/217/202/350/200/203.md +107 -0
- package/templates/.claude/skills/crud-generator/SKILL.md +133 -64
- package/templates/.claude/skills/database-design/README.md +207 -0
- package/templates/.claude/skills/database-design/SKILL.md +469 -82
- package/templates/.claude/skills/database-design/references//345/221/275/345/220/215/350/247/204/350/214/203.md +232 -0
- 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
- package/templates/.claude/skills/database-design/references//347/264/242/345/274/225/350/247/204/350/214/203.md +506 -0
- package/templates/avatar-scaffold-api/pom.xml +0 -5
- package/templates/avatar-scaffold-service/pom.xml +25 -87
- package/templates/avatar-scaffold-service/src/main/resources/application-dev.yaml +3 -5
- package/templates/avatar-scaffold-service/src/main/resources/application-local.yaml +2 -2
- package/templates/pom.xml +9 -18
|
@@ -1,594 +1,76 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: avatar-boot-starter-oss
|
|
3
|
-
description:
|
|
3
|
+
description: Avatar Boot OSS 模块使用指南。当用户询问对象存储接入、文件上传下载、预签名URL、分片上传、文件路径规范、DB存储规范、FileKey编码,或需要接入 avatar-boot-starter-oss 时触发。支持阿里云OSS/MinIO/腾讯COS/AWS S3。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Avatar Boot
|
|
6
|
+
# Avatar Boot Starter OSS 使用指南
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
你是 Avatar Boot OSS 模块的使用顾问与开发助手。
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- ✅ **简单上传** - 支持 putObject 方式上传文件和流
|
|
12
|
-
- ✅ **分片上传** - 支持大文件分片上传,自动管理分片生命周期
|
|
13
|
-
- ✅ **签名 URL** - 生成预签名 URL,支持临时授权访问
|
|
14
|
-
- ✅ **STS 直传** - 支持客户端直传模式,减轻服务端带宽压力
|
|
15
|
-
- ✅ **生命周期管理** - 支持配置文件自动过期和存储类型转换
|
|
10
|
+
## 交互流程(必须遵守)
|
|
16
11
|
|
|
17
|
-
|
|
12
|
+
**每次被触发时,先通过 AskUserQuestion 工具询问用户意图:**
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
问题:"您好!我是 Avatar Boot OSS 模块助手,请问您需要哪方面的帮助?"
|
|
15
|
+
选项:
|
|
16
|
+
1. **快速接入** - 添加依赖、配置 OSS 连接信息
|
|
17
|
+
2. **文件上传** - 字节数组上传、本地文件上传、预签名前端直传
|
|
18
|
+
3. **文件下载** - 预签名下载URL、本地下载、大文件分片下载
|
|
19
|
+
4. **分片上传** - 前端大文件分片上传完整流程
|
|
20
|
+
5. **规范说明** - 文件路径规范、DB存储规范、FileKey编码
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
**根据用户选择,用 Read 工具按下方「文档读取路由」加载对应文档,然后给出具体指导。**
|
|
22
23
|
|
|
23
|
-
|
|
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
|
-
**原因**:文件过大或网络不稳定
|
|
24
|
+
---
|
|
549
25
|
|
|
550
|
-
|
|
551
|
-
- 大文件(>100MB)使用分片上传
|
|
552
|
-
- 增大 OSSClient 超时配置
|
|
553
|
-
- 检查网络带宽和稳定性
|
|
554
|
-
- 使用客户端直传减轻服务端压力
|
|
26
|
+
## 行为准则
|
|
555
27
|
|
|
556
|
-
|
|
28
|
+
1. **回答要具体**:引用具体的方法名、配置项
|
|
29
|
+
2. **主动提醒**:
|
|
30
|
+
- AK/SK 必须通过环境变量注入,禁止明文写入代码
|
|
31
|
+
- DB 中只存 `{bucket}/{filekey}`,不存完整 URL
|
|
32
|
+
- 临时文件(temp/)用完必须主动删除
|
|
33
|
+
3. **使用中文回答**
|
|
557
34
|
|
|
558
|
-
|
|
35
|
+
---
|
|
559
36
|
|
|
560
|
-
|
|
561
|
-
- 检查 URL 过期时间设置
|
|
562
|
-
- 确保生成 URL 时的 endpoint 与实际访问一致
|
|
563
|
-
- 检查是否使用了 HTTPS(endpoint 和访问方式要一致)
|
|
564
|
-
- 验证 Bucket 名称和 objectKey 是否正确
|
|
37
|
+
## 文档读取路由
|
|
565
38
|
|
|
566
|
-
|
|
39
|
+
> 所有路径相对于 skill 目录 `docs/skills/avatar-boot-starter-oss-skill/`
|
|
567
40
|
|
|
568
|
-
|
|
41
|
+
| 用户需求 | 需读取的文件 |
|
|
42
|
+
|:--|:--|
|
|
43
|
+
| 快速接入 / 配置说明 | `references/快速接入与配置.md` |
|
|
44
|
+
| 文件上传 / 下载 / 分片上传 | `references/核心功能.md` |
|
|
45
|
+
| 规范说明 / 注意事项 | `references/规范与注意事项.md` |
|
|
569
46
|
|
|
570
|
-
|
|
571
|
-
- 上传失败时调用 `abortMultipartUpload` 清理
|
|
572
|
-
- 配置 Bucket 生命周期规则自动清理碎片
|
|
573
|
-
- 定期检查未完成的分片上传任务并清理
|
|
47
|
+
---
|
|
574
48
|
|
|
575
|
-
|
|
49
|
+
## 通用参考信息
|
|
576
50
|
|
|
577
|
-
|
|
51
|
+
### 支持的 OSS 类型
|
|
578
52
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
53
|
+
| type 值 | 说明 | 预签名URL特性 |
|
|
54
|
+
|:--|:--|:--|
|
|
55
|
+
| `ALIOSS` | 阿里云 OSS | signedHeaders 为空,URL 可直接浏览器访问 |
|
|
56
|
+
| `MINIO` | MinIO | 可能含 x-amz-te header,需客户端携带 |
|
|
57
|
+
| `TXCOS` | 腾讯 COS | 同 MinIO |
|
|
58
|
+
| `AWS3` | AWS S3 | 同 MinIO |
|
|
583
59
|
|
|
584
|
-
|
|
60
|
+
### 签名URL有效期建议
|
|
585
61
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
62
|
+
| 场景 | 建议有效期 |
|
|
63
|
+
|:--|:--|
|
|
64
|
+
| 视频播放 | 2~4 小时 |
|
|
65
|
+
| 文件下载 | 15~30 分钟 |
|
|
66
|
+
| 封面图/缩略图 | 1~2 小时 |
|
|
67
|
+
| 预签名上传 | 30~60 分钟 |
|
|
589
68
|
|
|
590
|
-
|
|
69
|
+
### OssFileKeyHelper 路径格式
|
|
591
70
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
71
|
+
| 方法 | 格式 |
|
|
72
|
+
|:--|:--|
|
|
73
|
+
| `materialKey(tenantId, filename)` | `materials/{tenantId}/{yyyy}/{MM}/{dd}/{uuid}/{filename}` |
|
|
74
|
+
| `videoKey(tenantId, taskId, filename)` | `videos/{tenantId}/{yyyy}/{MM}/{dd}/{taskId}/{filename}` |
|
|
75
|
+
| `tempKey(serviceName, taskId, filename)` | `temp/{serviceName}/{yyyy}/{MM}/{dd}/{taskId}/{filename}` |
|
|
76
|
+
| `staticKey(version, filename)` | `static/{version}/{filename}` |
|