feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
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:
@@ -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 格式校验已在此处)风格一致
|
||||
|
||||
### 决策 2:IoT 卡数据库层迁移分两步
|
||||
|
||||
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
|
||||
|
||||
- 无
|
||||
Reference in New Issue
Block a user