# device-import Specification

## Purpose
TBD - created by archiving change add-device-management. Update Purpose after archive.
## 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）: SN（可选）
  第3列（索引2）: 设备名称（可选）
  第4列（索引3）: 设备型号（可选）
  第5列（索引4）: 设备类型（可选）
  第6列（索引5）: IMEI（可选）
  第7列（索引6）: 制造商（可选）
  第8列（索引7）: 最大SIM槽数（可选，默认4，范围1-4）
  第9列（索引8）: 卡1 ICCID（可选，对应槽位1）
  第10列（索引9）: 卡2 ICCID（可选，对应槽位2）
  第11列（索引10）: 卡3 ICCID（可选，对应槽位3）
  第12列（索引11）: 卡4 ICCID（可选，对应槽位4）
  ```
- **列格式**: 所有列应设置为文本格式（避免数字被转为科学记数法）

**失败原因文本**:
- VirtualNo 为空：`"设备虚拟号(virtual_no)不能为空"`
- MaxSimSlots 越界：`"最大SIM槽数必须在1-4之间"`
- ICCID 填写槽位超出最大槽位：`"卡槽填写超出最大SIM槽数"`

**导入规则**:
- 按列索引取值，不识别列名，第一行永远跳过
- 若整行目标列皆为空，视为空行跳过，不计入 total，不计入失败
- MaxSimSlots 为空或 0 回填默认值 4；非空且不在 [1,4] 记录为失败
- `卡1~卡4` 的列位本身就是槽位定义，导入时必须保留原始列位信息，不得按非空 ICCID 顺序重排
- 前置槽位允许为空；如果仅填写 `卡2`、`卡3`，则表示设备只在槽位 2、3 上绑定卡
- 任一已填写 ICCID 的槽位编号若大于 `max_sim_slots`，则该行导入失败
- 导入的设备 shop_id = NULL（平台库存）
- 导入的设备 status = 1（在库）
- 设备号重复则该行跳过
- ICCID 必须已存在于系统中（先导入卡，再导入设备）
- ICCID 不存在则该行失败
- ICCID 已绑定其他设备则该行失败
- 导入通过异步任务处理，立即返回任务 ID

**权限**: 仅平台用户

**响应**:
- `task_id`: 导入任务 ID
- `task_no`: 任务编号
- `message`: 提示信息

#### Scenario: 提交设备导入任务

- **WHEN** 平台管理员上传 Excel 文件并提交导入请求
- **THEN** 系统创建导入任务，返回任务 ID，开始异步处理

#### Scenario: 中文表头正常导入

- **GIVEN** Excel 文件第1行表头为 `虚拟号 | SN | 设备名称 | 设备型号 | 设备类型 | IMEI | 制造商 | 最大SIM槽数 | 卡1 | 卡2 | 卡3 | 卡4`
- **WHEN** 系统解析该 Excel 文件
- **THEN** 系统跳过第1行，从第2行开始按列位置解析数据

#### Scenario: SN 列按固定顺序导入

- **GIVEN** 某行第2列（索引1）填写 `SN-001`
- **WHEN** 系统解析并导入该行
- **THEN** 创建设备记录时 `sn = "SN-001"`

#### Scenario: 仅填写卡2和卡3时保留原始槽位

- **GIVEN** 某行 `最大SIM槽数 = 3`
- **AND** `卡1` 为空，`卡2 = ICCID_A`，`卡3 = ICCID_B`
- **WHEN** 系统解析该行
- **THEN** 系统将 `ICCID_A` 识别为槽位2的卡
- **AND** 系统将 `ICCID_B` 识别为槽位3的卡
- **AND** 系统不得将其重排为槽位1和槽位2

#### Scenario: 填写槽位超出最大SIM槽数

- **GIVEN** 某行 `最大SIM槽数 = 2`
- **AND** `卡3 = ICCID_C`
- **WHEN** 系统解析该行
- **THEN** 该行导入失败
- **AND** 失败原因为"卡槽填写超出最大SIM槽数"

#### 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)不能为空"
- **AND** 其他合法行继续导入，不因此行中断

---

### Requirement: 设备导入任务执行

系统 SHALL 异步执行设备导入任务，逐行处理 Excel 数据。

**处理规则**:
- 打开Excel文件，选择第一个sheet（或优先"导入数据"sheet）
- 跳过第1行（表头行），从第2行开始按固定列位置解析数据
- 若整行目标列皆为空，视为空行跳过，不计入 total
- 逐行解析数据，并保留每个 ICCID 对应的原始槽位编号
- 对每行数据执行以下校验：
  1. 设备号是否已存在（已存在则跳过）
  2. ICCID 是否存在于系统中（不存在则失败）
  3. ICCID 是否已绑定其他设备（已绑定则失败）
  4. 已填写 ICCID 的槽位是否超出该设备 `max_sim_slots`（超出则失败）
- 校验通过后：
  1. 创建设备记录
  2. 按 ICCID 的原始槽位创建 `slot_position` 一致的设备-卡绑定记录
- 记录处理结果（成功/跳过/失败）

**任务状态**:
- 1: 待处理
- 2: 处理中
- 3: 已完成
- 4: 失败

#### Scenario: 导入成功

- **WHEN** Excel 中所有设备号不重复且 ICCID 有效
- **THEN** 系统创建所有设备和绑定记录，任务状态为"已完成"

#### Scenario: 前置空槽导入成功

- **GIVEN** 某行 `卡1` 为空，`卡2 = ICCID_A`，`卡3 = ICCID_B`
- **WHEN** 该行校验通过并执行导入
- **THEN** 系统创建两条绑定记录
- **AND** `ICCID_A` 的 `slot_position = 2`
- **AND** `ICCID_B` 的 `slot_position = 3`

#### Scenario: 部分导入成功

- **WHEN** Excel 中部分设备号已存在或部分 ICCID 无效
- **THEN** 系统只导入有效的行，记录跳过和失败的详情，任务状态为"已完成"

#### Scenario: ICCID 不存在

- **WHEN** Excel 中某行的 ICCID 在系统中不存在
- **THEN** 该行导入失败，记录失败原因"ICCID 不存在"

#### Scenario: ICCID 已绑定其他设备

- **WHEN** Excel 中某行的 ICCID 已绑定到其他设备
- **THEN** 该行导入失败，记录失败原因"ICCID 已绑定其他设备"

#### Scenario: 设备号重复

- **WHEN** Excel 中某行的设备号在系统中已存在
- **THEN** 该行被跳过，记录跳过原因"设备号已存在"

#### Scenario: 填写槽位超出最大SIM槽数时任务记录失败

- **WHEN** Excel 中某行声明 `最大SIM槽数 = 2`，但填写了 `卡3` 或 `卡4`
- **THEN** 该行计入失败（`fail_count++`）
- **AND** 失败明细中的 `reason` 字段为"卡槽填写超出最大SIM槽数"

#### Scenario: 导入任务结果报告包含 VirtualNo 失败原因

- **WHEN** 导入任务处理完毕，含有 VirtualNo 为空的行
- **THEN** 失败明细列表中，该行的 `reason` 字段为"设备虚拟号(virtual_no)不能为空"，`line` 字段为对应行号

---

### Requirement: 设备导入任务列表查询

系统 SHALL 提供设备导入任务列表查询功能，仅平台用户可操作。

**API 端点**: `GET /api/admin/devices/import/tasks`

**查询条件**:
- `status`（可选）: 任务状态 1-4
- `batch_no`（可选）: 批次号，模糊匹配
- `start_time`（可选）: 创建时间起始
- `end_time`（可选）: 创建时间结束

**分页**:
- 默认每页 20 条，最大每页 100 条

**响应字段**:
- `id`: 任务 ID
- `task_no`: 任务编号
- `status`: 任务状态
- `status_text`: 任务状态文本
- `batch_no`: 批次号
- `file_name`: 文件名
- `total_count`: 总数
- `success_count`: 成功数
- `skip_count`: 跳过数
- `fail_count`: 失败数
- `started_at`: 开始时间
- `completed_at`: 完成时间
- `error_message`: 错误信息
- `created_at`: 创建时间

**权限**: 仅平台用户

#### Scenario: 查询导入任务列表

- **WHEN** 平台管理员查询导入任务列表
- **THEN** 系统返回所有导入任务，按创建时间倒序排列

#### Scenario: 按状态筛选任务

- **WHEN** 平台管理员查询状态为 3（已完成）的任务
- **THEN** 系统只返回已完成的任务

#### Scenario: 代理尝试查询导入任务

- **WHEN** 代理用户尝试查询导入任务
- **THEN** 系统返回 403 错误，提示"无权限执行此操作"

---

### Requirement: 设备导入任务详情查询

系统 SHALL 提供设备导入任务详情查询功能，包含跳过和失败记录的详细信息。

**API 端点**: `GET /api/admin/devices/import/tasks/:id`

**响应字段**:
- 包含任务列表的所有字段
- `skipped_items`: 跳过记录详情列表
  - `line`: 行号
  - `device_no`: 设备号
  - `reason`: 跳过原因
- `failed_items`: 失败记录详情列表
  - `line`: 行号
  - `device_no`: 设备号
  - `reason`: 失败原因

**权限**: 仅平台用户

#### Scenario: 查询导入任务详情

- **WHEN** 平台管理员查询导入任务详情（ID=1）
- **THEN** 系统返回任务的完整信息，包括跳过和失败记录详情

#### Scenario: 查询不存在的任务

- **WHEN** 平台管理员查询不存在的任务（ID=999）
- **THEN** 系统返回 404 错误，提示"导入任务不存在"

---

### Requirement: 设备导入批次支持实名策略配置

系统 SHALL 在设备导入请求（`ImportDeviceRequest`）中支持 `realname_policy` 参数，以批次为单位指定导入设备的实名策略，默认为 `none`。

**请求字段新增**：
- `realname_policy`：实名认证策略（枚举：`none` / `before_order` / `after_order`，可选，默认 `none`）

**任务字段新增**：
- `DeviceImportTask` 新增 `realname_policy` 字段（VARCHAR(20) NOT NULL DEFAULT 'none'）

**导入行为**：
- 该批次导入的所有设备统一使用 `realname_policy` 的值写入 `Device.realname_policy`，不支持同一批次混用
- 导入任务记录该字段用于审计

#### Scenario: 不传 realname_policy 时默认为 none
- **WHEN** 设备导入请求未包含 `realname_policy` 字段
- **THEN** 导入的所有设备 `realname_policy` 为 `none`

#### Scenario: 指定 before_order 导入设备
- **WHEN** 设备导入请求中 `realname_policy` = `before_order`
- **THEN** 该批次导入的所有设备 `realname_policy` 均为 `before_order`

#### Scenario: 传入无效 realname_policy 被拒绝
- **WHEN** 设备导入请求中 `realname_policy` = `unknown`
- **THEN** 系统返回参数校验错误，提示实名策略值无效

#### Scenario: 设备导入任务响应返回 realname_policy
- **WHEN** 管理员查询设备导入任务列表或详情
- **THEN** 响应中包含 `realname_policy` 字段
