# IoT Device Management ## Purpose Manage IoT devices and their bindings with IoT cards (SIM cards), supporting device lifecycle management, device-card binding relationships, device-level package purchases, batch allocation, and remote device operations. This capability supports: - Device entity definition and lifecycle management - Device-IoT card binding relationships (1-4 cards per device) - Device-level package purchases with shared data pool - Batch device allocation to agents - Remote device operations (reboot, password change, reset) ## Requirements ### Requirement: 设备实体定义 系统 SHALL 定义设备(Device)实体,用于管理用户的物联网设备(如 GPS 追踪器、智能传感器等),支持设备与 IoT 卡的绑定关系、设备批量分配和设备操作。 **核心概念**: 设备不在卡管系统中销售,主要用于: 1. 用户设备管理(用户添加自己的设备,绑定 IoT 卡) 2. 方便运营人员管理投诉和代理要求(通过设备维度批量查看绑定的所有 IoT 卡) 3. 设备操作(重启、修改账号密码、重置等) 4. 设备批量分配(运营人员在别的系统报单后发货,把设备和绑定的 IoT 卡一起分配给代理) **实体字段**: **基本属性**: - `id`: 设备 ID(主键,BIGINT) - `device_no`: 设备编号(唯一,VARCHAR(100)) - `device_name`: 设备名称(VARCHAR(255)) - `device_model`: 设备型号(VARCHAR(100)) - `device_type`: 设备类型(VARCHAR(50),如 "GPS Tracker"、"Camera"、"Sensor") - `max_sim_slots`: 最大 IoT 卡插槽数量(INT,1-4,默认 4) - `manufacturer`: 设备制造商(VARCHAR(255),可选) - `batch_no`: 批次号(VARCHAR(100),用于批量导入追溯) **店铺归属和状态**: - `shop_id`: 店铺 ID(BIGINT,可空,NULL 表示平台库存,有值表示店铺所有) - `status`: 设备状态(INT,1-在库 2-已分销 3-已激活 4-已停用) - `activated_at`: 激活时间(TIMESTAMP,可空) **设备操作配置**(预留字段,用于后续设备操作功能): - `device_username`: 设备登录账号(VARCHAR(100),可选) - `device_password_encrypted`: 设备登录密码(加密存储,VARCHAR(255),可选) - `device_api_endpoint`: 设备 API 接口地址(VARCHAR(500),可选) **系统字段**: - `created_at`: 创建时间(TIMESTAMP,自动填充) - `updated_at`: 更新时间(TIMESTAMP,自动填充) - `creator`: 创建人 ID(BIGINT) - `updater`: 更新人 ID(BIGINT) #### Scenario: 用户添加设备 - **WHEN** 用户添加自己的设备(设备编号为 "GPS-001",设备名称为 "物流车辆追踪器") - **THEN** 系统创建设备记录,根据用户归属设置 `shop_id`,状态为 1(在库) #### Scenario: 平台导入设备到库存 - **WHEN** 平台批量导入设备数据(准备发货给代理) - **THEN** 系统创建设备记录,`shop_id` 为 NULL(平台库存),状态为 1(在库) #### Scenario: 运营人员批量分配设备给代理店铺 - **WHEN** 运营人员将平台库存设备(ID 为 1001)分配给代理店铺(ID 为 10) - **THEN** 系统将设备的 `shop_id` 设置为 10,同时自动将该设备绑定的所有 IoT 卡的 `shop_id` 也设置为 10 --- ### Requirement: 设备状态流转 系统 SHALL 管理设备的状态流转,确保状态变更符合业务规则。 **状态定义**: - **1-未激活**: 设备尚未激活使用 - **2-已激活**: 设备已被用户激活使用 - **3-已停用**: 设备已停用,不可使用 **状态流转规则**: - 未激活(1) → 已激活(2): 用户激活设备 - 已激活(2) → 已停用(3): 用户或平台主动停用设备 - 已停用(3) → 已激活(2): 用户或平台主动恢复设备(仅在符合业务规则时) #### Scenario: 用户激活设备 - **WHEN** 用户激活自己的设备 - **THEN** 系统将设备状态从 1(未激活) 变更为 2(已激活),`activated_at` 记录激活时间 #### Scenario: 用户停用设备 - **WHEN** 用户停用已激活的设备 - **THEN** 系统将设备状态从 2(已激活) 变更为 3(已停用),同时可选择是否停用该设备绑定的所有 IoT 卡 --- ### Requirement: 设备与 IoT 卡绑定关系 系统 SHALL 管理设备与 IoT 卡的绑定关系,一个设备可以绑定 1-4 张 IoT 卡。 **绑定规则**: - 一个设备最多绑定 4 张 IoT 卡(由 `max_sim_slots` 字段控制) - 一个 IoT 卡同一时间只能绑定一个设备 - 绑定时记录插槽位置(slot_position: 1, 2, 3, 4) - 绑定时记录绑定时间和绑定状态(1-已绑定 2-已解绑) - 绑定/解绑操作不改变 IoT 卡的 shop_id(所有权由分销操作管理,而非绑定操作) - **新增**: 同一设备的同一插槽同一时间只能绑定一张卡(数据库唯一约束) **中间表 tb_device_sim_binding**: - `id`: 绑定记录 ID(主键,BIGINT) - `device_id`: 设备 ID(BIGINT) - `iot_card_id`: IoT 卡 ID(BIGINT) - `slot_position`: 插槽位置(INT,1-4) - `bind_status`: 绑定状态(INT,1-已绑定 2-已解绑) - `bind_time`: 绑定时间(TIMESTAMP) - `unbind_time`: 解绑时间(TIMESTAMP,可空) - `created_at`: 创建时间(TIMESTAMP,自动填充) - `updated_at`: 更新时间(TIMESTAMP,自动填充) - `deleted_at`: 软删除时间(TIMESTAMP,可空) - `creator`: 创建人 ID(BIGINT) - `updater`: 更新人 ID(BIGINT) **数据库约束**: - `idx_device_sim_bindings_active_card`: 唯一索引 (iot_card_id) WHERE bind_status = 1,防止同一张卡绑定到多个设备 - **新增** `idx_active_device_slot`: 唯一索引 (device_id, slot_position) WHERE bind_status = 1 AND deleted_at IS NULL,防止同一插槽绑定多张卡 **并发安全**: - 系统 SHALL 在数据库层面通过唯一约束防止并发绑定导致的数据不一致 - 系统 SHALL 正确处理唯一约束冲突错误,返回友好的用户提示而非通用数据库错误 #### Scenario: 绑定 IoT 卡到设备 - **WHEN** 用户将 IoT 卡(ID 为 101)绑定到设备(ID 为 1001)的插槽 1 - **THEN** 系统创建绑定记录,`device_id` 为 1001,`iot_card_id` 为 101,`slot_position` 为 1,`bind_status` 为 1(已绑定),`bind_time` 为当前时间 #### Scenario: 解绑 IoT 卡 - **WHEN** 用户解绑设备的 IoT 卡(绑定记录 ID 为 10) - **THEN** 系统将绑定记录的 `bind_status` 从 1(已绑定) 变更为 2(已解绑),`unbind_time` 记录解绑时间,IoT 卡的 `shop_id` 保持不变 #### Scenario: 并发绑定同一张卡到不同设备 - **WHEN** 两个请求同时尝试将同一张 IoT 卡(ID 为 101)绑定到不同设备 - **THEN** 第一个请求成功,第二个请求返回错误"该卡已绑定到其他设备" #### Scenario: 并发绑定不同卡到同一设备插槽 - **WHEN** 两个请求同时尝试将不同 IoT 卡绑定到同一设备(ID 为 1001)的同一插槽(slot_position 为 1) - **THEN** 第一个请求成功,第二个请求返回错误"该插槽已有绑定的卡" --- ### Requirement: 设备套餐购买和流量共享 系统 SHALL 支持用户为设备购买套餐,套餐自动分配到设备绑定的所有 IoT 卡,流量在设备级别共享。 **设备套餐业务规则**: - 用户为设备购买套餐时,套餐会分配到设备绑定的**所有 IoT 卡**(1-4 张) - 套餐的流量是**设备级别共享的**(例如 3000G/月共享,不管用哪张卡) - 分佣**只计算一次**(不按卡数倍增) - 订单表通过 `device_id` 字段关联设备,通过 `device_sim_bindings` 表查找绑定的所有 IoT 卡 **套餐分配示例**: - 设备绑定 3 张 IoT 卡 - 用户购买套餐:399 元/年,每月 3000G 流量,长期佣金 100 元 - 用户支付:399 元 - 套餐分配:设备的 3 张 IoT 卡都获得该套餐 - 流量使用:3000G/月 在 3 张卡之间共享(不是每张卡 3000G,而是总共 3000G) - 分佣:代理获得 100 元分佣(只分一次,不是 3 × 100 元) #### Scenario: 用户为设备购买套餐 - **WHEN** 用户为设备(ID 为 1001,绑定 3 张 IoT 卡)购买套餐(套餐 ID 为 3001,399 元/年,3000G/月) - **THEN** 系统创建套餐订单,`device_id` 为 1001,`package_id` 为 3001,订单金额为 399 元,将套餐分配到设备绑定的 3 张 IoT 卡,设置流量共享模式为设备级别 #### Scenario: 设备级流量共享 - **WHEN** 设备(ID 为 1001)的套餐流量为 3000G/月,设备绑定 3 张 IoT 卡 - **THEN** 系统设置流量共享模式,3 张 IoT 卡共享 3000G/月(不是每张卡 3000G),无论使用哪张卡,都从这个流量池扣除 #### Scenario: 设备套餐分佣 - **WHEN** 用户为设备购买套餐,订单金额为 399 元,代理的长期分佣规则为 100 元 - **THEN** 系统为代理创建一条分佣记录,分佣金额为 100 元(只分一次,不按设备绑定的卡数倍增) --- ### Requirement: 设备批量分配 系统 SHALL 支持运营人员批量分配设备给代理店铺,设备分配时自动分配该设备绑定的所有 IoT 卡。 **分配规则**: - 只能分配 `shop_id` 为 NULL 的设备(平台库存) - 分配时,设备的 `shop_id` 设置为目标店铺 ID - 分配时,设备绑定的所有 IoT 卡的 `shop_id` 也设置为目标店铺 ID - 分配操作记录到操作日志 #### Scenario: 运营人员批量分配设备 - **WHEN** 运营人员将 10 台设备(平台库存)分配给代理店铺(ID 为 10) - **THEN** 系统将这 10 台设备的 `shop_id` 设置为 10,同时将这些设备绑定的所有 IoT 卡的 `shop_id` 也设置为 10 #### Scenario: 分配已分配的设备 - **WHEN** 运营人员尝试分配 `shop_id` 不为 NULL 的设备 - **THEN** 系统拒绝分配,返回错误信息"该设备已分配给店铺,不能重复分配" --- ### Requirement: 设备操作 系统 SHALL 支持对设备的远程操作(重启、修改账号密码、重置等),用于设备管理和故障排查。 **设备操作类型**: - **重启设备**: 远程重启设备 - **修改账号密码**: 修改设备的登录账号和密码 - **重置设备**: 将设备恢复到出厂设置 - **查询设备状态**: 查询设备的在线状态、运行状态等 - **设备配置更新**: 更新设备的配置参数 **操作说明**: - 本阶段只设计数据模型字段和接口定义,不实现设备操作的具体代码 - 后续 Service 层将调用设备厂商提供的 API 或通过 MQTT/HTTP 协议与设备通信 - 设备操作需要记录操作日志(操作类型、操作人、操作时间、操作结果) #### Scenario: 重启设备 - **WHEN** 用户或运营人员请求重启设备(ID 为 1001) - **THEN** 系统调用设备 API 发送重启命令,记录操作日志,返回操作结果 #### Scenario: 修改设备密码 - **WHEN** 用户或运营人员修改设备(ID 为 1001)的登录密码 - **THEN** 系统更新设备的 `device_password_encrypted` 字段(加密存储),调用设备 API 同步密码修改,记录操作日志 --- ### Requirement: 设备批量导入 系统 SHALL 支持批量导入设备数据,用于平台库存管理。 **导入字段**: - 设备编号(必填) - 设备名称(可选) - 设备型号(可选) - 设备类型(可选) - 最大插槽数(可选,默认 4) - 设备制造商(可选) - 批次号(可选,由任务自动生成) - **ICCID 1-4**(可选,用于绑定 IoT 卡) **导入规则**: - 设备编号必须唯一,重复编号将被跳过 - 导入的设备默认 `shop_id` 为 NULL(平台库存),状态为 1(在库) - 导入成功后记录操作日志 **IoT 卡绑定规则**(新增): - 系统 SHALL 校验 ICCID 对应的卡是否存在 - 系统 SHALL 校验卡是否已绑定到其他设备 - **新增**: 系统 SHALL 校验卡的归属权,只允许绑定平台库存的卡(shop_id = NULL) - 如果卡已分配给店铺(shop_id != NULL),系统 SHALL 拒绝绑定并记录原因 **导入结果分类**(新增): - **完全成功**: 设备创建且所有指定的卡都绑定成功 - **部分成功**: 设备创建但部分卡绑定失败(新增 warning 状态) - **跳过**: 设备编号已存在 - **失败**: 设备创建失败或所有指定的卡都不可用 **导入任务模型扩展**(新增): - `warning_count`: 警告数量(部分成功的设备数) - `warning_items`: 警告记录详情(JSONB,记录哪些卡绑定失败及原因) #### Scenario: 批量导入设备成功 - **WHEN** 平台上传包含 50 条设备数据的 CSV 文件 - **THEN** 系统创建 50 条设备记录,`shop_id` 为 NULL(平台库存),状态为 1(在库),返回导入成功消息 #### Scenario: 批量导入包含重复编号 - **WHEN** 平台上传的 CSV 文件中包含已存在的设备编号 - **THEN** 系统跳过重复编号的设备,记录到 skipped_items 并列出重复编号,其他有效设备正常导入 #### Scenario: 导入时绑定平台库存的卡 - **WHEN** CSV 行指定了 ICCID,且该卡为平台库存(shop_id = NULL)且未绑定其他设备 - **THEN** 系统创建设备并绑定该卡,记录为完全成功 #### Scenario: 导入时尝试绑定已分配给店铺的卡 - **WHEN** CSV 行指定了 ICCID,但该卡已分配给店铺(shop_id != NULL) - **THEN** 系统创建设备但不绑定该卡,将该设备记录到 warning_items,原因为"ICCID-XXX 已分配给店铺,不能绑定到平台库存设备" #### Scenario: 导入时部分卡绑定成功 - **WHEN** CSV 行指定了 4 张卡,其中 2 张为平台库存且未绑定,1 张已分配给店铺,1 张不存在 - **THEN** 系统创建设备并绑定 2 张有效的卡,将该设备记录到 warning_items,原因为"部分卡绑定失败: ICCID-001 已分配给店铺,不能绑定到平台库存设备; ICCID-002 不存在",success_count 和 warning_count 各加 1 #### Scenario: 导入时所有指定的卡都不可用 - **WHEN** CSV 行指定了 2 张卡,但都已绑定到其他设备 - **THEN** 系统不创建设备,将该行记录到 failed_items,原因为"所有指定的卡都不可用: ICCID-001 已绑定其他设备, ICCID-002 已绑定其他设备" ### Requirement: 设备查询和筛选 系统 SHALL 支持多维度查询和筛选设备,包括状态、店铺归属、批次号、设备类型等。 **查询条件**: - 设备编号(精确匹配或模糊匹配) - 设备名称(模糊匹配) - 设备状态(单选或多选) - 店铺 ID(shop_id): 可选,NULL 表示平台库存 - 批次号(精确匹配) - 设备类型(单选或多选) - 设备制造商(模糊匹配) - 激活时间范围(开始时间 - 结束时间) - 创建时间范围(开始时间 - 结束时间) **分页**: - 默认每页 20 条,最大每页 100 条 - 返回总记录数和总页数 **数据权限**: - 基于 shop_id 自动应用数据权限过滤 - 代理只能看到自己店铺及下级店铺的设备 #### Scenario: 查询平台库存设备 - **WHEN** 运营人员查询平台库存设备 - **THEN** 系统返回 `shop_id` 为 NULL 的设备列表 #### Scenario: 代理查询自己店铺的设备 - **WHEN** 代理店铺(ID 为 10)查询自己的设备 - **THEN** 系统返回 `shop_id` 为 10(及其下级店铺)的设备列表 --- ### Requirement: 设备数据校验 系统 SHALL 对设备数据进行校验,确保数据完整性和一致性。 **校验规则**: - 设备编号(device_no):必填,长度 1-100 字符,唯一 - 设备名称(device_name):可选,长度 1-255 字符 - 设备型号(device_model):可选,长度 1-100 字符 - 设备类型(device_type):可选,长度 1-50 字符 - 最大插槽数(max_sim_slots):必填,1-4 之间的整数 - 店铺 ID(shop_id):可选,NULL 表示平台库存,有值必须是有效的店铺 ID - 设备状态(status):必填,枚举值 1(在库) | 2(已分销) | 3(已激活) | 4(已停用) #### Scenario: 创建设备时插槽数超出范围 - **WHEN** 用户创建设备,最大插槽数为 5 - **THEN** 系统拒绝创建,返回错误信息"最大插槽数必须在 1-4 之间" #### Scenario: 创建设备时设备编号重复 - **WHEN** 用户创建设备,设备编号为已存在的 "DEV-001" - **THEN** 系统拒绝创建,返回错误信息"设备编号已存在" ### Requirement: DeviceSimBinding 模型组织 系统 SHALL 将 DeviceSimBinding 模型定义在独立的文件中,遵循项目代码组织规范。 **文件位置**: - 从: `internal/model/package.go` - 到: `internal/model/device_sim_binding.go` **模型内容**: ```go // DeviceSimBinding 设备-IoT卡绑定关系模型 // 管理设备与 IoT 卡的多对多绑定关系(1 设备绑定 1-4 张 IoT 卡) type DeviceSimBinding struct { gorm.Model BaseModel `gorm:"embedded"` DeviceID uint `gorm:"column:device_id;index:idx_device_slot;not null;comment:设备ID"` IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID"` SlotPosition int `gorm:"column:slot_position;type:int;index:idx_device_slot;comment:插槽位置(1, 2, 3, 4)"` BindStatus int `gorm:"column:bind_status;type:int;default:1;comment:绑定状态 1-已绑定 2-已解绑"` BindTime *time.Time `gorm:"column:bind_time;comment:绑定时间"` UnbindTime *time.Time `gorm:"column:unbind_time;comment:解绑时间"` } func (DeviceSimBinding) TableName() string { return "tb_device_sim_binding" } ``` #### Scenario: 模型文件独立 - **WHEN** 开发者需要查找或修改 DeviceSimBinding 模型 - **THEN** 模型定义位于 `internal/model/device_sim_binding.go` 文件中,而非混杂在 `package.go` 中 --- ### Requirement: Device Handler 分层修复 Device Handler SHALL 不再直接持有 `gateway.Client` 引用。所有 Gateway API 调用 SHALL 通过 Device Service 层发起。 #### Scenario: DeviceHandler 不持有 gatewayClient - **WHEN** 创建 `DeviceHandler` 实例 - **THEN** `NewDeviceHandler` 构造函数不接收 `gateway.Client` 参数 - **AND** Handler 结构体不包含 `gatewayClient` 字段 #### Scenario: 查询设备网关信息通过 Service 调用 - **WHEN** Handler 的 `GetGatewayInfo` 方法被调用 - **THEN** Handler 调用 `service.GetGatewayInfo(ctx, identifier)` #### Scenario: 查询设备卡槽信息通过 Service 调用 - **WHEN** Handler 的 `GetGatewaySlots` 方法被调用 - **THEN** Handler 调用 `service.GetGatewaySlots(ctx, identifier)` #### Scenario: 设置设备限速通过 Service 调用 - **WHEN** Handler 的 `SetSpeedLimit` 方法被调用 - **THEN** Handler 调用 `service.SetGatewaySpeedLimit(ctx, identifier, speedLimit)` #### Scenario: 设置设备 WiFi 通过 Service 调用 - **WHEN** Handler 的 `SetWiFi` 方法被调用 - **THEN** Handler 调用 `service.SetGatewayWiFi(ctx, identifier, req)` #### Scenario: 切换设备卡通过 Service 调用 - **WHEN** Handler 的 `SwitchCard` 方法被调用 - **THEN** Handler 调用 `service.GatewaySwitchCard(ctx, identifier, targetICCID)` #### Scenario: 重启设备通过 Service 调用 - **WHEN** Handler 的 `RebootDevice` 方法被调用 - **THEN** Handler 调用 `service.GatewayRebootDevice(ctx, identifier)` #### Scenario: 恢复出厂设置通过 Service 调用 - **WHEN** Handler 的 `ResetDevice` 方法被调用 - **THEN** Handler 调用 `service.GatewayResetDevice(ctx, identifier)` ### Requirement: Device Service Gateway 代理方法 Device Service SHALL 提供 Gateway API 的代理方法,封装设备标识符解析、IMEI 检查和 Gateway 调用。 #### Scenario: GetGatewayInfo 方法 - **WHEN** 调用 `service.GetGatewayInfo(ctx, identifier)` - **THEN** 先通过 `GetDeviceByIdentifier` 查找设备并验证权限 - **AND** 检查设备 IMEI 不为空 - **AND** 调用 `gatewayClient.GetDeviceInfo` 传入设备 IMEI - **AND** 返回 `*gateway.DeviceInfoResp` #### Scenario: GetGatewaySlots 方法 - **WHEN** 调用 `service.GetGatewaySlots(ctx, identifier)` - **THEN** 先查找设备、验证 IMEI 不为空 - **AND** 调用 `gatewayClient.GetSlotInfo` - **AND** 返回 `*gateway.SlotInfoResp` #### Scenario: SetGatewaySpeedLimit 方法 - **WHEN** 调用 `service.SetGatewaySpeedLimit(ctx, identifier, speedLimit)` - **THEN** 先查找设备、验证 IMEI - **AND** 调用 `gatewayClient.SetSpeedLimit` 传入设备 IMEI 和限速值 #### Scenario: SetGatewayWiFi 方法 - **WHEN** 调用 `service.SetGatewayWiFi(ctx, identifier, cardNo, ssid, password string, enabled bool)` - **THEN** 先查找设备、验证 IMEI - **AND** 调用 `gatewayClient.SetWiFi` 传入设备 IMEI、cardNo(ICCID)、ssid、password、enabled #### Scenario: GatewaySwitchCard 方法 - **WHEN** 调用 `service.GatewaySwitchCard(ctx, identifier, targetICCID)` - **THEN** 先查找设备、验证 IMEI - **AND** 调用 `gatewayClient.SwitchCard` 传入设备 IMEI 作为 cardNo 和目标 ICCID #### Scenario: GatewayRebootDevice 方法 - **WHEN** 调用 `service.GatewayRebootDevice(ctx, identifier)` - **THEN** 先查找设备、验证 IMEI - **AND** 调用 `gatewayClient.RebootDevice` 传入设备 IMEI #### Scenario: GatewayResetDevice 方法 - **WHEN** 调用 `service.GatewayResetDevice(ctx, identifier)` - **THEN** 先查找设备、验证 IMEI - **AND** 调用 `gatewayClient.ResetDevice` 传入设备 IMEI #### Scenario: 设备 IMEI 为空 - **WHEN** 调用任意 Gateway 代理方法且设备的 IMEI 字段为空 - **THEN** 返回 `CodeInvalidParam` 错误 - **AND** 错误信息说明该设备未配置 IMEI #### Scenario: 设备不存在或无权限 - **WHEN** 调用任意 Gateway 代理方法且标识符无法匹配到设备 - **THEN** 返回对应的错误(由 `GetDeviceByIdentifier` 返回) ### Requirement: Device Service 接收 Gateway Client Device Service SHALL 在构造函数中接收 `*gateway.Client` 依赖。 #### Scenario: Device Service 初始化 - **WHEN** 创建 Device Service 实例 - **THEN** 构造函数接收 `gatewayClient *gateway.Client` 参数 - **AND** 存储为 Service 的内部字段 - **AND** `gatewayClient` 可以为 nil(Gateway 配置缺失时) #### Scenario: Gateway Client 为 nil 时调用 Gateway 方法 - **WHEN** `gatewayClient` 为 nil 且调用任意 Gateway 代理方法 - **THEN** 返回 `CodeGatewayError` 错误 - **AND** 错误信息为 "Gateway 客户端未配置"