## 1. 数据库迁移:IoT 卡 VirtualNo NOT NULL - [x] 1.1 手动清理测试库:执行 `DELETE FROM tb_iot_card WHERE virtual_no IS NULL OR virtual_no = ''`,确认影响行数为 0 后继续(或直接清库) - [x] 1.2 创建迁移文件(如 `000XXX_iot_card_virtual_no_not_null.up.sql`): ```sql ALTER TABLE tb_iot_card ALTER COLUMN virtual_no SET NOT NULL; DROP INDEX IF EXISTS idx_iot_card_virtual_no; CREATE UNIQUE INDEX idx_iot_card_virtual_no ON tb_iot_card(virtual_no) WHERE deleted_at IS NULL; ``` - [x] 1.3 创建对应 down 迁移文件(回滚为 nullable) - [x] 1.4 执行迁移,验证约束生效(尝试插入 virtual_no=NULL 的记录应失败) ## 2. 模型层:更新 IotCard GORM Tag - [x] 2.1 更新 `internal/model/iot_card.go` 中 `VirtualNo` 字段的 GORM tag: - 去掉 `omitempty`(JSON tag) - 将唯一索引条件从 `where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> ''` 简化为 `where:deleted_at IS NULL` - 更新字段注释:从"虚拟号(可空,全局唯一)"改为"虚拟号(必填,全局唯一)" ## 3. Excel 解析层:IoT 卡 VirtualNo 必填校验 - [x] 3.1 更新 `pkg/utils/excel.go` 的 `parseCardRows()` 函数(或等效解析逻辑): - 当 `virtualNo == ""` 时,不再跳过,而是将该行追加到解析错误列表:`ParseErrors = append(ParseErrors, ParseError{Line: lineNum, Reason: "虚拟号(virtual_no)不能为空"})` - 若 Excel 文件中完全没有 VirtualNo 列(`virtualNoCol == -1`),在函数开头直接返回错误:`return nil, errors.New(errors.CodeInvalidParam, "Excel 文件缺少 virtual_no 列,请使用最新模板")` - 确认函数返回值能携带 ParseErrors(若当前无此结构,需扩展返回类型) ## 4. Excel 解析层:设备 VirtualNo 改为失败行 - [x] 4.1 更新 `pkg/utils/excel.go` 的设备行解析逻辑: - 找到 `if row.VirtualNo == "" { continue }` 代码(当前静默跳过) - 改为将该行追加到失败列表:`failedRows = append(failedRows, FailedRow{Line: row.Line, Reason: "设备虚拟号(virtual_no)不能为空"})` - 确认 `skip_count` 不再计入此类行,改计入 `fail_count` ## 5. 任务处理层:IoT 卡导入移除残余跳过逻辑 - [x] 5.1 检查 `internal/task/iot_card_import.go` 中所有 `if card.VirtualNo != ""` 条件: - 唯一性检查处(当前只对非空 VirtualNo 检查):移除条件,强制检查所有行的 VirtualNo - 批内去重处:同样移除条件 - 若 VirtualNo 为空的行此时能到达任务处理层(不应发生,因解析层已拦截),也在此处记录失败并跳过 ## 6. 任务处理层:设备导入统计修正 - [x] 6.1 检查 `internal/task/device_import.go` 中处理空 VirtualNo 行的逻辑: - 确认 Excel 解析层改动后,空 VirtualNo 行已以 `FailedRow` 形式传入,不再需要任务层的额外 `continue` 跳过 - 验证 `result.failCount` 正确累加,`result.skipCount` 不受影响 ## 7. 文档更新 - [x] 7.1 更新 `docs/excel-import-frontend-guide.md` 及路由描述(`internal/routes/iot_card.go`, `internal/routes/device.go`): - IoT 卡导入模板字段表:`virtual_no` 列的"必填"列从"否"改为"是" - 新增说明:"`virtual_no` 为必填列,留空将导致该行导入失败" - 设备导入模板字段表:`virtual_no` 列同样标注"必填",并说明"留空不再跳过,将记录为失败行" ## 8. 验证 - [x] 8.1 构建验证:`go build ./...` 无编译错误(本次修改包 build OK;bootstrap 已有预存在错误与本次无关) - [x] 8.2 LSP 诊断:对所有修改文件运行 lsp_diagnostics,无错误/警告 - [ ] 8.3 IoT 卡导入验证(使用 Hurl 或 curl): - 上传含完整 VirtualNo 的 Excel:导入成功,success_count 正确 - 上传含空 VirtualNo 行的 Excel:该行 fail_count+1,reason 为"虚拟号(virtual_no)不能为空",其他行正常导入 - 上传无 VirtualNo 列的 Excel:全部失败,返回"Excel 文件缺少 virtual_no 列" - 上传重复 VirtualNo 的 Excel:该行 fail_count+1,reason 为"虚拟号已被占用: <值>" - [ ] 8.4 设备导入验证: - 上传含空 VirtualNo 行的 Excel:该行计入 fail_count(而非 skip_count),reason 为"设备虚拟号(virtual_no)不能为空" - [x] 8.5 数据库约束验证(PostgreSQL MCP): - `virtual_no` 列 is_nullable=NO 已验证;新索引 `CREATE UNIQUE INDEX idx_iot_card_virtual_no ON tb_iot_card(virtual_no) WHERE deleted_at IS NULL` 已验证