Files
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

7.1 KiB

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

模型内容:

// 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