## ADDED Requirements

### Requirement: 单卡分配功能

系统 SHALL 支持将单卡（未绑定设备的 IoT 卡）分配给直属下级店铺，实现资产所有权的层级流转。

**分配规则**:
- 只能分配给直属下级店铺，不可跨级分配
- 平台（shop_id=NULL）只能分配状态为在库(status=1)的卡
- 代理店铺可以分配状态为已分销(status=2)的卡（继续往下分销）
- 分配后状态变更：在库(1)→已分销(2)，已分销(2)保持不变
- 分配后 shop_id 变更为目标店铺 ID
- 分配不涉及费用，纯资产所有权转移
- 分配后上级仍能看到和管理（通过数据权限机制）

**选卡方式**（三选一）:
- ICCID 列表：指定具体的 ICCID 列表
- 号段范围：指定起始 ICCID 和结束 ICCID
- 筛选条件：按运营商、批次号、状态等条件批量选择

**API 端点**: `POST /api/admin/iot-cards/standalone/allocate`

**请求参数**:
- `to_shop_id`（必填）: 目标店铺 ID
- `selection_type`（必填）: 选卡方式，枚举值 "list" | "range" | "filter"
- `iccids`（selection_type=list 时必填）: ICCID 列表
- `iccid_start`（selection_type=range 时必填）: 起始 ICCID
- `iccid_end`（selection_type=range 时必填）: 结束 ICCID
- `carrier_id`（selection_type=filter 时可选）: 运营商 ID
- `batch_no`（selection_type=filter 时可选）: 批次号
- `status`（selection_type=filter 时可选）: 卡状态
- `remark`（可选）: 备注

**响应**:
- `total_count`: 待分配总数
- `success_count`: 成功数
- `fail_count`: 失败数
- `failed_items`: 失败项列表（包含 ICCID 和失败原因）

#### Scenario: 平台通过 ICCID 列表分配单卡给一级代理

- **WHEN** 平台管理员选择 3 张在库单卡（ICCID 列表），分配给一级代理店铺（ID=10）
- **THEN** 系统将这 3 张卡的 shop_id 更新为 10，status 从 1 变为 2，创建分配记录，返回成功数 3

#### Scenario: 平台通过号段范围批量分配单卡

- **WHEN** 平台管理员指定 ICCID 范围 "8986001000000000000" 至 "8986001000000000099"，分配给一级代理店铺（ID=10）
- **THEN** 系统查询该范围内的所有在库单卡，批量更新 shop_id 和 status，创建分配记录

#### Scenario: 代理通过筛选条件分配单卡给下级

- **WHEN** 一级代理（店铺 ID=10）按条件筛选（运营商=电信，批次号=BATCH-001）自己的已分销卡，分配给二级代理店铺（ID=20）
- **THEN** 系统查询符合条件的卡，校验店铺 20 是店铺 10 的直属下级，批量更新 shop_id 为 20，status 保持 2

#### Scenario: 拒绝跨级分配

- **WHEN** 平台尝试将卡直接分配给二级代理店铺（非直属下级）
- **THEN** 系统拒绝分配，返回错误"只能分配给直属下级店铺"

#### Scenario: 拒绝平台分配已分销的卡

- **WHEN** 平台尝试分配状态为已分销(status=2)的卡
- **THEN** 系统拒绝分配，返回错误"在库状态的卡才能分配，请先回收"

#### Scenario: 拒绝分配已绑定设备的卡

- **WHEN** 用户尝试分配已绑定设备的卡（在 device_sim_bindings 中 bind_status=1）
- **THEN** 系统拒绝分配，返回错误"已绑定设备的卡不能单独分配"

---

### Requirement: 单卡回收功能

系统 SHALL 支持上级回收已分配给直属下级的单卡，将卡的所有权收回。

**回收规则**:
- 只有上级可以回收，代理不能主动退回给上级
- 只能回收直属下级的卡，不可跨级回收
- 平台回收：shop_id 变为 NULL，status 变为 1（在库）
- 店铺回收：shop_id 变为执行回收的店铺 ID，status 保持 2（已分销）
- 只能回收单卡（未绑定设备的卡）

**选卡方式**（与分配相同，三选一）:
- ICCID 列表
- 号段范围
- 筛选条件

**API 端点**: `POST /api/admin/iot-cards/standalone/recall`

**请求参数**:
- `from_shop_id`（必填）: 来源店铺 ID（被回收方）
- `selection_type`（必填）: 选卡方式，枚举值 "list" | "range" | "filter"
- `iccids`（selection_type=list 时必填）: ICCID 列表
- `iccid_start`（selection_type=range 时必填）: 起始 ICCID
- `iccid_end`（selection_type=range 时必填）: 结束 ICCID
- `carrier_id`（selection_type=filter 时可选）: 运营商 ID
- `batch_no`（selection_type=filter 时可选）: 批次号
- `remark`（可选）: 备注

**响应**:
- `total_count`: 待回收总数
- `success_count`: 成功数
- `fail_count`: 失败数
- `failed_items`: 失败项列表

#### Scenario: 平台回收一级代理的单卡

- **WHEN** 平台管理员选择一级代理店铺（ID=10）的 5 张单卡进行回收
- **THEN** 系统将这 5 张卡的 shop_id 更新为 NULL，status 从 2 变为 1，创建回收记录

#### Scenario: 一级代理回收二级代理的单卡

- **WHEN** 一级代理（店铺 ID=10）选择二级代理店铺（ID=20）的 3 张单卡进行回收
- **THEN** 系统将这 3 张卡的 shop_id 更新为 10，status 保持 2，创建回收记录

#### Scenario: 拒绝回收非直属下级的卡

- **WHEN** 一级代理（店铺 ID=10）尝试回收非直属下级店铺（ID=30，归属于店铺 ID=20）的卡
- **THEN** 系统拒绝回收，返回错误"只能回收直属下级店铺的卡"

#### Scenario: 拒绝代理主动退回

- **WHEN** 二级代理（店铺 ID=20）尝试将卡退回给上级店铺（ID=10）
- **THEN** 系统拒绝操作，返回错误"不能主动退回卡给上级，请联系上级进行回收"

#### Scenario: 拒绝回收已绑定设备的卡

- **WHEN** 用户尝试回收已绑定设备的卡
- **THEN** 系统拒绝回收，返回错误"已绑定设备的卡不能单独回收"
