切换主题
📤 文件上传接口
本文档介绍文件上传相关的所有接口,包括普通上传和分片上传两种方式。
普通上传
适用于小文件(建议 < 100MB)的快速上传场景。
上传流程
业务类型上传接口
用于上传文件到指定业务类型,支持多服务场景下的业务数据隔离。
接口信息
POST /upload/{业务类型代码}
Content-Type: multipart/form-data1
2
2
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | File | 是 | 上传的文件对象 |
响应示例
json
{
"code": 0,
"successful": true,
"msg": null,
"data": {
"filePath": "/2024/08/25/23/891dc10b-9b8f-4d1a-bdcd-a298bcc6dc0b.apk",
"fileName": "example.apk",
"originalPath": "2024/08/25/23/891dc10b-9b8f-4d1a-bdcd-a298bcc6dc0b.apk",
"fileSize": 1048576,
"extName": "apk"
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
响应字段说明
| 字段名 | 类型 | 说明 |
|---|---|---|
code | Number | 错误码,0表示成功 |
successful | Boolean | 操作是否成功 |
msg | String | 错误信息,成功时为null |
data.filePath | String | 文件访问路径 |
data.fileName | String | 原始文件名称 |
data.originalPath | String | 文件在存储系统中的相对路径 |
data.fileSize | Number | 文件大小(字节) |
data.extName | String | 文件扩展名 |
默认上传接口
使用第一个文件配置,适用于单一业务服务场景。
接口信息
POST /upload
Content-Type: multipart/form-data1
2
2
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | File | 是 | 上传的文件对象 |
响应格式与业务类型上传接口相同。
分片上传
使用场景
适用场景
- ✅ 大文件上传(> 100MB)
- ✅ 网络不稳定环境
- ✅ 需要断点续传
- ✅ 需要显示上传进度
分片上传流程
分片上传接口说明
1️⃣ 初始化分片上传
接口信息
POST /upload/chunk/init/{业务类型}
POST /upload/chunk/init
Content-Type: application/json1
2
3
2
3
请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
fileName | String | 是 | 原始文件名 | video.mp4 |
fileSize | Number | 是 | 文件总大小(字节) | 104857600 |
服务端自动计算
chunkSize: 根据配置文件的chunk-size决定(默认5MB)totalChunks: 根据文件大小和分片大小自动计算
请求示例
json
{
"fileName": "large-video.mp4",
"fileSize": 104857600
}1
2
3
4
2
3
4
响应示例
json
{
"code": 0,
"successful": true,
"msg": null,
"data": {
"fileId": "550e8400-e29b-41d4-a716-446655440000",
"uploadId": "upload-id-from-cloud",
"fileName": "large-video.mp4",
"chunkSize": 5242880,
"totalChunks": 20,
"expiresAt": 1735689600
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
响应字段说明
| 字段名 | 类型 | 说明 |
|---|---|---|
fileId | String | 文件唯一标识,后续接口必需 |
uploadId | String | 对象存储的上传ID(仅对象存储) |
fileName | String | 文件名 |
chunkSize | Number | 分片大小(字节) |
totalChunks | Number | 总分片数 |
expiresAt | Number | 过期时间戳(秒),24小时有效 |
2️⃣ 上传分片
接口信息
POST /upload/chunk/{业务类型}
POST /upload/chunk
Content-Type: multipart/form-data1
2
3
2
3
请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
file | File | 是 | 分片文件数据 | - |
fileId | String | 是 | 文件唯一标识 | 550e8400... |
chunkIndex | Number | 是 | 分片索引(从0开始) | 0 |
提示
- 分片可以乱序上传
- 支持并发上传(建议3-5个并发)
- 单个分片失败可重试
响应示例
json
{
"code": 0,
"successful": true,
"msg": null,
"data": {
"fileId": "550e8400-e29b-41d4-a716-446655440000",
"chunkIndex": 0,
"totalChunks": 20,
"uploaded": true
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
3️⃣ 合并分片
接口信息
POST /upload/merge/{业务类型}
POST /upload/merge
Content-Type: application/json1
2
3
2
3
请求参数
| 参数名 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
fileId | String | 是 | 文件唯一标识 | 550e8400... |
请求示例
json
{
"fileId": "550e8400-e29b-41d4-a716-446655440000"
}1
2
3
2
3
响应示例
json
{
"code": 0,
"successful": true,
"msg": null,
"data": {
"filePath": "/2024/08/25/23/891dc10b.mp4",
"fileName": "large-video.mp4",
"originalPath": "2024/08/25/23/891dc10b.mp4",
"fileSize": 104857600,
"extName": "mp4"
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
示例代码
JavaScript 完整示例
javascript
/**
* 分片上传文件
* @param {File} file - 要上传的文件对象
* @param {string} businessType - 业务类型(可选)
* @returns {Promise<Object>} 上传结果
*/
async function uploadFileInChunks(file, businessType = '') {
try {
// 步骤1: 初始化分片上传
console.log('📤 开始上传:', file.name);
const initData = await initChunkUpload(file, businessType);
const { fileId, chunkSize, totalChunks } = initData;
console.log(`📦 分片信息: ${totalChunks}个分片, 每片${(chunkSize/1024/1024).toFixed(2)}MB`);
// 步骤2: 上传所有分片
await uploadAllChunks(file, fileId, chunkSize, totalChunks, businessType);
// 步骤3: 合并分片
console.log('🔄 正在合并分片...');
const result = await mergeChunks(fileId, businessType);
console.log('✅ 上传成功!', result);
return result;
} catch (error) {
console.error('❌ 上传失败:', error);
throw error;
}
}
// 初始化分片上传
async function initChunkUpload(file, businessType) {
const url = businessType
? `/upload/chunk/init/${businessType}`
: '/upload/chunk/init';
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
fileSize: file.size
})
});
const result = await response.json();
if (!result.successful) {
throw new Error(result.msg || '初始化失败');
}
return result.data;
}
// 上传所有分片
async function uploadAllChunks(file, fileId, chunkSize, totalChunks, businessType) {
const url = businessType
? `/upload/chunk/${businessType}`
: '/upload/chunk';
// 并发上传(最多5个并发)
const concurrency = 5;
const chunks = [];
for (let i = 0; i < totalChunks; i++) {
chunks.push(i);
}
// 分批上传
for (let i = 0; i < chunks.length; i += concurrency) {
const batch = chunks.slice(i, i + concurrency);
await Promise.all(
batch.map(index => uploadChunk(file, fileId, index, chunkSize, url))
);
const progress = Math.min(((i + concurrency) / totalChunks * 100), 100);
console.log(`📊 上传进度: ${progress.toFixed(1)}%`);
}
}
// 上传单个分片
async function uploadChunk(file, fileId, chunkIndex, chunkSize, url) {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', chunkIndex);
const response = await fetch(url, {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.successful) {
throw new Error(`分片${chunkIndex}上传失败: ${result.msg}`);
}
return result.data;
}
// 合并分片
async function mergeChunks(fileId, businessType) {
const url = businessType
? `/upload/merge/${businessType}`
: '/upload/merge';
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileId })
});
const result = await response.json();
if (!result.successful) {
throw new Error(result.msg || '合并失败');
}
return result.data;
}
// 使用示例
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadFileInChunks(file, 'video');
alert('上传成功: ' + result.filePath);
} catch (error) {
alert('上传失败: ' + error.message);
}
});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
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
最佳实践
✅ 推荐做法
分片大小配置
yaml# config.yaml files: - business-type: video chunk-size: 10MB # 视频文件使用较大分片 - business-type: document chunk-size: 5MB # 文档文件使用默认分片1
2
3
4
5
6并发控制
javascript// 根据网络状况调整并发数 const concurrency = navigator.connection?.effectiveType === '4g' ? 5 : 3;1
2错误重试
javascriptasync function uploadChunkWithRetry(file, fileId, index, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await uploadChunk(file, fileId, index); } catch (error) { if (i === maxRetries - 1) throw error; await sleep(1000 * (i + 1)); // 指数退避 } } }1
2
3
4
5
6
7
8
9
10进度显示
javascriptfunction updateProgress(uploaded, total) { const percent = (uploaded / total * 100).toFixed(1); document.getElementById('progress').textContent = `${percent}%`; document.getElementById('progressBar').style.width = `${percent}%`; }1
2
3
4
5断点续传
javascript// 保存上传进度 localStorage.setItem(`upload_${fileId}`, JSON.stringify({ uploadedChunks: [0, 1, 2, 3], totalChunks: 20 })); // 恢复上传 const progress = JSON.parse(localStorage.getItem(`upload_${fileId}`)); const remainingChunks = Array.from( {length: progress.totalChunks}, (_, i) => i ).filter(i => !progress.uploadedChunks.includes(i));1
2
3
4
5
6
7
8
9
10
11
12
⚠️ 注意事项
| 项目 | 说明 |
|---|---|
| Redis依赖 | 分片上传依赖Redis存储元数据,确保Redis正常运行 |
| 过期时间 | 上传会话24小时后过期,需在此期间完成上传 |
| 存储类型 | 对象存储(OSS/COS/OBS/S3)直接上传到云端,本地存储使用临时文件 |
| 临时文件 | 合并成功后自动清理,失败的临时文件24小时后过期 |
| 分片顺序 | 分片可以乱序上传,系统会自动按序合并 |
| 网络中断 | 支持断点续传,无需重新初始化 |
| 文件大小 | 建议大于100MB的文件使用分片上传 |
| 并发数量 | 建议3-5个并发,过多可能导致网络拥塞 |
错误码说明
| 错误码 | 说明 | 解决方案 |
|---|---|---|
-1000 | 请求参数错误 | 检查必填参数是否完整 |
-1000 | 业务类型不存在 | 确认业务类型配置正确 |
-1000 | 文件上传会话不存在或已过期 | 重新初始化分片上传 |
-1000 | 分片索引无效 | 检查chunkIndex是否在有效范围内 |
-1000 | 分片未上传 | 确保所有分片都已上传 |
-1000 | 不允许上传的文件类型 | 检查文件扩展名是否在允许列表中 |
-1000 | 上传文件大小超出限制 | 减小文件大小或调整配置 |
-1000 | 存储客户端未初始化 | 检查对应业务类型的存储配置 |
配置参考
yaml
# config.yaml 示例
server:
port: 9830
name: file
redis:
addr: localhost:6379
password: ""
db: 0
files:
# 视频文件配置
- business-type: video
file-type: oss
chunk-size: 10MB # 分片大小
max-size: 2GB # 最大文件大小
allowed-upload-suffix: mp4,avi,mov
bucket: video-bucket
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id: YOUR_ACCESS_KEY
access-key-secret: YOUR_SECRET_KEY
# 文档文件配置
- business-type: document
file-type: local
chunk-size: 5MB
max-size: 100MB
root-path: /data/files
allowed-upload-suffix: pdf,doc,docx,xls,xlsx1
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
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
常见问题
Q: 什么时候使用分片上传?
A: 建议在以下场景使用分片上传:
- 文件大小 > 100MB
- 网络环境不稳定
- 需要显示上传进度
- 需要支持断点续传
Q: 分片大小如何选择?
A: 分片大小由服务端配置决定,建议:
- 视频文件:10MB
- 文档文件:5MB
- 图片文件:2MB
- 根据网络环境和文件类型调整
Q: 上传失败如何处理?
A:
- 单个分片失败:重试该分片(建议最多3次)
- 初始化失败:检查参数和配置
- 合并失败:确认所有分片已上传
- 会话过期:重新初始化上传
Q: 如何实现断点续传?
A:
- 保存已上传的分片索引到localStorage
- 恢复上传时,只上传未完成的分片
- FileId在24小时内有效,可继续使用