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>
225 lines
7.1 KiB
Markdown
225 lines
7.1 KiB
Markdown
# 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 脚本修复
|
||
- 建议后续添加数据一致性检查接口
|