Files
junhong_cmp_fiber/openspec/specs/iot-device/spec.md
huang 477a9fc98d
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat: 添加设备IMEI和单卡ICCID查询接口
- 新增 GET /api/admin/devices/by-imei/:imei 接口,支持通过设备号查询设备详情
- 新增 GET /api/admin/iot-cards/by-iccid/:iccid 接口,支持通过ICCID查询单卡详情
- 添加对应的 Service 层方法和 Handler
- 更新 OpenAPI 文档
- 添加集成测试并修复测试环境配置(使用环境变量)
- 归档已完成的 OpenSpec 变更记录

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-27 09:59:54 +08:00

392 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`