feat: 添加设备IMEI和单卡ICCID查询接口
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增 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>
This commit is contained in:
@@ -102,8 +102,9 @@ This capability supports:
|
||||
- 绑定时记录插槽位置(slot_position: 1, 2, 3, 4)
|
||||
- 绑定时记录绑定时间和绑定状态(1-已绑定 2-已解绑)
|
||||
- 绑定/解绑操作不改变 IoT 卡的 shop_id(所有权由分销操作管理,而非绑定操作)
|
||||
- **新增**: 同一设备的同一插槽同一时间只能绑定一张卡(数据库唯一约束)
|
||||
|
||||
**中间表 device_sim_bindings**:
|
||||
**中间表 tb_device_sim_binding**:
|
||||
- `id`: 绑定记录 ID(主键,BIGINT)
|
||||
- `device_id`: 设备 ID(BIGINT)
|
||||
- `iot_card_id`: IoT 卡 ID(BIGINT)
|
||||
@@ -113,6 +114,17 @@ This capability supports:
|
||||
- `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 卡到设备
|
||||
|
||||
@@ -124,6 +136,16 @@ This capability supports:
|
||||
- **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: 设备套餐购买和流量共享
|
||||
@@ -217,29 +239,64 @@ This capability supports:
|
||||
|
||||
**导入字段**:
|
||||
- 设备编号(必填)
|
||||
- 设备名称(必填)
|
||||
- 设备型号(必填)
|
||||
- 设备类型(必填)
|
||||
- 设备名称(可选)
|
||||
- 设备型号(可选)
|
||||
- 设备类型(可选)
|
||||
- 最大插槽数(可选,默认 4)
|
||||
- 设备制造商(可选)
|
||||
- 批次号(必填)
|
||||
- 批次号(可选,由任务自动生成)
|
||||
- **ICCID 1-4**(可选,用于绑定 IoT 卡)
|
||||
|
||||
**导入规则**:
|
||||
- 设备编号必须唯一,重复编号将被拒绝
|
||||
- 导入的设备默认 `owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活)
|
||||
- 设备编号必须唯一,重复编号将被跳过
|
||||
- 导入的设备默认 `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 条设备记录,`owner_type` 为 "platform",`owner_id` 为 0,状态为 1(未激活),返回导入成功消息
|
||||
- **THEN** 系统创建 50 条设备记录,`shop_id` 为 NULL(平台库存),状态为 1(在库),返回导入成功消息
|
||||
|
||||
#### Scenario: 批量导入包含重复编号
|
||||
|
||||
- **WHEN** 平台上传的 CSV 文件中包含已存在的设备编号
|
||||
- **THEN** 系统拒绝重复编号的设备,返回错误信息并列出重复编号,其他有效设备正常导入
|
||||
- **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: 设备查询和筛选
|
||||
|
||||
@@ -299,3 +356,36 @@ This capability supports:
|
||||
- **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` 中
|
||||
|
||||
|
||||
Reference in New Issue
Block a user