# IoT Device - Delta Spec ## MODIFIED Requirements ### 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 支持批量导入设备数据,用于平台库存管理。 **导入字段**: - 设备编号(必填) - 设备名称(可选) - 设备型号(可选) - 设备类型(可选) - 最大插槽数(可选,默认 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 已绑定其他设备" ## ADDED Requirements ### 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` 中