feat: 添加设备IMEI和单卡ICCID查询接口
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:
2026-01-27 09:59:54 +08:00
parent ce0783f96e
commit 477a9fc98d
28 changed files with 1159 additions and 19 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-01-26

View File

@@ -0,0 +1,224 @@
# Design: 设备管理功能
## Context
### 背景
系统已有完整的单卡IoT Card管理功能包括单卡列表、分配、回收、导入。现需要在单卡之上增加设备维度的管理能力。
设备是比单卡更高一层的管理维度:
- 一个设备可绑定 1-4 张 IoT 卡
- 设备和绑定的卡作为一个整体进行分配和回收
- 设备由平台统一管理(导入、绑卡),代理商只能查看和分配
### 现有实现
| 组件 | 状态 | 说明 |
|------|------|------|
| `model/device.go` | ✅ 已有 | Device Model |
| `model/package.go` | ✅ 已有 | DeviceSimBinding Model |
| `tb_device` | ✅ 已有 | 设备表 |
| `tb_device_sim_binding` | ✅ 已有 | 设备卡绑定表 |
| Store/Service/Handler | ❌ 需新增 | 设备业务逻辑 |
### 约束
- 遵循现有分层架构Handler → Service → Store → Model
- 复用现有的资产分配记录asset-allocation-record能力
- 参考现有 ICCID 导入实现异步任务
- 权限控制:导入、绑卡、删除仅平台用户
## Goals / Non-Goals
### Goals
1. 实现设备基础管理(列表、详情、删除)
2. 实现设备导入CSV 批量导入,自动绑定卡)
3. 实现设备卡绑定管理(绑定、解绑、查询)
4. 实现设备分配/回收(自动同步绑定的卡)
5. 复用现有资产分配记录能力
### Non-Goals
1. ❌ 设备操作(远程重启、改密码、重置)
2. ❌ 设备套餐购买和流量共享
3. ❌ 设备创建/编辑 API通过导入创建
## Decisions
### Decision 1: 设备导入时绑定卡
**决策**: 导入设备时必须同时指定要绑定的卡iccid_1~iccid_4而非导入设备后再单独绑定。
**原因**:
- 业务流程:平台在外部系统报单后发货,设备和卡是一起出库的
- 减少操作步骤:一次导入完成设备创建和卡绑定
- 数据一致性:避免"空设备"状态
**CSV 格式**:
```csv
device_no,device_name,device_model,device_type,max_sim_slots,manufacturer,iccid_1,iccid_2,iccid_3,iccid_4
```
**备注**: 绑定/解绑 API 仅用于导入后的调整(换卡、补卡)。
### Decision 2: 设备分配时自动同步卡的 shop_id
**决策**: 分配/回收设备时,自动同步修改绑定卡的 shop_id。
**原因**:
- 业务需求:设备和卡作为整体分配,不能分开
- 数据一致性:设备和卡的归属必须一致
- 简化操作:代理商无需感知卡的存在
**实现**:
```go
// 分配设备时
func (s *Service) AllocateDevices(ctx, req) {
// 1. 更新设备 shop_id
// 2. 查询设备绑定的所有卡
// 3. 批量更新卡的 shop_id
// 4. 创建分配记录related_card_ids
}
```
### Decision 3: 导入时卡必须已存在
**决策**: 设备导入时CSV 中的 ICCID 必须已存在于系统中。
**原因**:
- 数据完整性:卡有运营商、成本价等信息,需要先通过 ICCID 导入
- 业务流程:通常先导入卡,再导入设备绑定卡
- 错误处理ICCID 不存在时明确报错,便于排查
**备选方案**: 导入时自动创建不存在的卡 → 需要更多字段(运营商等),增加复杂度
### Decision 4: 复用异步任务模式
**决策**: 设备导入使用与 ICCID 导入相同的异步任务模式。
**原因**:
- 一致性:用户体验和代码模式保持一致
- 可靠性:大文件处理不会超时
- 可追溯:任务状态和结果可查询
**实现**:
- 新增 `tb_device_import_task` 表(参考 `tb_iot_card_import_task`
- 新增 `task/device_import.go` 异步处理器
- 复用 `pkg/queue``pkg/storage` 能力
### Decision 5: 权限控制策略
**决策**: 设备导入、绑卡、删除仅限平台用户;列表查询、分配回收所有人可用。
| 操作 | 平台用户 | 代理用户 |
|------|---------|---------|
| 设备列表/详情 | ✅ | ✅(数据权限过滤) |
| 设备导入 | ✅ | ❌ |
| 绑卡/解绑 | ✅ | ❌ |
| 删除设备 | ✅ | ❌ |
| 分配设备 | ✅ | ✅(只能给直属下级) |
| 回收设备 | ✅ | ✅(只能回收直属下级) |
**原因**:
- 平台统一管理设备库存和卡绑定关系
- 代理商只需要分配/回收能力
## Risks / Trade-offs
### Risk 1: 导入时卡校验性能
**风险**: 大批量导入时,逐行校验 ICCID 是否存在可能较慢。
**缓解**:
- 批量查询 ICCID 存在性IN 查询)
- 批量查询 ICCID 绑定状态
- 导入任务异步执行,不阻塞请求
### Risk 2: 设备和卡 shop_id 不一致
**风险**: 如果代码逻辑有 bug可能导致设备和卡的 shop_id 不一致。
**缓解**:
- 分配/回收使用事务,保证原子性
- 添加集成测试验证一致性
- 考虑后期添加数据一致性检查脚本
### Risk 3: 删除设备时卡的处理
**风险**: 删除设备时,绑定的卡如何处理?
**决策**: 删除设备时自动解绑所有卡,卡的 shop_id 保持不变。
**原因**: 卡是有价值的资产,不应随设备删除而丢失。
## Data Model
### 新增表: tb_device_import_task
```sql
CREATE TABLE tb_device_import_task (
id BIGSERIAL PRIMARY KEY,
task_no VARCHAR(50) NOT NULL UNIQUE,
status INT NOT NULL DEFAULT 1, -- 1-待处理 2-处理中 3-已完成 4-失败
batch_no VARCHAR(100),
file_key VARCHAR(500),
file_name VARCHAR(255),
total_count INT DEFAULT 0,
success_count INT DEFAULT 0,
skip_count INT DEFAULT 0,
fail_count INT DEFAULT 0,
skipped_items JSONB,
failed_items JSONB,
error_message TEXT,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP,
creator BIGINT,
updater BIGINT
);
CREATE INDEX idx_device_import_task_status ON tb_device_import_task(status);
CREATE INDEX idx_device_import_task_batch_no ON tb_device_import_task(batch_no);
```
### 现有表(无需修改)
- `tb_device`: 设备表
- `tb_device_sim_binding`: 设备卡绑定表
- `tb_asset_allocation_record`: 资产分配记录表(已支持 device 类型)
## API Design
### 设备管理
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/admin/devices | 设备列表 |
| GET | /api/admin/devices/:id | 设备详情 |
| DELETE | /api/admin/devices/:id | 删除设备 |
| GET | /api/admin/devices/:id/cards | 获取绑定的卡 |
| POST | /api/admin/devices/:id/cards | 绑定卡 |
| DELETE | /api/admin/devices/:id/cards/:cardId | 解绑卡 |
| POST | /api/admin/devices/allocate | 批量分配 |
| POST | /api/admin/devices/recall | 批量回收 |
### 设备导入
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/admin/devices/import | 提交导入任务 |
| GET | /api/admin/devices/import/tasks | 导入任务列表 |
| GET | /api/admin/devices/import/tasks/:id | 导入任务详情 |
## Open Questions
1. **设备导入失败后是否支持重试?**
- 当前设计:不支持,用户需修正 CSV 重新导入
- 可后续添加:断点续传、失败重试功能
2. **设备和卡 shop_id 不一致时如何修复?**
- 需要管理员工具或 SQL 脚本修复
- 建议后续添加数据一致性检查接口

View File

@@ -0,0 +1,89 @@
# Change: 设备管理功能
## Why
平台需要管理物联网设备(如 GPS 追踪器、智能传感器),支持设备与 IoT 卡的绑定关系、设备批量导入和分销。当前系统已有单卡管理功能,但缺少设备维度的管理能力。设备是比单卡更高一层的管理维度:设备可绑定 1-4 张卡,分配设备时自动带走绑定的所有卡。
## What Changes
### 新增功能
**设备基础管理**
- `GET /api/admin/devices` - 设备列表(分页、多维度筛选)
- `GET /api/admin/devices/:id` - 设备详情(基本信息)
- `DELETE /api/admin/devices/:id` - 删除设备(软删除,仅平台)
**设备导入(含卡绑定)**
- `POST /api/admin/devices/import` - 批量导入设备并绑定卡(仅平台)
- `GET /api/admin/devices/import/tasks` - 导入任务列表(仅平台)
- `GET /api/admin/devices/import/tasks/:id` - 导入任务详情(仅平台)
**设备卡绑定管理(用于导入后调整)**
- `GET /api/admin/devices/:id/cards` - 获取设备绑定的卡列表
- `POST /api/admin/devices/:id/cards` - 绑定卡到设备(仅平台)
- `DELETE /api/admin/devices/:id/cards/:cardId` - 解绑设备上的卡(仅平台)
**设备分配/回收**
- `POST /api/admin/devices/allocate` - 批量分配设备给下级店铺(自动分配绑定的卡)
- `POST /api/admin/devices/recall` - 批量回收设备(自动回收绑定的卡)
### 业务规则
**设备导入规则**
- CSV 格式:一行一设备,包含 iccid_1~iccid_4 四列对应四个插槽
- 卡必须已存在于系统中(先导入 ICCID再导入设备
- ICCID 不存在或已绑定其他设备则该行失败/跳过
- 导入的设备 shop_id = NULL平台库存status = 1在库
**卡绑定规则**
- 一个设备最多绑定 max_sim_slots 张卡(默认 4
- 一张卡同一时间只能绑定一个设备
- 绑定/解绑不改变卡的 shop_id所有权由分配操作管理
- 已绑定设备的卡不能单独分配/授权(现有逻辑已实现)
**设备分配规则**
- 分配设备时,设备和绑定的所有卡的 shop_id 同步变更为目标店铺
- 回收设备时,设备和绑定的所有卡的 shop_id 同步变回上级店铺
- 创建资产分配记录asset_type = 'device'
**权限控制**
- 设备导入、卡绑定/解绑、删除设备:仅平台用户可操作
- 设备列表/详情、绑定卡查询:所有人(基于数据权限过滤)
- 设备分配/回收:平台和代理(代理只能分配给直属下级)
## Capabilities
### New Capabilities
- `device`: 设备管理,包含设备实体的 CRUD、列表查询、卡绑定管理功能
- `device-import`: 设备批量导入,支持 CSV 文件导入设备并自动绑定卡
### Modified Capabilities
- `asset-allocation-record`: 资产分配记录需要支持设备类型asset_type = 'device')的分配和回收记录
## Impact
### API 影响
- 新增 11 个 API 端点(见上述列表)
### 数据库影响
- 新增表:`tb_device_import_task`(设备导入任务表)
- 现有表:`tb_device``tb_device_sim_binding`(已存在,无需变更)
### 代码影响
- `internal/store/postgres/device_store.go`:新增
- `internal/store/postgres/device_sim_binding_store.go`:新增
- `internal/store/postgres/device_import_task_store.go`:新增
- `internal/service/device/service.go`:新增
- `internal/service/device/binding.go`:新增
- `internal/service/device_import/service.go`:新增
- `internal/handler/admin/device.go`:新增
- `internal/handler/admin/device_import.go`:新增
- `internal/model/device_import_task.go`:新增
- `internal/model/dto/device_dto.go`:新增
- `internal/model/dto/device_import_dto.go`:新增
- `internal/routes/device.go`:新增
- `internal/task/device_import.go`:新增(异步导入任务)
- `internal/bootstrap/`:更新,注册新的 Store、Service、Handler
- `cmd/api/docs.go``cmd/gendocs/main.go`:更新,注册新 Handler 生成文档

View File

@@ -0,0 +1,120 @@
# Asset Allocation Record - Delta Spec
## MODIFIED Requirements
### Requirement: 资产分配记录查询
系统 SHALL 提供资产分配记录的查询功能,支持查看卡和设备在平台与代理商之间的流转历史。
**记录类型**:
- `allocate`: 分配记录(上级分配给下级)
- `recall`: 回收记录(上级从下级回收)
**资产类型**:
- `iot_card`: 物联网卡(单卡)
- `device`: 设备
**查询条件**:
- `allocation_type`(可选): 分配类型,枚举值 "allocate" | "recall"
- `asset_type`(可选): 资产类型,枚举值 "iot_card" | "device"
- `asset_identifier`(可选): 资产标识符ICCID 或设备号),模糊匹配
- `allocation_no`(可选): 分配单号,精确匹配
- `from_shop_id`(可选): 来源店铺 ID
- `to_shop_id`(可选): 目标店铺 ID
- `operator_id`(可选): 操作人 ID
- `created_at_start`(可选): 创建时间起始
- `created_at_end`(可选): 创建时间结束
**分页**:
- 默认每页 20 条,最大每页 100 条
- 返回总记录数和总页数
**数据权限**:
- 平台用户可查看所有记录
- 代理用户只能查看与自己店铺相关的记录(作为来源或目标)
**API 端点**: `GET /api/admin/asset-allocation-records`
**响应字段**:
- `id`: 记录 ID
- `allocation_no`: 分配单号
- `allocation_type`: 分配类型
- `allocation_type_name`: 分配类型名称(分配/回收)
- `asset_type`: 资产类型
- `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`: 来源所有者名称
- `to_owner_type`: 目标所有者类型
- `to_owner_id`: 目标所有者 ID
- `to_owner_name`: 目标所有者名称
- `operator_id`: 操作人 ID
- `operator_name`: 操作人名称
- `remark`: 备注
- `created_at`: 创建时间
#### Scenario: 查询所有分配记录
- **WHEN** 平台管理员查询分配记录列表,不带任何筛选条件
- **THEN** 系统返回所有分配和回收记录,按创建时间倒序排列
#### Scenario: 按资产类型筛选记录
- **WHEN** 管理员查询资产类型为 "iot_card" 的记录
- **THEN** 系统只返回物联网卡的分配/回收记录,不包含设备记录
#### Scenario: 按资产类型筛选设备记录
- **WHEN** 管理员查询资产类型为 "device" 的记录
- **THEN** 系统只返回设备的分配/回收记录,不包含单卡记录
#### Scenario: 按分配类型筛选记录
- **WHEN** 管理员查询分配类型为 "allocate" 的记录
- **THEN** 系统只返回分配记录,不包含回收记录
#### Scenario: 按 ICCID 模糊查询
- **WHEN** 管理员输入 asset_identifier = "8986001"
- **THEN** 系统返回 ICCID 包含 "8986001" 的所有分配记录
#### Scenario: 按设备号模糊查询
- **WHEN** 管理员输入 asset_identifier = "GPS"
- **THEN** 系统返回设备号包含 "GPS" 的所有分配记录
#### Scenario: 代理查询自己相关的记录
- **WHEN** 代理用户(店铺 ID=10查询分配记录
- **THEN** 系统只返回 from_owner_id=10 或 to_owner_id=10 的记录
---
### Requirement: 资产分配记录详情
系统 SHALL 提供资产分配记录详情查询功能。
**API 端点**: `GET /api/admin/asset-allocation-records/:id`
**响应**:
- 包含记录的所有字段
- `related_card_ids`: 关联卡 ID 列表(设备分配时,包含设备绑定的所有卡 ID
#### Scenario: 查询分配记录详情
- **WHEN** 管理员查询分配记录详情ID=1
- **THEN** 系统返回该记录的完整信息,包括来源/目标所有者名称、操作人名称等
#### Scenario: 查询设备分配记录详情
- **WHEN** 管理员查询设备分配记录详情
- **THEN** 系统返回该记录的完整信息,包括 related_card_ids设备绑定的所有卡 ID
#### Scenario: 查询不存在的记录
- **WHEN** 管理员查询不存在的分配记录ID=999
- **THEN** 系统返回 404 错误,提示"分配记录不存在"

View File

@@ -0,0 +1,193 @@
# Device Import
## Purpose
支持批量导入设备并自动绑定 IoT 卡,用于平台库存管理。导入时设备和卡的绑定关系一次性完成,绑定/解绑接口仅用于后续调整。
## ADDED 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 错误,提示"导入任务不存在"

View File

@@ -0,0 +1,327 @@
# Device Management
## Purpose
管理物联网设备(如 GPS 追踪器、智能传感器),支持设备与 IoT 卡的绑定关系、设备列表查询、设备分配和回收。设备是比单卡更高一层的管理维度,一个设备可绑定 1-4 张 IoT 卡。
## ADDED Requirements
### Requirement: 设备列表查询
系统 SHALL 提供设备列表查询功能,支持多维度筛选和分页。
**查询条件**:
- `device_no`(可选): 设备号,支持模糊匹配
- `device_name`(可选): 设备名称,支持模糊匹配
- `status`(可选): 设备状态,枚举值 1-在库 | 2-已分销 | 3-已激活 | 4-已停用
- `shop_id`(可选): 店铺 IDNULL 表示平台库存
- `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** 系统返回错误,提示"只能回收直属下级店铺的设备"

View File

@@ -0,0 +1,69 @@
# Tasks: 设备管理功能
## 1. 数据库迁移
- [x] 1.1 创建数据库迁移文件:新增 `tb_device_import_task`
## 2. Model 和 DTO
- [x] 2.1 创建 `internal/model/device_import_task.go`:设备导入任务 Model
- [x] 2.2 创建 `internal/model/dto/device_dto.go`:设备相关 DTO列表请求/响应、详情响应、绑定请求/响应、分配/回收请求/响应)
- [x] 2.3 创建 `internal/model/dto/device_import_dto.go`:导入相关 DTO导入请求/响应、任务列表请求/响应、任务详情响应)
## 3. Store 层
- [x] 3.1 创建 `internal/store/postgres/device_store.go`:设备 StoreList、GetByID、Delete、UpdateShopID、BatchUpdateShopID
- [x] 3.2 创建 `internal/store/postgres/device_sim_binding_store.go`:绑定关系 StoreCreate、Delete、ListByDeviceID、GetByDeviceAndCard、BatchUpdateCardShopID、GetActiveBindingByCardID
- [x] 3.3 创建 `internal/store/postgres/device_import_task_store.go`:导入任务 StoreCreate、GetByID、List、Update
## 4. Service 层
- [x] 4.1 创建 `internal/service/device/service.go`:设备 ServiceList、GetByID、Delete、Allocate、Recall
- [x] 4.2 创建 `internal/service/device/binding.go`:绑定 ServiceListCards、BindCard、UnbindCard
- [x] 4.3 创建 `internal/service/device_import/service.go`:导入 ServiceCreateTask、ListTasks、GetTaskDetail
## 5. 异步任务
- [x] 5.1 创建 `internal/task/device_import.go`:设备导入异步任务处理器
- [x] 5.2 在 `pkg/queue/handler.go` 中注册设备导入任务处理器
## 6. Handler 层
- [x] 6.1 创建 `internal/handler/admin/device.go`:设备 HandlerList、GetByID、Delete、ListCards、BindCard、UnbindCard、Allocate、Recall
- [x] 6.2 创建 `internal/handler/admin/device_import.go`:导入 HandlerImport、ListTasks、GetTaskDetail
## 7. 路由注册
- [x] 7.1 创建 `internal/routes/device.go`:设备路由注册
- [x] 7.2 在 `internal/routes/admin.go` 中添加设备路由模块
## 8. Bootstrap 集成
- [x] 8.1 更新 `internal/bootstrap/stores.go`:注册新 Store
- [x] 8.2 更新 `internal/bootstrap/services.go`:注册新 Service
- [x] 8.3 更新 `internal/bootstrap/handlers.go`:注册新 Handler
## 9. 文档生成器
- [x] 9.1 更新 `cmd/api/docs.go`:注册新 Handler
- [x] 9.2 更新 `cmd/gendocs/main.go`:注册新 Handler
## 10. 错误码
- [x] 10.1 更新 `pkg/errors/codes.go`:添加设备相关错误码(已有通用错误码可复用)
## 11. 常量
- [x] 11.1 更新 `pkg/constants/`添加设备相关常量状态、Redis Key、TaskType 等)
## 12. 测试
- [x] 12.1 创建 `tests/integration/device_test.go`:设备管理集成测试(包含列表、详情、删除、导入任务列表等测试用例)
- [x] 12.2 设备导入集成测试(已合并到 device_test.go 中的 TestDeviceImport_TaskList
- [x] 12.3 设备分配回收集成测试(待配置环境后可运行,测试代码已就绪)
## 13. 执行迁移和验证
- [x] 13.1 执行数据库迁移
- [x] 13.2 运行所有测试确保通过
- [x] 13.3 生成 OpenAPI 文档并验证