Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-07-import-mandatory-virtual-no/tasks.md
huang 80c6f6c756
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m21s
feat: 资产标识符标准化、资产历史订单查询及导入虚拟号强制验证
主要变更:
- 新增 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>
2026-04-07 17:39:36 +08:00

4.5 KiB
Raw Blame History

1. 数据库迁移IoT 卡 VirtualNo NOT NULL

  • 1.1 手动清理测试库:执行 DELETE FROM tb_iot_card WHERE virtual_no IS NULL OR virtual_no = '',确认影响行数为 0 后继续(或直接清库)
  • 1.2 创建迁移文件(如 000XXX_iot_card_virtual_no_not_null.up.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;
    
  • 1.3 创建对应 down 迁移文件(回滚为 nullable
  • 1.4 执行迁移,验证约束生效(尝试插入 virtual_no=NULL 的记录应失败)

2. 模型层:更新 IotCard GORM Tag

  • 2.1 更新 internal/model/iot_card.goVirtualNo 字段的 GORM tag
    • 去掉 omitemptyJSON tag
    • 将唯一索引条件从 where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '' 简化为 where:deleted_at IS NULL
    • 更新字段注释:从"虚拟号(可空,全局唯一)"改为"虚拟号(必填,全局唯一)"

3. Excel 解析层IoT 卡 VirtualNo 必填校验

  • 3.1 更新 pkg/utils/excel.goparseCardRows() 函数(或等效解析逻辑):
    • 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 改为失败行

  • 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 卡导入移除残余跳过逻辑

  • 5.1 检查 internal/task/iot_card_import.go 中所有 if card.VirtualNo != "" 条件:
    • 唯一性检查处(当前只对非空 VirtualNo 检查):移除条件,强制检查所有行的 VirtualNo
    • 批内去重处:同样移除条件
    • 若 VirtualNo 为空的行此时能到达任务处理层(不应发生,因解析层已拦截),也在此处记录失败并跳过

6. 任务处理层:设备导入统计修正

  • 6.1 检查 internal/task/device_import.go 中处理空 VirtualNo 行的逻辑:
    • 确认 Excel 解析层改动后,空 VirtualNo 行已以 FailedRow 形式传入,不再需要任务层的额外 continue 跳过
    • 验证 result.failCount 正确累加,result.skipCount 不受影响

7. 文档更新

  • 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. 验证

  • 8.1 构建验证:go build ./... 无编译错误(本次修改包 build OKbootstrap 已有预存在错误与本次无关)
  • 8.2 LSP 诊断:对所有修改文件运行 lsp_diagnostics无错误/警告
  • 8.3 IoT 卡导入验证(使用 Hurl 或 curl
    • 上传含完整 VirtualNo 的 Excel导入成功success_count 正确
    • 上传含空 VirtualNo 行的 Excel该行 fail_count+1reason 为"虚拟号(virtual_no)不能为空",其他行正常导入
    • 上传无 VirtualNo 列的 Excel全部失败返回"Excel 文件缺少 virtual_no 列"
    • 上传重复 VirtualNo 的 Excel该行 fail_count+1reason 为"虚拟号已被占用: <值>"
  • 8.4 设备导入验证:
    • 上传含空 VirtualNo 行的 Excel该行计入 fail_count而非 skip_countreason 为"设备虚拟号(virtual_no)不能为空"
  • 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 已验证