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:
@@ -3,9 +3,7 @@
|
||||
## Purpose
|
||||
|
||||
管理资产(IoT 卡、设备)在平台与代理商之间的流转记录,支持分配和回收操作的完整追溯。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 资产分配记录查询
|
||||
|
||||
系统 SHALL 提供资产分配记录的查询功能,支持查看卡和设备在平台与代理商之间的流转历史。
|
||||
@@ -16,7 +14,7 @@
|
||||
|
||||
**资产类型**:
|
||||
- `iot_card`: 物联网卡(单卡)
|
||||
- `device`: 设备(未来扩展)
|
||||
- `device`: 设备
|
||||
|
||||
**查询条件**:
|
||||
- `allocation_type`(可选): 分配类型,枚举值 "allocate" | "recall"
|
||||
@@ -48,6 +46,8 @@
|
||||
- `asset_type_name`: 资产类型名称(物联网卡/设备)
|
||||
- `asset_id`: 资产 ID
|
||||
- `asset_identifier`: 资产标识符
|
||||
- `related_device_id`: 关联设备 ID(单卡分配时,如果卡绑定了设备)
|
||||
- `related_card_ids`: 关联卡 ID 列表(设备分配时,包含设备绑定的所有卡 ID)
|
||||
- `from_owner_type`: 来源所有者类型
|
||||
- `from_owner_id`: 来源所有者 ID
|
||||
- `from_owner_name`: 来源所有者名称
|
||||
@@ -69,6 +69,11 @@
|
||||
- **WHEN** 管理员查询资产类型为 "iot_card" 的记录
|
||||
- **THEN** 系统只返回物联网卡的分配/回收记录,不包含设备记录
|
||||
|
||||
#### Scenario: 按资产类型筛选设备记录
|
||||
|
||||
- **WHEN** 管理员查询资产类型为 "device" 的记录
|
||||
- **THEN** 系统只返回设备的分配/回收记录,不包含单卡记录
|
||||
|
||||
#### Scenario: 按分配类型筛选记录
|
||||
|
||||
- **WHEN** 管理员查询分配类型为 "allocate" 的记录
|
||||
@@ -79,6 +84,11 @@
|
||||
- **WHEN** 管理员输入 asset_identifier = "8986001"
|
||||
- **THEN** 系统返回 ICCID 包含 "8986001" 的所有分配记录
|
||||
|
||||
#### Scenario: 按设备号模糊查询
|
||||
|
||||
- **WHEN** 管理员输入 asset_identifier = "GPS"
|
||||
- **THEN** 系统返回设备号包含 "GPS" 的所有分配记录
|
||||
|
||||
#### Scenario: 代理查询自己相关的记录
|
||||
|
||||
- **WHEN** 代理用户(店铺 ID=10)查询分配记录
|
||||
@@ -94,14 +104,20 @@
|
||||
|
||||
**响应**:
|
||||
- 包含记录的所有字段
|
||||
- 关联卡 ID 列表(如果是设备分配,包含设备下的所有卡 ID)
|
||||
- `related_card_ids`: 关联卡 ID 列表(设备分配时,包含设备绑定的所有卡 ID)
|
||||
|
||||
#### Scenario: 查询分配记录详情
|
||||
|
||||
- **WHEN** 管理员查询分配记录详情(ID=1)
|
||||
- **THEN** 系统返回该记录的完整信息,包括来源/目标所有者名称、操作人名称等
|
||||
|
||||
#### Scenario: 查询设备分配记录详情
|
||||
|
||||
- **WHEN** 管理员查询设备分配记录详情
|
||||
- **THEN** 系统返回该记录的完整信息,包括 related_card_ids(设备绑定的所有卡 ID)
|
||||
|
||||
#### Scenario: 查询不存在的记录
|
||||
|
||||
- **WHEN** 管理员查询不存在的分配记录(ID=999)
|
||||
- **THEN** 系统返回 404 错误,提示"分配记录不存在"
|
||||
|
||||
|
||||
191
openspec/specs/device-import/spec.md
Normal file
191
openspec/specs/device-import/spec.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# device-import Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-device-management. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 设备批量导入
|
||||
|
||||
系统 SHALL 提供设备批量导入功能,通过 CSV 文件导入设备并自动绑定卡,仅平台用户可操作。
|
||||
|
||||
**API 端点**: `POST /api/admin/devices/import`
|
||||
|
||||
**请求参数**:
|
||||
- `batch_no`: 批次号(必填)
|
||||
- `file_key`: 对象存储文件路径(必填,通过 /storage/upload-url 获取)
|
||||
|
||||
**CSV 格式**:
|
||||
```
|
||||
device_no,device_name,device_model,device_type,max_sim_slots,manufacturer,iccid_1,iccid_2,iccid_3,iccid_4
|
||||
DEV-001,GPS追踪器A,GT06N,GPS Tracker,4,Concox,8986001234567890001,8986001234567890002,,
|
||||
DEV-002,GPS追踪器B,GT06N,GPS Tracker,4,Concox,8986001234567890003,,,
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
- `device_no`: 设备号(必填,唯一)
|
||||
- `device_name`: 设备名称(可选)
|
||||
- `device_model`: 设备型号(可选)
|
||||
- `device_type`: 设备类型(可选)
|
||||
- `max_sim_slots`: 最大插槽数(可选,默认 4,范围 1-4)
|
||||
- `manufacturer`: 制造商(可选)
|
||||
- `iccid_1` ~ `iccid_4`: 对应插槽 1-4 的 ICCID(可选,空值表示该插槽无卡)
|
||||
|
||||
**导入规则**:
|
||||
- 导入的设备 shop_id = NULL(平台库存)
|
||||
- 导入的设备 status = 1(在库)
|
||||
- 设备号重复则该行跳过
|
||||
- ICCID 必须已存在于系统中(先导入卡,再导入设备)
|
||||
- ICCID 不存在则该行失败
|
||||
- ICCID 已绑定其他设备则该行失败
|
||||
- 导入通过异步任务处理,立即返回任务 ID
|
||||
|
||||
**权限**: 仅平台用户
|
||||
|
||||
**响应**:
|
||||
- `task_id`: 导入任务 ID
|
||||
- `task_no`: 任务编号
|
||||
- `message`: 提示信息
|
||||
|
||||
#### Scenario: 提交设备导入任务
|
||||
|
||||
- **WHEN** 平台管理员上传 CSV 文件并提交导入请求
|
||||
- **THEN** 系统创建导入任务,返回任务 ID,开始异步处理
|
||||
|
||||
#### Scenario: 代理尝试导入设备
|
||||
|
||||
- **WHEN** 代理用户尝试导入设备
|
||||
- **THEN** 系统返回 403 错误,提示"无权限执行此操作"
|
||||
|
||||
#### Scenario: 文件格式错误
|
||||
|
||||
- **WHEN** 平台管理员上传非 CSV 格式或格式不正确的文件
|
||||
- **THEN** 系统创建任务但处理失败,任务状态为"失败",记录错误信息
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备导入任务执行
|
||||
|
||||
系统 SHALL 异步执行设备导入任务,逐行处理 CSV 数据。
|
||||
|
||||
**处理规则**:
|
||||
- 逐行解析 CSV 文件
|
||||
- 对每行数据执行以下校验:
|
||||
1. 设备号是否已存在(已存在则跳过)
|
||||
2. ICCID 是否存在于系统中(不存在则失败)
|
||||
3. ICCID 是否已绑定其他设备(已绑定则失败)
|
||||
- 校验通过后:
|
||||
1. 创建设备记录
|
||||
2. 创建设备-卡绑定记录
|
||||
- 记录处理结果(成功/跳过/失败)
|
||||
|
||||
**任务状态**:
|
||||
- 1: 待处理
|
||||
- 2: 处理中
|
||||
- 3: 已完成
|
||||
- 4: 失败
|
||||
|
||||
#### Scenario: 导入成功
|
||||
|
||||
- **WHEN** CSV 中所有设备号不重复且 ICCID 有效
|
||||
- **THEN** 系统创建所有设备和绑定记录,任务状态为"已完成"
|
||||
|
||||
#### Scenario: 部分导入成功
|
||||
|
||||
- **WHEN** CSV 中部分设备号已存在或部分 ICCID 无效
|
||||
- **THEN** 系统只导入有效的行,记录跳过和失败的详情,任务状态为"已完成"
|
||||
|
||||
#### Scenario: ICCID 不存在
|
||||
|
||||
- **WHEN** CSV 中某行的 ICCID 在系统中不存在
|
||||
- **THEN** 该行导入失败,记录失败原因"ICCID 不存在"
|
||||
|
||||
#### Scenario: ICCID 已绑定其他设备
|
||||
|
||||
- **WHEN** CSV 中某行的 ICCID 已绑定到其他设备
|
||||
- **THEN** 该行导入失败,记录失败原因"ICCID 已绑定其他设备"
|
||||
|
||||
#### Scenario: 设备号重复
|
||||
|
||||
- **WHEN** CSV 中某行的设备号在系统中已存在
|
||||
- **THEN** 该行被跳过,记录跳过原因"设备号已存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备导入任务列表查询
|
||||
|
||||
系统 SHALL 提供设备导入任务列表查询功能,仅平台用户可操作。
|
||||
|
||||
**API 端点**: `GET /api/admin/devices/import/tasks`
|
||||
|
||||
**查询条件**:
|
||||
- `status`(可选): 任务状态 1-4
|
||||
- `batch_no`(可选): 批次号,模糊匹配
|
||||
- `start_time`(可选): 创建时间起始
|
||||
- `end_time`(可选): 创建时间结束
|
||||
|
||||
**分页**:
|
||||
- 默认每页 20 条,最大每页 100 条
|
||||
|
||||
**响应字段**:
|
||||
- `id`: 任务 ID
|
||||
- `task_no`: 任务编号
|
||||
- `status`: 任务状态
|
||||
- `status_text`: 任务状态文本
|
||||
- `batch_no`: 批次号
|
||||
- `file_name`: 文件名
|
||||
- `total_count`: 总数
|
||||
- `success_count`: 成功数
|
||||
- `skip_count`: 跳过数
|
||||
- `fail_count`: 失败数
|
||||
- `started_at`: 开始时间
|
||||
- `completed_at`: 完成时间
|
||||
- `error_message`: 错误信息
|
||||
- `created_at`: 创建时间
|
||||
|
||||
**权限**: 仅平台用户
|
||||
|
||||
#### Scenario: 查询导入任务列表
|
||||
|
||||
- **WHEN** 平台管理员查询导入任务列表
|
||||
- **THEN** 系统返回所有导入任务,按创建时间倒序排列
|
||||
|
||||
#### Scenario: 按状态筛选任务
|
||||
|
||||
- **WHEN** 平台管理员查询状态为 3(已完成)的任务
|
||||
- **THEN** 系统只返回已完成的任务
|
||||
|
||||
#### Scenario: 代理尝试查询导入任务
|
||||
|
||||
- **WHEN** 代理用户尝试查询导入任务
|
||||
- **THEN** 系统返回 403 错误,提示"无权限执行此操作"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备导入任务详情查询
|
||||
|
||||
系统 SHALL 提供设备导入任务详情查询功能,包含跳过和失败记录的详细信息。
|
||||
|
||||
**API 端点**: `GET /api/admin/devices/import/tasks/:id`
|
||||
|
||||
**响应字段**:
|
||||
- 包含任务列表的所有字段
|
||||
- `skipped_items`: 跳过记录详情列表
|
||||
- `line`: 行号
|
||||
- `device_no`: 设备号
|
||||
- `reason`: 跳过原因
|
||||
- `failed_items`: 失败记录详情列表
|
||||
- `line`: 行号
|
||||
- `device_no`: 设备号
|
||||
- `reason`: 失败原因
|
||||
|
||||
**权限**: 仅平台用户
|
||||
|
||||
#### Scenario: 查询导入任务详情
|
||||
|
||||
- **WHEN** 平台管理员查询导入任务详情(ID=1)
|
||||
- **THEN** 系统返回任务的完整信息,包括跳过和失败记录详情
|
||||
|
||||
#### Scenario: 查询不存在的任务
|
||||
|
||||
- **WHEN** 平台管理员查询不存在的任务(ID=999)
|
||||
- **THEN** 系统返回 404 错误,提示"导入任务不存在"
|
||||
|
||||
325
openspec/specs/device/spec.md
Normal file
325
openspec/specs/device/spec.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# device Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-device-management. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 设备列表查询
|
||||
|
||||
系统 SHALL 提供设备列表查询功能,支持多维度筛选和分页。
|
||||
|
||||
**查询条件**:
|
||||
- `device_no`(可选): 设备号,支持模糊匹配
|
||||
- `device_name`(可选): 设备名称,支持模糊匹配
|
||||
- `status`(可选): 设备状态,枚举值 1-在库 | 2-已分销 | 3-已激活 | 4-已停用
|
||||
- `shop_id`(可选): 店铺 ID,NULL 表示平台库存
|
||||
- `batch_no`(可选): 批次号,精确匹配
|
||||
- `device_type`(可选): 设备类型
|
||||
- `manufacturer`(可选): 制造商,支持模糊匹配
|
||||
- `created_at_start`(可选): 创建时间起始
|
||||
- `created_at_end`(可选): 创建时间结束
|
||||
|
||||
**分页**:
|
||||
- 默认每页 20 条,最大每页 100 条
|
||||
- 返回总记录数和总页数
|
||||
|
||||
**数据权限**:
|
||||
- 平台用户可查看所有设备
|
||||
- 代理用户只能查看自己店铺及下级店铺的设备
|
||||
|
||||
**API 端点**: `GET /api/admin/devices`
|
||||
|
||||
**响应字段**:
|
||||
- `id`: 设备 ID
|
||||
- `device_no`: 设备号
|
||||
- `device_name`: 设备名称
|
||||
- `device_model`: 设备型号
|
||||
- `device_type`: 设备类型
|
||||
- `max_sim_slots`: 最大插槽数
|
||||
- `manufacturer`: 制造商
|
||||
- `batch_no`: 批次号
|
||||
- `shop_id`: 店铺 ID
|
||||
- `shop_name`: 店铺名称
|
||||
- `status`: 状态
|
||||
- `status_name`: 状态名称
|
||||
- `bound_card_count`: 已绑定卡数量
|
||||
- `activated_at`: 激活时间
|
||||
- `created_at`: 创建时间
|
||||
- `updated_at`: 更新时间
|
||||
|
||||
#### Scenario: 平台查询所有设备
|
||||
|
||||
- **WHEN** 平台管理员查询设备列表,不带任何筛选条件
|
||||
- **THEN** 系统返回所有设备,按创建时间倒序排列
|
||||
|
||||
#### Scenario: 按设备号模糊查询
|
||||
|
||||
- **WHEN** 管理员输入 device_no = "GPS"
|
||||
- **THEN** 系统返回设备号包含 "GPS" 的所有设备
|
||||
|
||||
#### Scenario: 按状态筛选设备
|
||||
|
||||
- **WHEN** 管理员查询状态为 1(在库)的设备
|
||||
- **THEN** 系统只返回在库状态的设备
|
||||
|
||||
#### Scenario: 代理查询自己店铺的设备
|
||||
|
||||
- **WHEN** 代理用户(店铺 ID=10)查询设备列表
|
||||
- **THEN** 系统只返回 shop_id 为 10 及其下级店铺的设备
|
||||
|
||||
#### Scenario: 查询平台库存设备
|
||||
|
||||
- **WHEN** 平台管理员查询 shop_id 为空的设备
|
||||
- **THEN** 系统返回所有平台库存设备(shop_id = NULL)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 设备详情查询
|
||||
|
||||
系统 SHALL 提供设备详情查询功能,返回设备的基本信息。
|
||||
|
||||
**API 端点**: `GET /api/admin/devices/:id`
|
||||
|
||||
**响应字段**:
|
||||
- 包含设备的所有基本字段
|
||||
- `shop_name`: 店铺名称(如果有)
|
||||
|
||||
**数据权限**:
|
||||
- 平台用户可查看所有设备
|
||||
- 代理用户只能查看自己店铺及下级店铺的设备
|
||||
|
||||
#### Scenario: 查询设备详情成功
|
||||
|
||||
- **WHEN** 管理员查询设备详情(ID=1)
|
||||
- **THEN** 系统返回该设备的完整基本信息
|
||||
|
||||
#### Scenario: 查询不存在的设备
|
||||
|
||||
- **WHEN** 管理员查询不存在的设备(ID=999)
|
||||
- **THEN** 系统返回 404 错误,提示"设备不存在"
|
||||
|
||||
#### Scenario: 代理查询无权限的设备
|
||||
|
||||
- **WHEN** 代理用户(店铺 ID=10)查询其他店铺的设备(shop_id=20,非下级)
|
||||
- **THEN** 系统返回 404 错误,提示"设备不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 删除设备
|
||||
|
||||
系统 SHALL 提供删除设备功能,仅平台用户可操作,执行软删除。
|
||||
|
||||
**API 端点**: `DELETE /api/admin/devices/:id`
|
||||
|
||||
**业务规则**:
|
||||
- 仅平台用户可删除设备
|
||||
- 删除设备时自动解绑该设备上的所有卡
|
||||
- 执行软删除(设置 deleted_at)
|
||||
|
||||
**权限**: 仅平台用户
|
||||
|
||||
#### Scenario: 平台删除设备成功
|
||||
|
||||
- **WHEN** 平台管理员删除设备(ID=1)
|
||||
- **THEN** 系统软删除该设备,并解绑设备上的所有卡
|
||||
|
||||
#### Scenario: 代理尝试删除设备
|
||||
|
||||
- **WHEN** 代理用户尝试删除设备
|
||||
- **THEN** 系统返回 403 错误,提示"无权限执行此操作"
|
||||
|
||||
#### Scenario: 删除不存在的设备
|
||||
|
||||
- **WHEN** 平台管理员删除不存在的设备(ID=999)
|
||||
- **THEN** 系统返回 404 错误,提示"设备不存在"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 获取设备绑定的卡列表
|
||||
|
||||
系统 SHALL 提供查询设备绑定的 IoT 卡列表功能。
|
||||
|
||||
**API 端点**: `GET /api/admin/devices/:id/cards`
|
||||
|
||||
**响应字段**:
|
||||
- `bindings`: 绑定列表,每个元素包含:
|
||||
- `id`: 绑定记录 ID
|
||||
- `slot_position`: 插槽位置(1-4)
|
||||
- `iot_card_id`: IoT 卡 ID
|
||||
- `iccid`: ICCID
|
||||
- `msisdn`: 接入号
|
||||
- `carrier_name`: 运营商名称
|
||||
- `status`: 卡状态
|
||||
- `bind_time`: 绑定时间
|
||||
|
||||
#### Scenario: 查询设备绑定的卡
|
||||
|
||||
- **WHEN** 管理员查询设备(ID=1)绑定的卡
|
||||
- **THEN** 系统返回该设备所有已绑定的卡信息,按插槽位置排序
|
||||
|
||||
#### Scenario: 查询无绑定卡的设备
|
||||
|
||||
- **WHEN** 管理员查询没有绑定卡的设备
|
||||
- **THEN** 系统返回空的绑定列表
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 绑定卡到设备
|
||||
|
||||
系统 SHALL 提供将 IoT 卡绑定到设备指定插槽的功能,仅平台用户可操作。
|
||||
|
||||
**API 端点**: `POST /api/admin/devices/:id/cards`
|
||||
|
||||
**请求参数**:
|
||||
- `iot_card_id`: IoT 卡 ID(必填)
|
||||
- `slot_position`: 插槽位置 1-4(必填)
|
||||
|
||||
**业务规则**:
|
||||
- 仅平台用户可操作
|
||||
- 插槽位置不能超过设备的 max_sim_slots
|
||||
- 该插槽必须为空(无已绑定的卡)
|
||||
- 该卡不能已绑定到其他设备
|
||||
- 绑定操作不改变卡的 shop_id
|
||||
|
||||
**权限**: 仅平台用户
|
||||
|
||||
#### Scenario: 绑定卡到设备成功
|
||||
|
||||
- **WHEN** 平台管理员将 IoT 卡(ID=101)绑定到设备(ID=1)的插槽 2
|
||||
- **THEN** 系统创建绑定记录,返回绑定成功信息
|
||||
|
||||
#### Scenario: 绑定到已占用的插槽
|
||||
|
||||
- **WHEN** 平台管理员尝试绑定卡到已有卡的插槽
|
||||
- **THEN** 系统返回错误,提示"该插槽已有绑定的卡"
|
||||
|
||||
#### Scenario: 绑定已被绑定的卡
|
||||
|
||||
- **WHEN** 平台管理员尝试绑定已绑定到其他设备的卡
|
||||
- **THEN** 系统返回错误,提示"该卡已绑定到其他设备"
|
||||
|
||||
#### Scenario: 插槽位置超出范围
|
||||
|
||||
- **WHEN** 平台管理员尝试绑定卡到插槽 5(设备 max_sim_slots=4)
|
||||
- **THEN** 系统返回错误,提示"插槽位置超出设备最大插槽数"
|
||||
|
||||
#### Scenario: 代理尝试绑定卡
|
||||
|
||||
- **WHEN** 代理用户尝试绑定卡到设备
|
||||
- **THEN** 系统返回 403 错误,提示"无权限执行此操作"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 解绑设备上的卡
|
||||
|
||||
系统 SHALL 提供解绑设备上指定卡的功能,仅平台用户可操作。
|
||||
|
||||
**API 端点**: `DELETE /api/admin/devices/:id/cards/:cardId`
|
||||
|
||||
**业务规则**:
|
||||
- 仅平台用户可操作
|
||||
- 更新绑定记录的 bind_status 为 2(已解绑),记录 unbind_time
|
||||
- 解绑操作不改变卡的 shop_id
|
||||
|
||||
**权限**: 仅平台用户
|
||||
|
||||
#### Scenario: 解绑卡成功
|
||||
|
||||
- **WHEN** 平台管理员解绑设备(ID=1)上的卡(ID=101)
|
||||
- **THEN** 系统更新绑定记录状态为已解绑,返回成功信息
|
||||
|
||||
#### Scenario: 解绑不存在的绑定关系
|
||||
|
||||
- **WHEN** 平台管理员尝试解绑不存在的绑定关系
|
||||
- **THEN** 系统返回错误,提示"该卡未绑定到此设备"
|
||||
|
||||
#### Scenario: 代理尝试解绑卡
|
||||
|
||||
- **WHEN** 代理用户尝试解绑设备上的卡
|
||||
- **THEN** 系统返回 403 错误,提示"无权限执行此操作"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量分配设备
|
||||
|
||||
系统 SHALL 提供批量分配设备给下级店铺的功能,分配时自动同步绑定卡的归属。
|
||||
|
||||
**API 端点**: `POST /api/admin/devices/allocate`
|
||||
|
||||
**请求参数**:
|
||||
- `target_shop_id`: 目标店铺 ID(必填)
|
||||
- `device_ids`: 设备 ID 列表(必填,最多 100 个)
|
||||
- `remark`: 备注(可选)
|
||||
|
||||
**业务规则**:
|
||||
- 只能分配给直属下级店铺,不可跨级
|
||||
- 平台只能分配 shop_id=NULL 的设备
|
||||
- 代理只能分配自己店铺的设备
|
||||
- 分配后:
|
||||
- 设备的 shop_id 变更为目标店铺 ID
|
||||
- 设备绑定的所有卡的 shop_id 也变更为目标店铺 ID
|
||||
- 设备状态变为「已分销」(2)
|
||||
- 创建资产分配记录(asset_type='device')
|
||||
|
||||
**响应**:
|
||||
- `success_count`: 成功数量
|
||||
- `fail_count`: 失败数量
|
||||
- `failed_items`: 失败详情列表
|
||||
|
||||
#### Scenario: 平台分配设备给一级代理
|
||||
|
||||
- **WHEN** 平台管理员将 5 台设备分配给一级代理店铺(ID=10)
|
||||
- **THEN** 系统更新这 5 台设备及其绑定卡的 shop_id 为 10,创建分配记录,返回成功数量
|
||||
|
||||
#### Scenario: 代理分配设备给下级
|
||||
|
||||
- **WHEN** 代理(店铺 ID=10)将 3 台设备分配给直属下级店铺(ID=101)
|
||||
- **THEN** 系统更新这 3 台设备及其绑定卡的 shop_id 为 101,创建分配记录
|
||||
|
||||
#### Scenario: 分配给非直属下级
|
||||
|
||||
- **WHEN** 代理(店铺 ID=10)尝试分配设备给非直属下级店铺(ID=1011,是 101 的下级)
|
||||
- **THEN** 系统返回错误,提示"只能分配给直属下级店铺"
|
||||
|
||||
#### Scenario: 分配不属于自己的设备
|
||||
|
||||
- **WHEN** 代理(店铺 ID=10)尝试分配其他店铺的设备
|
||||
- **THEN** 系统跳过这些设备,只分配属于自己的设备
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 批量回收设备
|
||||
|
||||
系统 SHALL 提供批量回收已分配设备的功能,回收时自动同步绑定卡的归属。
|
||||
|
||||
**API 端点**: `POST /api/admin/devices/recall`
|
||||
|
||||
**请求参数**:
|
||||
- `device_ids`: 设备 ID 列表(必填,最多 100 个)
|
||||
- `remark`: 备注(可选)
|
||||
|
||||
**业务规则**:
|
||||
- 只能回收直属下级店铺的设备,不可跨级
|
||||
- 平台回收后:设备和绑定卡的 shop_id 变为 NULL
|
||||
- 代理回收后:设备和绑定卡的 shop_id 变为执行回收的店铺 ID
|
||||
- 创建资产回收记录(asset_type='device')
|
||||
|
||||
**响应**:
|
||||
- `success_count`: 成功数量
|
||||
- `fail_count`: 失败数量
|
||||
- `failed_items`: 失败详情列表
|
||||
|
||||
#### Scenario: 平台回收一级代理的设备
|
||||
|
||||
- **WHEN** 平台管理员回收一级代理店铺(ID=10)的 3 台设备
|
||||
- **THEN** 系统更新这 3 台设备及其绑定卡的 shop_id 为 NULL,创建回收记录
|
||||
|
||||
#### Scenario: 代理回收下级的设备
|
||||
|
||||
- **WHEN** 代理(店铺 ID=10)回收下级店铺(ID=101)的 2 台设备
|
||||
- **THEN** 系统更新这 2 台设备及其绑定卡的 shop_id 为 10,创建回收记录
|
||||
|
||||
#### Scenario: 回收非直属下级的设备
|
||||
|
||||
- **WHEN** 代理(店铺 ID=10)尝试回收非直属下级的设备
|
||||
- **THEN** 系统返回错误,提示"只能回收直属下级店铺的设备"
|
||||
|
||||
@@ -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