## 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 - 无