Files
junhong_cmp_fiber/docs/object-storage/前端接入指南.md
huang d309951493
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
feat(import): 用 Excel 格式替换 CSV 导入
- 删除 CSV 解析代码,新增 Excel 解析器 (excelize)

- 更新 IoT 卡和设备导入任务处理器

- 更新 API 路由文档和前端接入指南

- 归档变更到 openspec/changes/archive/

- 同步 delta specs 到 main specs
2026-01-31 14:13:02 +08:00

251 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 对象存储前端接入指南
## 文件上传流程
```
前端 后端 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 接口
### 请求
```http
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"
}
```
### 响应
```json
{
"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 请求上传文件到对象存储:
```javascript
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
### 变更前
```http
POST /api/admin/iot-cards/import
Content-Type: multipart/form-data
carrier_id=1
batch_no=BATCH-2025-01
file=@cards.csv
```
### 变更后
```http
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
```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 已过期,需要重新获取:
```typescript
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}`);
}
}
}
```
### 网络错误
对象存储上传可能因网络问题失败,建议实现重试机制:
```typescript
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 无法使用
1. 检查 URL 是否过期15 分钟有效期)
2. 确保 Content-Type 与获取 URL 时指定的一致
3. 检查文件大小是否超过限制
### Q: file_key 可以重复使用吗
可以。file_key 一旦上传成功就永久有效,可以在多个业务接口中使用同一个 file_key。