feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s

主要变更:
- 新增 AssetIdentifier 模型及 Store,统一管理资产标识符(ICCID/IMEI/SN 等)
- 新增迁移:asset_identifier 表、order 表新增 asset_identifier 字段、iot_card.virtual_no NOT NULL 约束
- 资产 Handler/Service/Route 全面重构,支持标识符路由查询与解析
- 新增资产历史订单查询接口,支持跨设备/卡/钱包维度的订单聚合
- 设备与物联卡导入任务强制校验虚拟号,缺失时直接拒绝
- Excel 工具函数优化,前端导入指引文档同步更新
- 归档三个 OpenSpec 提案:asset-identifier-standardization、asset-historical-orders、import-mandatory-virtual-no
- 更新 OpenAPI 文档及相关 DTO

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-07 17:39:36 +08:00
parent 7e489a19fb
commit 80c6f6c756
69 changed files with 3039 additions and 851 deletions

View File

@@ -0,0 +1,64 @@
## Context
**设备**`tb_device.virtual_no` 已是 `NOT NULL + UNIQUE`(数据库层已强制)。但 `pkg/utils/excel.go` 中设备行解析遇到空 VirtualNo 会 `continue`(跳过该行),导入任务报告跳过数量但不报告失败原因,用户无法知道为什么某些行没被导入。
**IoT 卡**`tb_iot_card.virtual_no` 当前为 nullable唯一索引条件是 `WHERE deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> ''`(允许多条 NULL 值)。导入时空 VirtualNo 的卡直接进库,不做任何处理。
两者均需要把"安静跳过/允许空值"改为"明确报错",并在数据库层为 IoT 卡补上 NOT NULL 约束。
## Goals / Non-Goals
**Goals:**
- 导入时 VirtualNo 为空 → 报告为失败行,包含行号和原因
- `tb_iot_card.virtual_no` 加 NOT NULL 约束
- 更新 Excel 解析层、任务处理层、模型层
**Non-Goals:**
- 为现有 NULL 记录自动生成 VirtualNo测试阶段直接清库
- IoT 卡导入的其他字段校验(不在本提案范围内)
- VirtualNo 格式校验(长度/字符集约束,留给未来)
## Decisions
### 决策 1在 Excel 解析层pkg/utils/excel.go拦截空值不在任务层
**选择**:在 `parseCardRows()``parseDeviceRows()` 中,当 VirtualNo 为空时将该行加入解析错误列表ParseError而非 `continue` 跳过
**备选方案**:在任务处理层(`iot_card_import.go` / `device_import.go`)拦截。问题:任务层已有处理批次逻辑,较复杂;解析层更早发现问题,更符合"fail fast"原则
**理由**:越早发现越好;解析层返回 ParseErrors 与当前批量验证逻辑ICCID 格式校验已在此处)风格一致
### 决策 2IoT 卡数据库层迁移分两步
1. **先清库**(测试阶段手动执行):`DELETE FROM tb_iot_card WHERE virtual_no IS NULL OR virtual_no = ''`
2. **再加约束**(迁移文件):
- `ALTER TABLE tb_iot_card ALTER COLUMN virtual_no SET NOT NULL`
- 重建唯一索引,去掉 `WHERE virtual_no IS NOT NULL AND virtual_no <> ''` 条件,改为无条件唯一
**理由**先清数据再加约束迁移不会失败两步操作在同一迁移文件中完成原子执行PostgreSQL 支持事务内 DDL
### 决策 3设备导入不需要数据库迁移只改导入行为
设备的 VirtualNo 数据库层已是 NOT NULL不需要迁移。只需把 `excel.go``if row.VirtualNo == "" { continue }` 改为加入失败列表即可。
## Risks / Trade-offs
| 风险 | 缓解措施 |
|------|---------|
| **现有使用方的 Excel 模板无 VirtualNo 列** | 属于 BREAKING 变更,提前通知;导入失败信息明确说明"VirtualNo 为必填列" |
| **迁移前 IoT 卡存在 NULL 记录导致 ALTER 失败** | 迁移文件中先执行 DELETE 清理(适用测试环境),再 ALTER生产环境需手动确认数据干净后执行 |
| **IoT 卡在注册表(提案一)中未注册 VirtualNo** | 本提案独立于提案一若提案一先完成IoT 卡导入时需同步注册 VirtualNo 到注册表(在提案一 task 6.2 中已覆盖) |
## Migration Plan
1. 清理测试库中 `virtual_no IS NULL` 的 IoT 卡记录
2. 执行数据库迁移:`tb_iot_card.virtual_no` 加 NOT NULL重建唯一索引
3. 更新 `pkg/utils/excel.go`(解析层)
4. 更新 `internal/task/iot_card_import.go`(任务层,去除残余跳过逻辑)
5. 更新 `internal/task/device_import.go`(任务层,同上)
6. 更新 `internal/model/iot_card.go`GORM tag
7. 更新文档 `docs/excel-import-frontend-guide.md`
## Open Questions
-