feat(import): 用 Excel 格式替换 CSV 导入
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
- 删除 CSV 解析代码,新增 Excel 解析器 (excelize) - 更新 IoT 卡和设备导入任务处理器 - 更新 API 路由文档和前端接入指南 - 归档变更到 openspec/changes/archive/ - 同步 delta specs 到 main specs
This commit is contained in:
@@ -7514,24 +7514,26 @@ paths:
|
||||
/api/admin/devices/import:
|
||||
post:
|
||||
description: |-
|
||||
仅平台用户可操作。
|
||||
仅平台用户可操作。文件格式已从 CSV 升级为 Excel (.xlsx)。
|
||||
|
||||
### 完整导入流程
|
||||
|
||||
1. **获取上传 URL**: 调用 `POST /api/admin/storage/upload-url`
|
||||
2. **上传 CSV 文件**: 使用预签名 URL 上传文件到对象存储
|
||||
2. **上传 Excel 文件**: 使用预签名 URL 上传文件到对象存储
|
||||
3. **调用本接口**: 使用返回的 `file_key` 提交导入任务
|
||||
|
||||
### CSV 文件格式
|
||||
### Excel 文件格式
|
||||
|
||||
必须包含列(首行为表头):
|
||||
- `device_no`: 设备号(必填,唯一)
|
||||
- `device_name`: 设备名称
|
||||
- `device_model`: 设备型号
|
||||
- `device_type`: 设备类型
|
||||
- `max_sim_slots`: 最大插槽数(默认4)
|
||||
- `manufacturer`: 制造商
|
||||
- `iccid_1` ~ `iccid_4`: 绑定的卡 ICCID(卡必须已存在且未绑定)
|
||||
- 文件格式:仅支持 .xlsx (Excel 2007+)
|
||||
- 必须包含列(首行为表头):
|
||||
- `device_no`: 设备号(必填,唯一)
|
||||
- `device_name`: 设备名称
|
||||
- `device_model`: 设备型号
|
||||
- `device_type`: 设备类型
|
||||
- `max_sim_slots`: 最大插槽数(默认4)
|
||||
- `manufacturer`: 制造商
|
||||
- `iccid_1` ~ `iccid_4`: 绑定的卡 ICCID(卡必须已存在且未绑定)
|
||||
- 列格式:设置为文本格式(避免长数字被转为科学记数法)
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -8881,11 +8883,12 @@ paths:
|
||||
## ⚠️ 接口变更说明(BREAKING CHANGE)
|
||||
|
||||
本接口已从 `multipart/form-data` 改为 `application/json`。
|
||||
文件格式从 CSV 升级为 Excel (.xlsx),解决长数字被转为科学记数法的问题。
|
||||
|
||||
### 完整导入流程
|
||||
|
||||
1. **获取上传 URL**: 调用 `POST /api/admin/storage/upload-url`
|
||||
2. **上传 CSV 文件**: 使用预签名 URL 上传文件到对象存储
|
||||
2. **上传 Excel 文件**: 使用预签名 URL 上传文件到对象存储
|
||||
3. **调用本接口**: 使用返回的 `file_key` 提交导入任务
|
||||
|
||||
### 请求示例
|
||||
@@ -8894,15 +8897,16 @@ paths:
|
||||
{
|
||||
"carrier_id": 1,
|
||||
"batch_no": "BATCH-2025-01",
|
||||
"file_key": "imports/2025/01/24/abc123.csv"
|
||||
"file_key": "imports/2025/01/24/abc123.xlsx"
|
||||
}
|
||||
```
|
||||
|
||||
### CSV 文件格式
|
||||
### Excel 文件格式
|
||||
|
||||
- 必须包含两列:`iccid`, `msisdn`
|
||||
- 首行为表头
|
||||
- 编码:UTF-8
|
||||
- 文件格式:仅支持 .xlsx (Excel 2007+)
|
||||
- 必须包含两列:`ICCID`, `MSISDN`
|
||||
- 首行为表头(可选,但建议包含)
|
||||
- 列格式:设置为文本格式(避免长数字被转为科学记数法)
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -14806,15 +14810,15 @@ paths:
|
||||
```javascript
|
||||
// 1. 获取预签名 URL
|
||||
const { data } = await api.post('/storage/upload-url', {
|
||||
file_name: 'cards.csv',
|
||||
content_type: 'text/csv',
|
||||
file_name: 'cards.xlsx',
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
purpose: 'iot_import'
|
||||
});
|
||||
|
||||
// 2. 上传文件到对象存储
|
||||
await fetch(data.upload_url, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'text/csv' },
|
||||
headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
|
||||
body: file
|
||||
});
|
||||
|
||||
@@ -14830,7 +14834,7 @@ paths:
|
||||
|
||||
| 值 | 说明 | 生成路径格式 |
|
||||
|---|------|-------------|
|
||||
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv |
|
||||
| iot_import | ICCID/设备导入 (Excel) | imports/YYYY/MM/DD/uuid.xlsx |
|
||||
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx |
|
||||
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext |
|
||||
|
||||
|
||||
192
docs/excel-import-frontend-guide.md
Normal file
192
docs/excel-import-frontend-guide.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Excel导入功能 - 前端接入指南
|
||||
|
||||
## 变更说明
|
||||
|
||||
导入功能已从CSV格式升级为Excel格式(.xlsx),解决长数字(如20位ICCID)被Excel自动转为科学记数法导致数据损坏的问题。
|
||||
|
||||
## 关键变更
|
||||
|
||||
### 1. 文件格式
|
||||
|
||||
| 项目 | 旧版本(CSV) | 新版本(Excel) |
|
||||
|-----|------------|--------------|
|
||||
| 文件扩展名 | `.csv` | `.xlsx` |
|
||||
| MIME类型 | `text/csv` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
|
||||
| 文件选择器accept | `*` 或 `.csv` | `.xlsx` |
|
||||
|
||||
### 2. 上传示例代码
|
||||
|
||||
**ICCID导入**:
|
||||
```javascript
|
||||
// 1. 获取预签名URL
|
||||
const response = await api.post('/api/admin/storage/upload-url', {
|
||||
file_name: 'cards.xlsx', // 修改扩展名
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // 修改MIME类型
|
||||
purpose: 'iot_import'
|
||||
});
|
||||
|
||||
const { upload_url, file_key } = response.data;
|
||||
|
||||
// 2. 上传Excel文件到对象存储
|
||||
await fetch(upload_url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' // 修改MIME类型
|
||||
},
|
||||
body: file // File对象来自<input type="file" accept=".xlsx">
|
||||
});
|
||||
|
||||
// 3. 提交导入任务
|
||||
await api.post('/api/admin/iot-cards/import', {
|
||||
carrier_id: 1,
|
||||
batch_no: 'BATCH-2025-01',
|
||||
file_key: file_key
|
||||
});
|
||||
```
|
||||
|
||||
**设备导入**: 流程相同,只需调用 `/api/admin/devices/import` 接口。
|
||||
|
||||
### 3. 文件选择器组件
|
||||
|
||||
**修改前**:
|
||||
```html
|
||||
<input type="file" accept="*" />
|
||||
<!-- 或 -->
|
||||
<input type="file" accept=".csv" />
|
||||
```
|
||||
|
||||
**修改后**:
|
||||
```html
|
||||
<input type="file" accept=".xlsx" />
|
||||
```
|
||||
|
||||
### 4. 文件验证
|
||||
|
||||
```javascript
|
||||
function validateFile(file) {
|
||||
// 检查扩展名
|
||||
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||||
throw new Error('仅支持上传Excel文件(.xlsx格式)');
|
||||
}
|
||||
|
||||
// 检查MIME类型(可选,部分浏览器可能不准确)
|
||||
const validTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/octet-stream' // 部分浏览器可能返回此类型
|
||||
];
|
||||
|
||||
if (!validTypes.includes(file.type)) {
|
||||
console.warn('文件MIME类型不匹配,但根据扩展名判断为有效文件');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Excel模板文件
|
||||
|
||||
### ICCID导入模板
|
||||
|
||||
**文件名**: `iccid_import_template.xlsx`
|
||||
|
||||
**格式**:
|
||||
| ICCID | MSISDN |
|
||||
|-------|--------|
|
||||
| 89860012345678901234 | 13800000001 |
|
||||
| 89860012345678901235 | 13800000002 |
|
||||
|
||||
**要点**:
|
||||
- 必须包含表头行(ICCID, MSISDN)
|
||||
- ICCID和MSISDN列必须设置为**文本格式**(重要!)
|
||||
- Excel中设置文本格式: 选中列 → 右键 → 设置单元格格式 → 文本
|
||||
|
||||
### 设备导入模板
|
||||
|
||||
**文件名**: `device_import_template.xlsx`
|
||||
|
||||
**格式**:
|
||||
| device_no | device_name | device_model | device_type | max_sim_slots | manufacturer | iccid_1 | iccid_2 | iccid_3 | iccid_4 |
|
||||
|-----------|-------------|--------------|-------------|---------------|--------------|---------|---------|---------|---------|
|
||||
| DEV-001 | GPS追踪器A | GT06N | GPS Tracker | 4 | Concox | 89860012345678901234 | 89860012345678901235 | | |
|
||||
| DEV-002 | GPS追踪器B | GT06N | GPS Tracker | 4 | Concox | 89860012345678901236 | | | |
|
||||
|
||||
**要点**:
|
||||
- 所有列都必须设置为**文本格式**
|
||||
- device_no为必填项
|
||||
- iccid_1 ~ iccid_4为可选项,填写时对应的ICCID必须已存在于系统中
|
||||
|
||||
## 模板下载功能实现
|
||||
|
||||
```javascript
|
||||
// 方案1: 后端提供静态文件下载
|
||||
<a href="/api/admin/storage/templates/iccid_import_template.xlsx" download>
|
||||
下载ICCID导入模板
|
||||
</a>
|
||||
|
||||
// 方案2: 前端本地存放模板文件
|
||||
<a href="/assets/templates/iccid_import_template.xlsx" download>
|
||||
下载ICCID导入模板
|
||||
</a>
|
||||
```
|
||||
|
||||
建议使用方案2(前端本地存放),减轻后端负担。
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 服务端错误示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "不支持的文件格式 .csv,请上传Excel文件(.xlsx)",
|
||||
"timestamp": "2025-01-31T13:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 前端错误提示
|
||||
|
||||
```javascript
|
||||
try {
|
||||
await uploadAndImport(file);
|
||||
} catch (error) {
|
||||
if (error.response?.data?.msg) {
|
||||
// 显示服务端返回的错误消息
|
||||
showError(error.response.data.msg);
|
||||
} else {
|
||||
showError('上传失败,请重试');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 迁移检查清单
|
||||
|
||||
- [ ] 修改文件选择器accept属性为 `.xlsx`
|
||||
- [ ] 更新上传时的MIME类型为Excel格式
|
||||
- [ ] 添加前端文件格式验证(扩展名检查)
|
||||
- [ ] 准备Excel模板文件并放置到前端资源目录
|
||||
- [ ] 添加"下载模板"按钮/链接
|
||||
- [ ] 更新相关提示文案(CSV → Excel)
|
||||
- [ ] 测试完整的上传流程
|
||||
- [ ] 验证错误场景(上传CSV文件时的提示)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **向后兼容**: 本次变更不向后兼容,旧的CSV文件无法使用,前端需同步更新
|
||||
2. **用户通知**: 建议在界面上添加醒目提示,告知用户格式变更
|
||||
3. **模板文件**: 模板文件中的ICCID列**必须**设置为文本格式,否则长数字会被Excel自动转为科学记数法
|
||||
4. **文件大小**: Excel文件比CSV大3-5倍,但对1万行数据影响不大(约3-5MB)
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 为什么要从CSV改为Excel?**
|
||||
A: Excel编辑CSV时会将超过15位的长数字(如20位ICCID)自动转为科学记数法,导致数据损坏。使用Excel格式并设置为文本格式可彻底解决此问题。
|
||||
|
||||
**Q: 用户已经准备好的CSV文件怎么办?**
|
||||
A: 用户可以在Excel中打开CSV,将ICCID/MSISDN列设置为文本格式,然后另存为.xlsx格式即可。
|
||||
|
||||
**Q: 是否支持.xls(旧版Excel)?**
|
||||
A: 不支持。仅支持.xlsx (Excel 2007+),建议在文档中明确说明。
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系后端开发团队。
|
||||
@@ -34,7 +34,7 @@ export JUNHONG_STORAGE_TEMP_DIR="/tmp/junhong-storage"
|
||||
### 获取预签名上传 URL
|
||||
|
||||
```go
|
||||
result, err := storageService.GetUploadURL(ctx, "iot_import", "cards.csv", "text/csv")
|
||||
result, err := storageService.GetUploadURL(ctx, "iot_import", "cards.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,7 +62,7 @@ defer f.Close()
|
||||
|
||||
```go
|
||||
reader := bytes.NewReader(content)
|
||||
err := storageService.Provider().Upload(ctx, fileKey, reader, "text/csv")
|
||||
err := storageService.Provider().Upload(ctx, fileKey, reader, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
```
|
||||
|
||||
### 检查文件是否存在
|
||||
@@ -81,7 +81,7 @@ err := storageService.Provider().Delete(ctx, fileKey)
|
||||
|
||||
| Purpose | 说明 | 生成路径 | ContentType |
|
||||
|---------|------|---------|-------------|
|
||||
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv | text/csv |
|
||||
| iot_import | ICCID 导入 (Excel) | imports/YYYY/MM/DD/uuid.xlsx | application/vnd.openxmlformats... |
|
||||
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx | application/vnd.openxmlformats... |
|
||||
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext | 自动检测 |
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
|
||||
{
|
||||
"file_name": "cards.csv",
|
||||
"content_type": "text/csv",
|
||||
"file_name": "cards.xlsx",
|
||||
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"purpose": "iot_import"
|
||||
}
|
||||
```
|
||||
@@ -49,8 +49,8 @@ Authorization: Bearer {token}
|
||||
"code": 0,
|
||||
"message": "成功",
|
||||
"data": {
|
||||
"upload_url": "http://obs-helf.cucloud.cn/cmp/imports/2025/01/24/abc123.csv?X-Amz-Algorithm=...",
|
||||
"file_key": "imports/2025/01/24/abc123.csv",
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ Authorization: Bearer {token}
|
||||
|
||||
| 值 | 说明 | 生成路径 |
|
||||
|---|------|---------|
|
||||
| iot_import | ICCID 导入 | imports/YYYY/MM/DD/uuid.csv |
|
||||
| iot_import | ICCID 导入 (Excel) | imports/YYYY/MM/DD/uuid.xlsx |
|
||||
| export | 数据导出 | exports/YYYY/MM/DD/uuid.xlsx |
|
||||
| attachment | 附件上传 | attachments/YYYY/MM/DD/uuid.ext |
|
||||
|
||||
@@ -107,7 +107,7 @@ Authorization: Bearer {token}
|
||||
{
|
||||
"carrier_id": 1,
|
||||
"batch_no": "BATCH-2025-01",
|
||||
"file_key": "imports/2025/01/24/abc123.csv"
|
||||
"file_key": "imports/2025/01/24/abc123.xlsx"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -134,7 +134,7 @@ async function uploadAndImportCards(
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_name: file.name,
|
||||
content_type: file.type || 'text/csv',
|
||||
content_type: file.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
purpose: 'iot_import'
|
||||
})
|
||||
});
|
||||
@@ -150,7 +150,7 @@ async function uploadAndImportCards(
|
||||
const uploadResponse = await fetch(upload_url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': file.type || 'text/csv'
|
||||
'Content-Type': file.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
},
|
||||
body: file
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user