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,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 脚本修复
- 建议后续添加数据一致性检查接口