## 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** 系统拒绝回收,返回错误"已绑定设备的卡不能单独回收"