Files
junhong_cmp_fiber/openspec/specs/iot-card/spec.md
huang a924e63e68
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m42s
feat: 实现物联网卡独立管理和批量导入功能
新增物联网卡独立管理模块,支持单卡查询、批量导入和状态管理。主要变更包括:

功能特性:
- 新增物联网卡 CRUD 接口(查询、分页列表、删除)
- 支持 CSV/Excel 批量导入物联网卡
- 实现异步导入任务处理和进度跟踪
- 新增 ICCID 号码格式校验器(支持 Luhn 算法)
- 新增 CSV 文件解析工具(支持编码检测和错误处理)

数据库变更:
- 移除 iot_card 和 device 表的 owner_id/owner_type 字段
- 新增 iot_card_import_task 导入任务表
- 为导入任务添加运营商类型字段

测试覆盖:
- 新增 IoT 卡 Store 层单元测试
- 新增 IoT 卡导入任务单元测试
- 新增 IoT 卡集成测试(包含导入流程测试)
- 新增 CSV 工具和 ICCID 校验器测试

文档更新:
- 更新 OpenAPI 文档(新增 7 个 IoT 卡接口)
- 归档 OpenSpec 变更提案
- 更新 API 文档规范和生成器指南

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 11:03:43 +08:00

16 KiB

IoT Card Management

Purpose

Manage IoT cards (SIM cards) for the IoT management system, including inventory management, distribution, activation, status tracking, and Gateway integration.

This capability supports:

  • IoT card entity definition and lifecycle management
  • Platform self-operation and agent distribution models
  • Integration with Gateway project for real-time status synchronization
  • Batch import and multi-dimensional querying
  • Support for normal cards (require real-name verification) and industry cards (no real-name required)

Requirements

Requirement: IoT 卡实体定义

系统 SHALL 定义 IoT 卡(IotCard)实体,包含 IoT 卡(物联网卡/流量卡/SIM卡)的商品属性、状态属性、所有权信息和 Gateway 集成字段。

核心概念: IoT 卡 = 物联网卡 = SIM 卡 = 网卡 = 流量卡(同一个东西,不同叫法)。系统使用 ICCID 作为 IoT 卡的唯一标识。

卡业务类型:

  • 普通卡(normal): 需要实名认证才能激活使用,遵循运营商实名制要求
  • 行业卡(industry): 不需要实名认证,可以直接激活使用,适用于企业/行业客户批量采购场景

实体字段:

商品属性:

  • id: IoT 卡 ID(主键,BIGINT)
  • iccid: ICCID(VARCHAR(50),唯一,国际移动用户识别码,IoT卡的唯一标识)
  • 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: 成本价(DECIMAL(10,2),平台进货价)
  • distribute_price: 分销价(DECIMAL(10,2),分销给代理的价格,仅当 owner_type 为 agent 时有值)

所有权和状态:

  • status: IoT 卡状态(INT,1-在库 2-已分销 3-已激活 4-已停用)
  • owner_type: 所有者类型(VARCHAR(20),"platform"-平台自营 | "agent"-代理商 | "user"-用户 | "device"-设备)
  • owner_id: 所有者 ID(BIGINT,platform 时为 0,agent/user/device 时为对应的 ID)
  • 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,自动填充)

Scenario: 创建平台自营 IoT 卡

  • WHEN 平台批量导入 IoT 卡数据,ICCID 为 "89860123456789012345"
  • THEN 系统创建 IoT 卡记录,owner_type 为 "platform",owner_id 为 0,状态为 1(在库),activation_status 为 0(未激活)

Scenario: 平台分销 IoT 卡给代理

  • WHEN 平台将在库 IoT 卡分销给代理商(用户 ID 为 123),设置分销价为 50.00 元
  • THEN 系统将 IoT 卡状态从 1(在库) 变更为 2(已分销),owner_type 变更为 "agent",owner_id 设置为 123,distribute_price 设置为 50.00

Scenario: IoT 卡绑定到设备

  • WHEN 用户将 IoT 卡(ICCID 为 "8986...")绑定到设备(ID 为 1001)
  • THEN 系统在 device_sim_bindings 表创建绑定记录,IoT 卡的 owner_type 变更为 "device",owner_id 变更为 1001

Scenario: IoT 卡直接销售给用户

  • WHEN 平台或代理将 IoT 卡直接销售给用户(用户 ID 为 2001)
  • THEN 系统创建套餐订单记录,IoT 卡的 owner_type 变更为 "user",owner_id 变更为 2001

Scenario: 行业卡无需实名认证

  • WHEN 创建卡业务类型为 "industry"(行业卡)的 IoT 卡
  • THEN 系统允许该卡在 real_name_status 为 0(未实名)的情况下激活使用,不强制要求实名认证

Scenario: 普通卡需要实名认证

  • WHEN 创建卡业务类型为 "normal"(普通卡)的 IoT 卡
  • THEN 系统要求该卡必须先完成实名认证(real_name_status 为 1)才能激活使用

Requirement: IoT 卡状态流转

系统 SHALL 管理 IoT 卡的状态流转,确保状态变更符合业务规则。

状态定义:

  • 1-在库: IoT 卡在平台库存中,未分销
  • 2-已分销: IoT 卡已分销给代理商,代理可销售
  • 3-已激活: IoT 卡已被终端用户激活使用
  • 4-已停用: IoT 卡已停用,不可使用

状态流转规则:

  • 在库(1) → 已分销(2): 平台分销给代理
  • 在库(1) → 已激活(3): 平台自营直接销售给用户并激活
  • 已分销(2) → 已激活(3): 代理销售给用户并激活
  • 已激活(3) → 已停用(4): 用户或平台主动停用
  • 已停用(4) → 已激活(3): 用户或平台主动复机(仅在符合业务规则时)

Scenario: 代理销售 IoT 卡给用户

  • WHEN 代理商销售已分销 IoT 卡给终端用户并激活
  • THEN 系统将 IoT 卡状态从 2(已分销) 变更为 3(已激活),activated_at 记录激活时间,activation_status 从 Gateway 同步后变更为 1

Scenario: 平台自营销售 IoT 卡

  • WHEN 平台直接销售在库 IoT 卡给终端用户并激活
  • THEN 系统将 IoT 卡状态从 1(在库) 变更为 3(已激活),owner_type 保持 "platform",activated_at 记录激活时间

Scenario: 停用已激活 IoT 卡

  • WHEN 用户或平台停用已激活 IoT 卡
  • THEN 系统将 IoT 卡状态从 3(已激活) 变更为 4(已停用),通过 Gateway API 执行停机操作

Requirement: IoT 卡平台自营和代理分销

系统 SHALL 支持 IoT 卡的平台自营销售和代理分销两种模式,通过 owner_typeowner_id 区分所有者。

平台自营:

  • owner_type 为 "platform"
  • owner_id 为 0
  • 平台直接销售给终端用户
  • 销售价格由平台自主定价

代理分销:

  • owner_type 为 "agent"
  • owner_id 为代理用户 ID
  • 代理商可以销售给终端用户或下级代理
  • 分销价格由平台设置(distribute_price),代理商可在分销价基础上加价(但不能超过 2 倍)

Scenario: 查询平台自营 IoT 卡库存

  • WHEN 查询平台自营 IoT 卡库存
  • THEN 系统返回 owner_type 为 "platform" 且 status 为 1(在库) 的 IoT 卡列表

Scenario: 查询代理分销 IoT 卡库存

  • WHEN 代理商(用户 ID 为 123)查询自己的 IoT 卡库存
  • THEN 系统返回 owner_type 为 "agent" 且 owner_id 为 123 且 status 为 2(已分销) 的 IoT 卡列表

Scenario: 代理加价销售 IoT 卡套餐

  • WHEN 代理商为已分销 IoT 卡设置套餐售价
  • THEN 系统校验套餐售价不超过分销价的 2 倍,校验通过后允许销售

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、失败原因

Requirement: IoT 卡查询和筛选

系统 SHALL 支持多维度查询和筛选 IoT 卡,包括状态、所有者、批次号、卡类型等。

查询条件:

  • ICCID(精确匹配或模糊匹配)
  • IoT 卡状态(单选或多选)
  • 所有者类型(platform | agent | user | device)
  • 所有者 ID(仅当所有者类型为 agent/user/device 时有效)
  • 批次号(精确匹配)
  • 卡类型(单选或多选)
  • 运营商 ID(单选或多选,从 carriers 表选择)
  • 激活状态(0-未激活 | 1-已激活)
  • 实名状态(0-未实名 | 1-已实名)
  • 网络状态(0-停机 | 1-开机)
  • 是否参与轮询(true | false)
  • 激活时间范围(开始时间 - 结束时间)
  • 创建时间范围(开始时间 - 结束时间)

分页:

  • 默认每页 20 条,最大每页 100 条
  • 返回总记录数和总页数

Scenario: 查询特定批次的在库 IoT 卡

  • WHEN 平台查询批次号为 "BATCH-2025-001" 且状态为 1(在库) 的 IoT 卡
  • THEN 系统返回符合条件的 IoT 卡列表,包含 ICCID、类型、运营商、成本价等信息

Scenario: 代理查询自己的已分销 IoT 卡

  • WHEN 代理商(用户 ID 为 123)查询自己的已分销 IoT 卡
  • THEN 系统返回 owner_type 为 "agent" 且 owner_id 为 123 且 status 为 2(已分销) 的 IoT 卡列表

Scenario: 分页查询 IoT 卡

  • WHEN 平台查询在库 IoT 卡,指定每页 50 条,查询第 2 页
  • THEN 系统返回第 51-100 条 IoT 卡记录,以及总记录数和总页数

Requirement: Gateway 集成

系统 SHALL 预留 IoT 卡状态相关字段,用于后续与 Gateway 项目集成。

集成字段:

  • activation_status: 激活状态(从 Gateway 同步)
  • real_name_status: 实名状态(从 Gateway 同步)
  • network_status: 网络状态(从 Gateway 同步)
  • data_usage_mb: 累计流量使用(从 Gateway 同步)
  • last_sync_time: 最后同步时间

集成说明:

  • 本阶段只设计数据模型字段,不实现 Gateway HTTP 客户端代码
  • 后续 Service 层将调用 Gateway API 获取 IoT 卡状态并更新这些字段
  • Gateway 使用 AES 加密 + MD5 签名的统一传输协议(参考 design.md)

Gateway API 功能:

  • 查询 IoT 卡状态(激活状态、实名状态、网络状态)
  • 查询流量详情(累计流量使用、剩余流量)
  • 停复机操作(停机、复机)
  • 实名认证操作

Scenario: 预留 Gateway 集成字段

  • WHEN 创建 IoT 卡记录
  • THEN 系统初始化 Gateway 相关字段为默认值:activation_status 为 0,real_name_status 为 0,network_status 为 0,data_usage_mb 为 0,last_sync_time 为空

Scenario: 从 Gateway 同步 IoT 卡状态

  • WHEN Service 层调用 Gateway API 查询 IoT 卡状态
  • THEN 系统更新 IoT 卡的 activation_statusreal_name_statusnetwork_statusdata_usage_mblast_sync_time 字段

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,最多 2 位小数
  • 分销价(distribute_price):可选,≥ 0,最多 2 位小数,≥ 成本价
  • 所有者类型(owner_type):必填,枚举值 "platform" | "agent" | "user" | "device"
  • 所有者 ID(owner_id):必填,≥ 0,当 owner_type 为 "platform" 时必须为 0
  • 激活状态(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 卡,ICCID 长度为 15(小于 19)
  • THEN 系统拒绝创建,返回错误信息"ICCID 长度必须为 19-20 字符"

Scenario: 创建 IoT 卡时 ICCID 重复

  • WHEN 平台创建 IoT 卡,ICCID 为已存在的 "89860123456789012345"
  • THEN 系统拒绝创建,返回错误信息"ICCID 已存在"

Scenario: 创建 IoT 卡时成本价为负数

  • WHEN 平台创建 IoT 卡,成本价为 -10.00
  • THEN 系统拒绝创建,返回错误信息"成本价必须 ≥ 0"

Scenario: 创建 IoT 卡时分销价低于成本价

  • WHEN 平台创建 IoT 卡,成本价为 50.00,分销价为 40.00
  • THEN 系统拒绝创建,返回错误信息"分销价不能低于成本价"

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 卡列表