# Device Management

## Purpose

管理物联网设备（如 GPS 追踪器、智能传感器），支持设备与 IoT 卡的绑定关系、设备列表查询、设备分配和回收。设备是比单卡更高一层的管理维度，一个设备可绑定 1-4 张 IoT 卡。

## ADDED Requirements

### Requirement: 设备列表查询

系统 SHALL 提供设备列表查询功能，支持多维度筛选和分页。

**查询条件**:
- `device_no`（可选）: 设备号，支持模糊匹配
- `device_name`（可选）: 设备名称，支持模糊匹配
- `status`（可选）: 设备状态，枚举值 1-在库 | 2-已分销 | 3-已激活 | 4-已停用
- `shop_id`（可选）: 店铺 ID，NULL 表示平台库存
- `batch_no`（可选）: 批次号，精确匹配
- `device_type`（可选）: 设备类型
- `manufacturer`（可选）: 制造商，支持模糊匹配
- `created_at_start`（可选）: 创建时间起始
- `created_at_end`（可选）: 创建时间结束

**分页**:
- 默认每页 20 条，最大每页 100 条
- 返回总记录数和总页数

**数据权限**:
- 平台用户可查看所有设备
- 代理用户只能查看自己店铺及下级店铺的设备

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

**响应字段**:
- `id`: 设备 ID
- `device_no`: 设备号
- `device_name`: 设备名称
- `device_model`: 设备型号
- `device_type`: 设备类型
- `max_sim_slots`: 最大插槽数
- `manufacturer`: 制造商
- `batch_no`: 批次号
- `shop_id`: 店铺 ID
- `shop_name`: 店铺名称
- `status`: 状态
- `status_name`: 状态名称
- `bound_card_count`: 已绑定卡数量
- `activated_at`: 激活时间
- `created_at`: 创建时间
- `updated_at`: 更新时间

#### Scenario: 平台查询所有设备

- **WHEN** 平台管理员查询设备列表，不带任何筛选条件
- **THEN** 系统返回所有设备，按创建时间倒序排列

#### Scenario: 按设备号模糊查询

- **WHEN** 管理员输入 device_no = "GPS"
- **THEN** 系统返回设备号包含 "GPS" 的所有设备

#### Scenario: 按状态筛选设备

- **WHEN** 管理员查询状态为 1（在库）的设备
- **THEN** 系统只返回在库状态的设备

#### Scenario: 代理查询自己店铺的设备

- **WHEN** 代理用户（店铺 ID=10）查询设备列表
- **THEN** 系统只返回 shop_id 为 10 及其下级店铺的设备

#### Scenario: 查询平台库存设备

- **WHEN** 平台管理员查询 shop_id 为空的设备
- **THEN** 系统返回所有平台库存设备（shop_id = NULL）

---

### Requirement: 设备详情查询

系统 SHALL 提供设备详情查询功能，返回设备的基本信息。

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

**响应字段**:
- 包含设备的所有基本字段
- `shop_name`: 店铺名称（如果有）

**数据权限**:
- 平台用户可查看所有设备
- 代理用户只能查看自己店铺及下级店铺的设备

#### Scenario: 查询设备详情成功

- **WHEN** 管理员查询设备详情（ID=1）
- **THEN** 系统返回该设备的完整基本信息

#### Scenario: 查询不存在的设备

- **WHEN** 管理员查询不存在的设备（ID=999）
- **THEN** 系统返回 404 错误，提示"设备不存在"

#### Scenario: 代理查询无权限的设备

- **WHEN** 代理用户（店铺 ID=10）查询其他店铺的设备（shop_id=20，非下级）
- **THEN** 系统返回 404 错误，提示"设备不存在"

---

### Requirement: 删除设备

系统 SHALL 提供删除设备功能，仅平台用户可操作，执行软删除。

**API 端点**: `DELETE /api/admin/devices/:id`

**业务规则**:
- 仅平台用户可删除设备
- 删除设备时自动解绑该设备上的所有卡
- 执行软删除（设置 deleted_at）

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

#### Scenario: 平台删除设备成功

- **WHEN** 平台管理员删除设备（ID=1）
- **THEN** 系统软删除该设备，并解绑设备上的所有卡

#### Scenario: 代理尝试删除设备

- **WHEN** 代理用户尝试删除设备
- **THEN** 系统返回 403 错误，提示"无权限执行此操作"

#### Scenario: 删除不存在的设备

- **WHEN** 平台管理员删除不存在的设备（ID=999）
- **THEN** 系统返回 404 错误，提示"设备不存在"

---

### Requirement: 获取设备绑定的卡列表

系统 SHALL 提供查询设备绑定的 IoT 卡列表功能。

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

**响应字段**:
- `bindings`: 绑定列表，每个元素包含：
  - `id`: 绑定记录 ID
  - `slot_position`: 插槽位置（1-4）
  - `iot_card_id`: IoT 卡 ID
  - `iccid`: ICCID
  - `msisdn`: 接入号
  - `carrier_name`: 运营商名称
  - `status`: 卡状态
  - `bind_time`: 绑定时间

#### Scenario: 查询设备绑定的卡

- **WHEN** 管理员查询设备（ID=1）绑定的卡
- **THEN** 系统返回该设备所有已绑定的卡信息，按插槽位置排序

#### Scenario: 查询无绑定卡的设备

- **WHEN** 管理员查询没有绑定卡的设备
- **THEN** 系统返回空的绑定列表

---

### Requirement: 绑定卡到设备

系统 SHALL 提供将 IoT 卡绑定到设备指定插槽的功能，仅平台用户可操作。

**API 端点**: `POST /api/admin/devices/:id/cards`

**请求参数**:
- `iot_card_id`: IoT 卡 ID（必填）
- `slot_position`: 插槽位置 1-4（必填）

**业务规则**:
- 仅平台用户可操作
- 插槽位置不能超过设备的 max_sim_slots
- 该插槽必须为空（无已绑定的卡）
- 该卡不能已绑定到其他设备
- 绑定操作不改变卡的 shop_id

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

#### Scenario: 绑定卡到设备成功

- **WHEN** 平台管理员将 IoT 卡（ID=101）绑定到设备（ID=1）的插槽 2
- **THEN** 系统创建绑定记录，返回绑定成功信息

#### Scenario: 绑定到已占用的插槽

- **WHEN** 平台管理员尝试绑定卡到已有卡的插槽
- **THEN** 系统返回错误，提示"该插槽已有绑定的卡"

#### Scenario: 绑定已被绑定的卡

- **WHEN** 平台管理员尝试绑定已绑定到其他设备的卡
- **THEN** 系统返回错误，提示"该卡已绑定到其他设备"

#### Scenario: 插槽位置超出范围

- **WHEN** 平台管理员尝试绑定卡到插槽 5（设备 max_sim_slots=4）
- **THEN** 系统返回错误，提示"插槽位置超出设备最大插槽数"

#### Scenario: 代理尝试绑定卡

- **WHEN** 代理用户尝试绑定卡到设备
- **THEN** 系统返回 403 错误，提示"无权限执行此操作"

---

### Requirement: 解绑设备上的卡

系统 SHALL 提供解绑设备上指定卡的功能，仅平台用户可操作。

**API 端点**: `DELETE /api/admin/devices/:id/cards/:cardId`

**业务规则**:
- 仅平台用户可操作
- 更新绑定记录的 bind_status 为 2（已解绑），记录 unbind_time
- 解绑操作不改变卡的 shop_id

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

#### Scenario: 解绑卡成功

- **WHEN** 平台管理员解绑设备（ID=1）上的卡（ID=101）
- **THEN** 系统更新绑定记录状态为已解绑，返回成功信息

#### Scenario: 解绑不存在的绑定关系

- **WHEN** 平台管理员尝试解绑不存在的绑定关系
- **THEN** 系统返回错误，提示"该卡未绑定到此设备"

#### Scenario: 代理尝试解绑卡

- **WHEN** 代理用户尝试解绑设备上的卡
- **THEN** 系统返回 403 错误，提示"无权限执行此操作"

---

### Requirement: 批量分配设备

系统 SHALL 提供批量分配设备给下级店铺的功能，分配时自动同步绑定卡的归属。

**API 端点**: `POST /api/admin/devices/allocate`

**请求参数**:
- `target_shop_id`: 目标店铺 ID（必填）
- `device_ids`: 设备 ID 列表（必填，最多 100 个）
- `remark`: 备注（可选）

**业务规则**:
- 只能分配给直属下级店铺，不可跨级
- 平台只能分配 shop_id=NULL 的设备
- 代理只能分配自己店铺的设备
- 分配后：
  - 设备的 shop_id 变更为目标店铺 ID
  - 设备绑定的所有卡的 shop_id 也变更为目标店铺 ID
  - 设备状态变为「已分销」(2)
- 创建资产分配记录（asset_type='device'）

**响应**:
- `success_count`: 成功数量
- `fail_count`: 失败数量
- `failed_items`: 失败详情列表

#### Scenario: 平台分配设备给一级代理

- **WHEN** 平台管理员将 5 台设备分配给一级代理店铺（ID=10）
- **THEN** 系统更新这 5 台设备及其绑定卡的 shop_id 为 10，创建分配记录，返回成功数量

#### Scenario: 代理分配设备给下级

- **WHEN** 代理（店铺 ID=10）将 3 台设备分配给直属下级店铺（ID=101）
- **THEN** 系统更新这 3 台设备及其绑定卡的 shop_id 为 101，创建分配记录

#### Scenario: 分配给非直属下级

- **WHEN** 代理（店铺 ID=10）尝试分配设备给非直属下级店铺（ID=1011，是 101 的下级）
- **THEN** 系统返回错误，提示"只能分配给直属下级店铺"

#### Scenario: 分配不属于自己的设备

- **WHEN** 代理（店铺 ID=10）尝试分配其他店铺的设备
- **THEN** 系统跳过这些设备，只分配属于自己的设备

---

### Requirement: 批量回收设备

系统 SHALL 提供批量回收已分配设备的功能，回收时自动同步绑定卡的归属。

**API 端点**: `POST /api/admin/devices/recall`

**请求参数**:
- `device_ids`: 设备 ID 列表（必填，最多 100 个）
- `remark`: 备注（可选）

**业务规则**:
- 只能回收直属下级店铺的设备，不可跨级
- 平台回收后：设备和绑定卡的 shop_id 变为 NULL
- 代理回收后：设备和绑定卡的 shop_id 变为执行回收的店铺 ID
- 创建资产回收记录（asset_type='device'）

**响应**:
- `success_count`: 成功数量
- `fail_count`: 失败数量
- `failed_items`: 失败详情列表

#### Scenario: 平台回收一级代理的设备

- **WHEN** 平台管理员回收一级代理店铺（ID=10）的 3 台设备
- **THEN** 系统更新这 3 台设备及其绑定卡的 shop_id 为 NULL，创建回收记录

#### Scenario: 代理回收下级的设备

- **WHEN** 代理（店铺 ID=10）回收下级店铺（ID=101）的 2 台设备
- **THEN** 系统更新这 2 台设备及其绑定卡的 shop_id 为 10，创建回收记录

#### Scenario: 回收非直属下级的设备

- **WHEN** 代理（店铺 ID=10）尝试回收非直属下级的设备
- **THEN** 系统返回错误，提示"只能回收直属下级店铺的设备"
