All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
- 删除 CSV 解析代码,新增 Excel 解析器 (excelize) - 更新 IoT 卡和设备导入任务处理器 - 更新 API 路由文档和前端接入指南 - 归档变更到 openspec/changes/archive/ - 同步 delta specs 到 main specs
6.7 KiB
6.7 KiB
对象存储前端接入指南
文件上传流程
前端 后端 API 对象存储
│ │ │
│ 1. POST /storage/upload-url │
│ {file_name, content_type, purpose} │
│ ─────────────────────────► │
│ │ │
│ 2. 返回 {upload_url, file_key, expires_in} │
│ ◄───────────────────────── │
│ │ │
│ 3. PUT upload_url (文件内容) │
│ ─────────────────────────────────────────────────► │
│ │ │
│ 4. 上传成功 (200 OK) │
│ ◄───────────────────────────────────────────────── │
│ │ │
│ 5. POST /iot-cards/import │
│ {carrier_id, batch_no, file_key} │
│ ─────────────────────────► │
│ │ │
│ 6. 返回任务创建成功 │
│ ◄───────────────────────── │
获取预签名 URL 接口
请求
POST /api/admin/storage/upload-url
Content-Type: application/json
Authorization: Bearer {token}
{
"file_name": "cards.xlsx",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"purpose": "iot_import"
}
响应
{
"code": 0,
"message": "成功",
"data": {
"upload_url": "http://obs-helf.cucloud.cn/cmp/imports/2025/01/24/abc123.xlsx?X-Amz-Algorithm=...",
"file_key": "imports/2025/01/24/abc123.xlsx",
"expires_in": 900
}
}
purpose 可选值
| 值 | 说明 | 生成路径 |
|---|---|---|
| iot_import | ICCID 导入 (Excel) | imports/YYYY/MM/DD/uuid.xlsx |
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx |
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext |
使用预签名 URL 上传文件
获取到 upload_url 后,直接使用 PUT 请求上传文件到对象存储:
const response = await fetch(upload_url, {
method: 'PUT',
headers: {
'Content-Type': content_type
},
body: file
});
if (response.ok) {
console.log('上传成功');
} else {
console.error('上传失败:', response.status);
}
ICCID 导入接口变更(BREAKING CHANGE)
变更前
POST /api/admin/iot-cards/import
Content-Type: multipart/form-data
carrier_id=1
batch_no=BATCH-2025-01
file=@cards.csv
变更后
POST /api/admin/iot-cards/import
Content-Type: application/json
Authorization: Bearer {token}
{
"carrier_id": 1,
"batch_no": "BATCH-2025-01",
"file_key": "imports/2025/01/24/abc123.xlsx"
}
完整代码示例(TypeScript)
interface UploadURLResponse {
upload_url: string;
file_key: string;
expires_in: number;
}
async function uploadAndImportCards(
file: File,
carrierId: number,
batchNo: string
): Promise<void> {
// 1. 获取预签名上传 URL
const urlResponse = await fetch('/api/admin/storage/upload-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
body: JSON.stringify({
file_name: file.name,
content_type: file.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
purpose: 'iot_import'
})
});
if (!urlResponse.ok) {
throw new Error('获取上传 URL 失败');
}
const { data } = await urlResponse.json();
const { upload_url, file_key } = data as UploadURLResponse;
// 2. 上传文件到对象存储
const uploadResponse = await fetch(upload_url, {
method: 'PUT',
headers: {
'Content-Type': file.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
},
body: file
});
if (!uploadResponse.ok) {
throw new Error('文件上传失败');
}
// 3. 调用导入接口
const importResponse = await fetch('/api/admin/iot-cards/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`
},
body: JSON.stringify({
carrier_id: carrierId,
batch_no: batchNo,
file_key: file_key
})
});
if (!importResponse.ok) {
throw new Error('导入任务创建失败');
}
console.log('导入任务已创建');
}
错误处理和重试策略
预签名 URL 过期
预签名 URL 有效期为 15 分钟。如果上传时 URL 已过期,需要重新获取:
async function uploadWithRetry(file: File, purpose: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const { upload_url, file_key } = await getUploadURL(file.name, file.type, purpose);
try {
await uploadFile(upload_url, file);
return file_key;
} catch (error) {
if (i === maxRetries - 1) throw error;
console.warn(`上传失败,重试 ${i + 1}/${maxRetries}`);
}
}
}
网络错误
对象存储上传可能因网络问题失败,建议实现重试机制:
async function uploadFile(url: string, file: File, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file
});
if (response.ok) return;
if (response.status >= 500) {
// 服务端错误,可重试
continue;
}
throw new Error(`上传失败: ${response.status}`);
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
常见问题
Q: 上传时报 CORS 错误
确保对象存储已配置 CORS 规则允许前端域名访问。
Q: 预签名 URL 无法使用
- 检查 URL 是否过期(15 分钟有效期)
- 确保 Content-Type 与获取 URL 时指定的一致
- 检查文件大小是否超过限制
Q: file_key 可以重复使用吗
可以。file_key 一旦上传成功就永久有效,可以在多个业务接口中使用同一个 file_key。