# IoT Card Management - Delta Spec ## MODIFIED Requirements ### Requirement: IoT 卡实体定义 系统 SHALL 定义 IoT 卡(IotCard)实体,包含 IoT 卡(物联网卡/流量卡/SIM卡)的商品属性、状态属性、店铺归属信息和 Gateway 集成字段。 **核心概念**: IoT 卡 = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)。系统使用 ICCID 作为 IoT 卡的唯一标识。 **卡业务类型**: - **普通卡(normal)**: 需要实名认证才能激活使用,遵循运营商实名制要求 - **行业卡(industry)**: 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景 **实体字段**: **商品属性**: - `id`: IoT 卡 ID(主键,BIGINT) - `iccid`: ICCID(VARCHAR(20),唯一,IoT卡的唯一标识,电信19位/联通移动广电20位,支持字母数字混合) - `card_type`: 卡类型(VARCHAR(50),如 "4G"、"5G"、"NB-IoT") - `card_category`: 卡业务类型(VARCHAR(20),枚举值:"normal"-普通卡 | "industry"-行业卡,默认 "normal") - `carrier_id`: 运营商 ID(BIGINT,关联 carriers 表,如中国移动、中国联通、中国电信、中国广电) - `imsi`: IMSI(VARCHAR(50),可选,国际移动用户识别码) - `msisdn`: 手机号码(VARCHAR(20),可选,即卡接入号) - `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯) - `supplier`: 供应商名称(VARCHAR(255),可选) - `cost_price`: 成本价(BIGINT,分为单位,平台进货价) - `distribute_price`: 分销价(BIGINT,分为单位,分销给代理的价格) **店铺归属和状态**: - `shop_id`: 店铺 ID(BIGINT,可空,NULL 表示平台所有,有值表示店铺所有) - `status`: IoT 卡状态(INT,1-在库 2-已分销 3-已激活 4-已停用) - `activated_at`: 激活时间(TIMESTAMP,可空) **Gateway 集成字段**(从 Gateway 项目同步): - `activation_status`: 激活状态(INT,0-未激活 1-已激活) - `real_name_status`: 实名状态(INT,0-未实名 1-已实名) - `network_status`: 网络状态(INT,0-停机 1-开机) - `data_usage_mb`: 累计流量使用(BIGINT,MB 为单位,默认 0) - `last_sync_time`: 最后一次与 Gateway 同步时间(TIMESTAMP,可空) **轮询控制字段**: - `enable_polling`: 是否参与轮询(BOOLEAN,默认 true,用于控制是否对该卡进行定时轮询) - `last_data_check_at`: 最后一次卡流量检查时间(TIMESTAMP,可空) - `last_real_name_check_at`: 最后一次实名检查时间(TIMESTAMP,可空) **系统字段**: - `created_at`: 创建时间(TIMESTAMP,自动填充) - `updated_at`: 更新时间(TIMESTAMP,自动填充) - `creator`: 创建人 ID(BIGINT) - `updater`: 更新人 ID(BIGINT) #### Scenario: 创建平台库存 IoT 卡 - **WHEN** 平台批量导入 IoT 卡数据,ICCID 为 "89860123456789012345" - **THEN** 系统创建 IoT 卡记录,`shop_id` 为 NULL(平台所有),状态为 1(在库),`activation_status` 为 0(未激活) #### Scenario: 平台分销 IoT 卡给代理店铺 - **WHEN** 平台将在库 IoT 卡分销给代理店铺(店铺 ID 为 10),设置分销价为 5000 分 - **THEN** 系统将 IoT 卡状态从 1(在库) 变更为 2(已分销),`shop_id` 设置为 10,`distribute_price` 设置为 5000 #### Scenario: 代理店铺分销 IoT 卡给下级店铺 - **WHEN** 代理店铺(ID 为 10)将已分销 IoT 卡分销给下级店铺(ID 为 20) - **THEN** 系统将 IoT 卡的 `shop_id` 从 10 变更为 20,状态保持 2(已分销) --- ### Requirement: IoT 卡平台自营和代理分销 系统 SHALL 支持 IoT 卡的平台自营销售和代理分销两种模式,通过 `shop_id` 区分所有者。 **平台自营**: - `shop_id` 为 NULL - 平台直接销售给终端用户 - 销售价格由平台自主定价 **代理分销**: - `shop_id` 为店铺 ID - 代理商可以销售给终端用户或下级代理 - 分销价格由上级设置(`distribute_price`),代理商可在分销价基础上加价 **企业授权**: - 企业用户没有 IoT 卡所有权 - 通过 `EnterpriseCardAuthorization` 表管理企业对 IoT 卡的访问权限 - 授权由店铺发起,企业只能操作被授权的卡 #### Scenario: 查询平台自营 IoT 卡库存 - **WHEN** 查询平台自营 IoT 卡库存 - **THEN** 系统返回 `shop_id` 为 NULL 且 `status` 为 1(在库) 的 IoT 卡列表 #### Scenario: 查询代理店铺 IoT 卡库存 - **WHEN** 代理店铺(ID 为 10)查询自己的 IoT 卡库存 - **THEN** 系统返回 `shop_id` 为 10 且 `status` 为 2(已分销) 的 IoT 卡列表 --- ### Requirement: IoT 卡数据校验 系统 SHALL 对 IoT 卡数据进行校验,确保数据完整性和一致性。 **校验规则**: - ICCID(iccid):必填,字母数字混合,长度根据运营商:电信19位,联通/移动/广电20位,唯一 - 卡类型(card_type):必填,长度 1-50 字符 - 卡业务类型(card_category):必填,枚举值 "normal"(普通卡) | "industry"(行业卡),默认 "normal" - 运营商 ID(carrier_id):必填,≥ 1,必须是有效的运营商 ID - 成本价(cost_price):必填,≥ 0,单位为分 - 分销价(distribute_price):可选,≥ 0,单位为分,≥ 成本价 - 店铺 ID(shop_id):可选,NULL 表示平台所有,有值必须是有效的店铺 ID - 激活状态(activation_status):必填,枚举值 0(未激活) | 1(已激活) - 实名状态(real_name_status):必填,枚举值 0(未实名) | 1(已实名),当 card_category 为 "industry"(行业卡)时可以保持 0 - 网络状态(network_status):必填,枚举值 0(停机) | 1(开机) - 轮询开关(enable_polling):必填,布尔值 true | false #### Scenario: 创建电信 IoT 卡时 ICCID 长度校验 - **WHEN** 创建电信 IoT 卡(carrier_id 对应电信),ICCID 长度为 20 - **THEN** 系统拒绝创建,返回错误信息"电信 ICCID 必须为 19 位" #### Scenario: 创建移动 IoT 卡时 ICCID 长度校验 - **WHEN** 创建移动 IoT 卡(carrier_id 对应移动),ICCID 长度为 19 - **THEN** 系统拒绝创建,返回错误信息"该运营商 ICCID 必须为 20 位" #### Scenario: 创建 IoT 卡时 ICCID 包含字母 - **WHEN** 创建移动 IoT 卡,ICCID 为 "8986001234567890123A"(包含字母 A) - **THEN** 系统允许创建,因为移动 ICCID 支持字母 #### Scenario: 创建 IoT 卡时 ICCID 重复 - **WHEN** 平台创建 IoT 卡,ICCID 为已存在的 "89860123456789012345" - **THEN** 系统拒绝创建,返回错误信息"ICCID 已存在" --- ## ADDED Requirements ### Requirement: 单卡列表查询 系统 SHALL 提供单卡列表查询功能,用于管理未绑定设备的 IoT 卡资产。 **单卡定义**: 单卡是指未绑定到任何设备的 IoT 卡,即在 `device_sim_bindings` 表中不存在 `bind_status = 1`(已绑定) 的记录。 **查询条件**: - 套餐 ID(package_id): 可选,筛选已购买指定套餐的卡 - 是否分销(is_distributed): 可选,true-已分销 false-未分销 - 卡号状态(status): 可选,1-在库 2-已分销 3-已激活 4-已停用 - 运营商(carrier_id): 可选,运营商 ID - 分销商 ID(shop_id): 可选,店铺 ID - 网卡号段(iccid_range): 可选,格式 "起始ICCID-结束ICCID" - ICCID: 可选,模糊匹配 - 卡接入号(msisdn): 可选,模糊匹配 - 是否换卡(is_replaced): 可选,true-有换卡记录 false-无换卡记录 **分页**: - 默认每页 20 条,最大每页 100 条 - 返回总记录数和总页数 **数据权限**: - 基于 shop_id 自动应用数据权限过滤 - 代理只能看到自己店铺及下级店铺的卡 #### Scenario: 查询未绑定设备的单卡列表 - **WHEN** 管理员查询单卡列表 - **THEN** 系统返回所有未绑定设备的 IoT 卡(在 device_sim_bindings 中无 bind_status=1 记录的卡) #### Scenario: 按运营商筛选单卡 - **WHEN** 管理员查询运营商 ID 为 1(电信)的单卡 - **THEN** 系统返回 carrier_id = 1 且未绑定设备的 IoT 卡列表 #### Scenario: 按网卡号段筛选单卡 - **WHEN** 管理员查询 ICCID 号段为 "8986001000000000000-8986001999999999999" 的单卡 - **THEN** 系统返回 ICCID 在该号段范围内且未绑定设备的 IoT 卡列表 #### Scenario: 按是否换卡筛选单卡 - **WHEN** 管理员查询有换卡记录的单卡(is_replaced=true) - **THEN** 系统返回在 card_replacement_records 表中有记录的 IoT 卡列表 --- ## MODIFIED Requirements ### Requirement: IoT 卡批量导入 系统 SHALL 支持通过 CSV 文件批量导入 IoT 卡 ICCID,支持大批量数据(几万条),异步处理并跟踪导入进度。 **导入方式**: - 上传 CSV 文件,每行一个 ICCID - 在界面选择运营商、批次号等公共参数 - 不支持一次导入多种运营商的卡 **导入参数**: - CSV 文件(必填): 仅包含 ICCID 列 - 运营商 ID(必填): 在界面选择 - 批次号(可选): 在界面填写 **校验规则**: - ICCID 格式校验: 字母数字混合,长度根据运营商(电信19位,其他20位) - ICCID 唯一性校验: 重复 ICCID 跳过,不中断导入 **处理规则**: - 异步处理: 创建导入任务后立即返回任务 ID - 分批处理: 每批 1000 条 - 重复处理: 跳过已存在的 ICCID,记录跳过原因 - 格式错误: 记录失败原因,继续处理其他行 **导入结果**: - 总数(total_count) - 成功数(success_count) - 跳过数(skip_count): 因重复等原因跳过 - 失败数(fail_count): 因格式错误等原因失败 - 跳过详情: 包含行号、ICCID、原因 - 失败详情: 包含行号、ICCID、原因 #### Scenario: 发起 IoT 卡批量导入 - **WHEN** 管理员上传包含 10000 个 ICCID 的 CSV 文件,选择运营商为电信,批次号为 "BATCH-2025-001" - **THEN** 系统创建导入任务,返回任务 ID,后台异步处理导入 #### Scenario: 导入时跳过重复 ICCID - **WHEN** CSV 文件中的 ICCID "8986001234567890123" 已存在于系统中 - **THEN** 系统跳过该 ICCID,记录跳过原因为"ICCID 已存在",继续处理其他 ICCID #### Scenario: 导入时记录格式错误 - **WHEN** CSV 文件第 100 行的 ICCID "12345" 长度不符合电信卡要求(19位) - **THEN** 系统记录失败,原因为"电信 ICCID 必须为 19 位",行号为 100,继续处理其他 ICCID #### Scenario: 查询导入任务进度 - **WHEN** 管理员查询导入任务(ID 为 1)的进度 - **THEN** 系统返回任务状态、总数、成功数、跳过数、失败数、开始时间、完成时间 #### Scenario: 查询导入任务失败详情 - **WHEN** 管理员查询导入任务(ID 为 1)的失败详情 - **THEN** 系统返回失败记录列表,每条包含行号、ICCID、失败原因