## Why

虚拟号（VirtualNo）是系统中资产的核心业务标识符，也是提案一确立的接口参数规范的基础。然而当前导入流程将其作为可选字段：IoT 卡导入时 VirtualNo 可为空（数据库层 nullable），设备导入时 VirtualNo 为空的行会被静默跳过而非报错。这导致可能存在没有虚拟号的资产进入系统，无法通过统一标识符接口操作，与资产标识符规范化目标冲突。

## What Changes

- **[BREAKING]** IoT 卡导入：VirtualNo 改为必填，Excel 中 VirtualNo 列为空的行报错拒绝
- **[BREAKING]** 设备导入：VirtualNo 为空的行从原有的"静默跳过"改为"记录为失败行"并报告原因
- **[NEW]** 数据库迁移：`tb_iot_card.virtual_no` 添加 NOT NULL 约束和去除条件唯一索引改为无条件唯一索引（配合清库执行）
- 更新 Excel 导入错误提示，明确 VirtualNo 为必填项
- 更新接口文档，标注 VirtualNo 为必填列

## Capabilities

### New Capabilities

（无新能力——本提案为约束强化，不新增业务功能）

### Modified Capabilities

- `iot-card-import-task`：VirtualNo 校验规则从"可选"改为"必填"，空值触发失败行记录
- `device-import`：VirtualNo 校验规则从"跳过空值行"改为"空值行计入失败并报告原因"

## Impact

**受影响的代码**：
- `pkg/utils/excel.go`：`parseCardRows()` 和设备行解析逻辑，增加 VirtualNo 空值校验
- `internal/task/iot_card_import.go`：移除"空 VirtualNo 跳过"逻辑，改为记录失败行
- `internal/task/device_import.go`：将"空 VirtualNo 跳过"改为"记录失败行"
- `internal/model/iot_card.go`：`VirtualNo` 字段 GORM tag 更新，去掉 nullable，更新注释
- `migrations/`：新增迁移文件，为 `tb_iot_card.virtual_no` 加 NOT NULL 约束

**前置条件**：
- 测试数据库中现有 `virtual_no IS NULL` 的 IoT 卡记录需先清除（测试阶段直接清库）
- 设备的 `virtual_no` 在数据库层已经是 NOT NULL，无需迁移，仅修改导入行为

**破坏性影响**：
- 现有 IoT 卡导入 Excel 模板如果没有 VirtualNo 列，导入将全部失败；需通知使用方更新模板
