feat: Excel 导入按列位置读取,IoT 卡导入支持卡业务类型
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m28s

- 重写 parseCardRows:移除列名识别,固定跳过第1行表头,按 col0=ICCID、
  col1=MSISDN、col2=VirtualNo 取值,VirtualNo 升级为必填字段
- 重写 ParseDeviceExcel:移除 buildDeviceColumnIndex,IMEI 调整至第5列
  新增 MaxSimSlots 范围校验 [1,4],整行为空时不计入 total
- 删除 findCardColumns 和 buildDeviceColumnIndex 两个冗余函数
- ImportIotCardRequest 新增 card_category 字段(normal/industry,默认 normal)
- IotCardImportTask model 新增 card_category 字段
- 迁移 000111:tb_iot_card_import_task 新增 card_category 列
- CreateImportTask 将 req.CardCategory 写入任务,空串兜底 normal
- processBatch 改用 task.CardCategory 替代写死的 CardCategoryNormal
- ImportTaskResponse 新增 card_category 字段返回

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 17:16:31 +08:00
parent ff7e749bf2
commit cd807e58e3
13 changed files with 419 additions and 152 deletions

View File

@@ -0,0 +1,101 @@
## MODIFIED Requirements
### Requirement: 设备批量导入
系统 SHALL 提供设备批量导入功能,通过 Excel 文件导入设备并自动绑定卡,按固定列位置读取,表头行内容不影响解析结果,仅平台用户可操作。
**API 端点**: `POST /api/admin/devices/import`
**请求参数**:
- `batch_no`: 批次号(必填)
- `file_key`: 对象存储文件路径(必填,通过 /storage/upload-url 获取)
**Excel 格式**:
- **文件格式**: 仅支持 `.xlsx` (Excel 2007+)
- **Sheet**: 读取第一个sheet或优先读取名为"导入数据"的sheet
- **表头行**: 第1行固定为表头永远跳过内容不限中文、英文均可
- **列位置(固定,不可变)**:
```
第1列索引0: 虚拟号(必填,全局唯一)
第2列索引1: 设备名称(可选)
第3列索引2: 设备型号(可选)
第4列索引3: 设备类型(可选)
第5列索引4: IMEI可选
第6列索引5: 制造商(可选)
第7列索引6: 最大SIM槽数可选默认4范围1-4
第8列索引7: 卡1 ICCID可选
第9列索引8: 卡2 ICCID可选
第10列索引9: 卡3 ICCID可选
第11列索引10: 卡4 ICCID可选
```
- **列格式**: 所有列应设置为文本格式(避免数字被转为科学记数法)
**失败原因文本**:
- VirtualNo 为空:`"设备虚拟号(virtual_no)不能为空"`
- MaxSimSlots 越界:`"最大SIM槽数必须在1-4之间"`
**导入规则**:
- 按列索引取值,不识别列名,第一行永远跳过
- 若整行目标列皆为空,视为空行跳过,不计入 total不计入失败
- MaxSimSlots 为空或 0 回填默认值 4非空且不在 [1,4] 记录为失败
- 导入的设备 shop_id = NULL平台库存
- 导入的设备 status = 1在库
- 设备号重复则该行跳过
- ICCID 必须已存在于系统中(先导入卡,再导入设备)
- ICCID 不存在则该行失败
- ICCID 已绑定其他设备则该行失败
- 导入通过异步任务处理,立即返回任务 ID
**权限**: 仅平台用户
**响应**:
- `task_id`: 导入任务 ID
- `task_no`: 任务编号
- `message`: 提示信息
#### Scenario: 提交设备导入任务
- **WHEN** 平台管理员上传 Excel 文件并提交导入请求
- **THEN** 系统创建导入任务,返回任务 ID开始异步处理
#### Scenario: 中文表头正常导入
- **GIVEN** Excel 文件第1行表头为 `虚拟号 | 设备名称 | 设备型号 | 设备类型 | IMEI | 制造商 | 最大SIM槽数 | 卡1 | 卡2 | 卡3 | 卡4`
- **WHEN** 系统解析该 Excel 文件
- **THEN** 系统跳过第1行从第2行开始按列位置解析数据
#### Scenario: 代理尝试导入设备
- **WHEN** 代理用户尝试导入设备
- **THEN** 系统返回 403 错误,提示"无权限执行此操作"
#### Scenario: 文件格式错误
- **WHEN** 平台管理员上传非 Excel 格式(.xlsx)的文件
- **THEN** 系统创建任务但处理失败,任务状态为"失败",错误信息为"不支持的文件格式 .csv请上传Excel文件(.xlsx)"
#### Scenario: Excel结构错误
- **WHEN** 平台管理员上传的Excel文件无工作表或无数据行
- **THEN** 系统创建任务但处理失败,任务状态为"失败",记录相应错误信息
#### Scenario: VirtualNo 为空的行记录为失败
- **WHEN** Excel 中某行第1列虚拟号为空
- **THEN** 该行计入失败(`fail_count++`),失败原因为"设备虚拟号(virtual_no)不能为空"
- **THEN** 其他合法行继续导入,不因此行中断
#### Scenario: ICCID 不存在
- **WHEN** Excel 中某行的 ICCID 在系统中不存在
- **THEN** 该行导入失败,记录失败原因"ICCID 不存在"
#### Scenario: ICCID 已绑定其他设备
- **WHEN** Excel 中某行的 ICCID 已绑定到其他设备
- **THEN** 该行导入失败,记录失败原因"ICCID 已绑定其他设备"
#### Scenario: 设备号重复
- **WHEN** Excel 中某行的设备号在系统中已存在
- **THEN** 该行被跳过,记录跳过原因"设备号已存在"

View File

@@ -0,0 +1,98 @@
## MODIFIED Requirements
### Requirement: Excel 文件格式规范
系统 SHALL 要求 Excel 文件必须包含 ICCID、MSISDN、虚拟号三列按固定列位置读取表头行内容不影响解析结果。
**文件格式要求**:
- **文件格式**: 仅支持 `.xlsx` (Excel 2007+)
- **Sheet**: 读取第一个sheet或优先读取名为"导入数据"的sheet
- **表头行**: 第1行固定为表头永远跳过内容不限中文、英文均可
- **列位置(固定,不可变)**:
- 第1列索引0: ICCID
- 第2列索引1: MSISDN
- 第3列索引2: 虚拟号(必填)
- **列格式**: 应设置为文本格式(避免长数字被转为科学记数法)
**解析规则**:
- 按列索引取值,不识别列名,第一行永远跳过
- 自动去除单元格首尾空格
- 若 ICCID、MSISDN、virtual_no 三列皆为空,视为空行跳过,不计入 total不计入失败
- ICCID 为空(但其他列非空)的行记录为失败,失败原因为"ICCID 不能为空"
- MSISDN 为空(但其他列非空)的行记录为失败,失败原因为"MSISDN 不能为空"
- virtual_no第3列为空的行记录为失败失败原因为"虚拟号(virtual_no)不能为空"
**virtual_no 导入规则**:
- virtual_no 为必填,为空则该行记录为失败
- virtual_no 全局唯一(跨卡和设备),重复则失败,原因为"虚拟号已被占用: <值>"
#### Scenario: 正常导入ICCID 和 VirtualNo 均有值)
- **WHEN** Excel 中某行第1列 ICCID="898600XXXXX"第3列 virtual_no="CARD-001"第2列 msisdn="13800000001"
- **THEN** 导入成功卡记录写入数据库VirtualNo 和 ICCID 同步注册到 `tb_asset_identifier`
#### Scenario: 中文表头正常导入
- **GIVEN** Excel 文件第1行表头为 `ICCID | 接入号 | 虚拟号`(任意中文或英文内容)
- **WHEN** 系统解析该 Excel 文件
- **THEN** 系统跳过第1行从第2行开始按列位置解析数据
#### Scenario: VirtualNo 为空的行被拒绝
- **WHEN** Excel 中某行第3列为空
- **THEN** 该行计入失败,失败原因为"虚拟号(virtual_no)不能为空"
- **THEN** 其他合法行继续导入,不因此行中断
#### Scenario: VirtualNo 重复被拒绝
- **WHEN** Excel 中某行第3列的值与已有卡/设备的 VirtualNo 重复(跨表)
- **THEN** 该行计入失败,原因为"虚拟号已被占用: <值>"
#### Scenario: MSISDN 为空的行记录失败
- **WHEN** Excel 中某行第2列为空
- **THEN** 该行标记为失败,原因为"MSISDN 不能为空"
#### Scenario: 长数字无损解析
- **GIVEN** Excel 文件中第1列设置为文本格式包含 20 位数字 "89860012345678901234"
- **WHEN** 系统解析该 Excel 文件
- **THEN** ICCID 完整保留为 "89860012345678901234",无精度损失,无科学记数法
#### Scenario: 拒绝非Excel格式文件
- **GIVEN** 上传文件扩展名为 .csv
- **WHEN** 系统尝试解析该文件
- **THEN** 系统返回错误"不支持的文件格式 .csv请上传Excel文件(.xlsx)"
---
## ADDED Requirements
### Requirement: 导入批次支持卡业务类型
系统 SHALL 在 IoT 卡导入请求中支持 `card_category` 参数,以批次为单位指定导入卡的业务类型,默认为普通卡。
**请求字段**:
- `card_category`: 卡业务类型(枚举:`normal` / `industry`,可选,默认 `normal`
**任务字段**:
- `IotCardImportTask` 新增 `card_category` 字段VARCHAR(20),默认 `normal`
**导入行为**:
- 导入任务中所有卡统一使用 `card_category` 的值,不支持同一批次混用
#### Scenario: 不传 card_category 时默认为普通卡
- **WHEN** 导入请求未包含 `card_category` 字段
- **THEN** 导入的所有卡 `card_category``normal`
#### Scenario: 指定行业卡导入
- **WHEN** 导入请求中 `card_category` = `industry`
- **THEN** 该批次导入的所有卡 `card_category` 均为 `industry`
#### Scenario: 传入无效 card_category
- **WHEN** 导入请求中 `card_category` = `unknown`(非枚举值)
- **THEN** 系统返回参数校验错误,提示卡业务类型无效