## 1. 数据库迁移（先行）

- [x] 1.1 创建迁移文件：`tb_device.device_no` → `virtual_no`，`tb_personal_customer_device.device_no` → `virtual_no`（两张表在同一个迁移文件中完成）
- [x] 1.2 创建迁移文件：`tb_iot_card` 新增 `virtual_no VARCHAR(50)` 字段，创建部分唯一索引 `idx_iot_card_virtual_no`（`WHERE deleted_at IS NULL`）
- [x] 1.3 创建迁移文件：`tb_package` 新增 `virtual_ratio DECIMAL(10,6) DEFAULT 1.0` 字段，回填现有数据（根据 `enable_virtual_data` 和 `real_data_mb / virtual_data_mb` 计算）
- [x] 1.4 执行全部迁移，验证三张表结构变更成功（PostgreSQL MCP 确认）

## 2. 数据模型更新（device_no 全量改名）

- [x] 2.1 更新 `internal/model/device.go`：`DeviceNo` → `VirtualNo`，GORM column tag 从 `device_no` 改为 `virtual_no`，更新注释
- [x] 2.2 更新 `internal/model/personal_customer_device.go`：`DeviceNo` → `VirtualNo`，column tag 同步更新
- [x] 2.3 更新 `internal/model/dto/device_dto.go`：`DeviceResponse.DeviceNo` → `VirtualNo`，JSON tag 从 `"device_no"` 改为 `"virtual_no"`；`ListDeviceRequest.DeviceNo` → `VirtualNo`，query tag 同步更新；`AllocationDeviceFailedItem.DeviceNo` → `VirtualNo`；`DeviceSeriesBindngFailedItem.DeviceNo` → `VirtualNo`
- [x] 2.4 全量搜索代码中所有 `.DeviceNo` 引用（Store/Service/Handler 层），逐一替换为 `.VirtualNo`（使用 lsp_rename 保证全量覆盖）
- [x] 2.5 运行 `go build ./...` 确认无编译错误，运行 `lsp_diagnostics` 确认无类型错误

## 3. IotCard 模型更新（新增 virtual_no 字段）

- [x] 3.1 更新 `internal/model/iot_card.go`：新增 `VirtualNo string` 字段，GORM tag 包含 `column:virtual_no; type:varchar(50); uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL` 注释
- [x] 3.2 运行 `lsp_diagnostics` 确认模型字段无错误

## 4. Package 模型更新（新增 virtual_ratio 字段）

- [x] 4.1 更新 `internal/model/package.go`：`Package` 结构体新增 `VirtualRatio float64` 字段，GORM tag `column:virtual_ratio; type:decimal(10,6); default:1.0`
- [x] 4.2 更新 `internal/service/package/service.go`：在套餐创建和更新逻辑中自动计算并存储 `virtual_ratio`（`enable_virtual_data=true` 且 `virtual_data_mb>0` 时 = `real_data_mb/virtual_data_mb`，否则 = 1.0）
- [x] 4.3 运行 `lsp_diagnostics` 确认无错误

## 5. Redis 常量新增

- [x] 5.1 在 `pkg/constants/redis.go` 新增 `RedisDeviceProtectKey(deviceID uint, action string) string`（格式：`protect:device:{id}:{action}`，TTL 注释：1 小时）
- [x] 5.2 在 `pkg/constants/redis.go` 新增 `RedisDeviceRefreshCooldownKey(deviceID uint) string`（格式：`refresh:cooldown:device:{id}`，TTL 注释：冷却时长，建议 30 秒）
- [x] 5.3 在 `pkg/constants/redis.go` 新增 `RedisPollingQueueProtectKey() string`（格式：`polling:queue:protect`）
- [x] 5.4 在 `pkg/constants/` 新增设备保护期时长常量 `DeviceProtectPeriodDuration = 1 * time.Hour`，设备刷新冷却时长常量 `DeviceRefreshCooldownDuration = 30 * time.Second`

## 6. RefreshCardDataFromGateway 方法增强

- [x] 6.1 在 `internal/service/iot_card/service.go` 中将 `SyncCardStatusFromGateway` 改名为 `RefreshCardDataFromGateway`
- [x] 6.2 增强方法实现：调用网关查询卡状态（网络状态）、实名状态、本月流量用量，将结果写回 DB：更新 `network_status`、`real_name_status`、`current_month_usage_mb`、`last_sync_time`（参考 polling_handler.go 中的完整同步逻辑）
- [x] 6.3 更新所有调用 `SyncCardStatusFromGateway` 的地方改为 `RefreshCardDataFromGateway`（全局搜索替换）
- [x] 6.4 运行 `go build ./...` 确认无编译错误

## 7. 新建 AssetService

- [x] 7.1 创建 `internal/service/asset/service.go`，定义 `Service` 结构体，依赖注入：DeviceStore、IotCardStore、PackageUsageStore、PackageStore、Redis（通过现有 bootstrap 体系接入）
- [x] 7.2 实现 `Resolve(ctx, identifier string) (*dto.AssetResolveResponse, error)`：先查设备（virtual_no/imei/sn），再查卡（virtual_no/iccid/msisdn），应用数据权限过滤，聚合套餐流量（含 virtual_ratio 换算）、保护期状态、绑定信息
- [x] 7.3 实现 `GetRealtimeStatus(ctx, assetType string, id uint) (*dto.AssetRealtimeStatusResponse, error)`：仅读 DB/Redis 持久化数据，不调网关；card 返回网络状态/实名/流量/最后同步；device 返回保护期状态+所有绑定卡状态
- [x] 7.4 实现 `Refresh(ctx, assetType string, id uint) (*dto.AssetRealtimeStatusResponse, error)`：card 调 `RefreshCardDataFromGateway`；device 检查 Redis 冷却期（429），可刷新则遍历绑定卡调 `RefreshCardDataFromGateway`，设置冷却 Key
- [x] 7.5 实现 `GetPackages(ctx, assetType string, id uint) ([]*dto.AssetPackageResponse, error)`：按 asset_type 查 PackageUsage（card→iot_card_id，device→device_id），全量返回按创建时间倒序，含 virtual_ratio 展示换算
- [x] 7.6 实现 `GetCurrentPackage(ctx, assetType string, id uint) (*dto.AssetPackageResponse, error)`：查 status=1 且 master_usage_id IS NULL 的主套餐，无则返回 ErrNotFound

## 8. 设备停复机 Service

- [x] 8.1 在 `internal/service/device/service.go` 实现 `StopDevice(ctx, deviceID uint) (*dto.DeviceSuspendResponse, error)`：验证设备存在、检查保护期、获取已实名绑定卡、批量调网关停机、更新卡状态、设置 Redis stop 保护期（部分失败时仍设置）
- [x] 8.2 实现 `StartDevice(ctx, deviceID uint) error`：验证设备存在、检查保护期、获取已实名绑定卡、批量调网关复机、更新卡状态、设置 Redis start 保护期

## 9. 卡停复机 Service 扩展

- [x] 9.1 在 `internal/service/iot_card/stop_resume_service.go` 实现 `ManualStopCard(ctx, iccid string) error`：通过 ICCID 查卡、验证已实名、检查绑定设备的保护期（stop 保护期允许、start 保护期允许）、调网关停机、更新卡状态
- [x] 9.2 实现 `ManualStartCard(ctx, iccid string) error`：通过 ICCID 查卡、验证已实名、检查绑定设备的保护期（stop 保护期→拒绝 403、start 保护期→允许）、调网关复机、更新卡状态

## 10. DTO 新增

- [x] 10.1 在 `internal/model/dto/` 新增 `asset_dto.go`，定义以下 DTO（含所有字段及 description tag）：
  - `AssetResolveResponse`、`BoundCardInfo`、`AssetRealtimeStatusResponse`、`AssetPackageResponse`
- [x] 10.2 `DeviceSuspendResponse`（成功信息 + 失败卡列表，已在 device_dto.go 中）

## 11. 新建 AssetHandler 和路由注册

- [x] 11.1 创建 `internal/handler/admin/asset.go`，定义 `AssetHandler` 结构体和 9 个 Handler 方法（Resolve、RealtimeStatus、Refresh、Packages、CurrentPackage、StopDevice、StartDevice、StopCard、StartCard）
- [x] 11.2 创建 `internal/routes/asset.go` 注册 `/api/admin/assets/*` 路由（9 个端点）；企业账号访问 resolve 时在 Handler 层检查 user_type 返回 403
- [x] 11.3 更新 `internal/bootstrap/types.go`、`services.go`、`handlers.go`，将 Asset、StopResumeService 加入 bootstrap 体系；更新 docs 文件

## 12. 轮询系统新增保护期一致性检查任务

- [x] 12.1 `RedisPollingQueueProtectKey()` 已存在（Task 5.3）
- [x] 12.2 在 `internal/task/polling_handler.go` 新增 `HandleProtectConsistencyCheck`：检查 is_standalone、real_name_status、设备保护期 Redis Key，按规则强制同步网络状态
- [x] 12.3 在 `pkg/constants/constants.go` 添加 `TaskTypePollingProtect`，在 `pkg/queue/handler.go` 注册处理器

## 13. 卡 ICCID 导入支持 virtual_no 列

- [x] 13.1 `pkg/utils/excel.go` 新增 virtual_no 列识别（关键字：virtual_no/VirtualNo/虚拟号/设备号）
- [x] 13.2 `internal/task/iot_card_import.go` 实现 virtual_no 唯一性校验和写入逻辑；`IotCardStore` 新增 `ExistsByVirtualNoBatch`

## 14. 删除废弃接口

### 14a. 废弃停复机接口

- [x] 14.1 删除 `internal/handler/admin/enterprise_card.go` 中的 `SuspendCard` 和 `ResumeCard` Handler
- [x] 14.2 删除 `internal/handler/h5/enterprise_device.go` 中的 `SuspendCard` 和 `ResumeCard` Handler
- [x] 14.3 删除 `internal/handler/admin/iot_card.go` 中的 `StopCard` 和 `StartCard` Handler
- [x] 14.4 清理对应路由注册，`go build ./...` 通过

### 14b. 废弃详情查询和网关直查接口

- [x] 14.5 删除 `internal/handler/admin/iot_card.go` 中的 `GetByICCID` Handler
- [x] 14.6 删除 `internal/handler/admin/iot_card.go` 中的 `GetGatewayStatus`、`GetGatewayFlow`、`GetGatewayRealname` 三个 Handler
- [x] 14.7 删除 `internal/handler/admin/device.go` 中的 `GetByID` Handler
- [x] 14.8 删除 `internal/handler/admin/device.go` 中的 `GetByIdentifier` Handler
- [x] 14.9 删除 `internal/handler/admin/device.go` 中的 `GetGatewayInfo` Handler
- [x] 14.10 清理对应路由注册，`go build ./...` 通过

## 15. 文档和最终验收

- [x] 15.1 更新 API 文档生成器，运行 `go run cmd/gendocs/main.go` 确认 9 个新接口出现在文档中
- [x] 15.2 使用 PostgreSQL MCP 验证三张表结构变更正确（tb_device.virtual_no 唯一索引、tb_iot_card.virtual_no 条件唯一索引、tb_package.virtual_ratio 字段 NOT NULL DEFAULT 1.0）
- [x] 15.3 使用 PostgreSQL MCP 验证 Package 数据回填正确（enable_virtual_data=true 的套餐 virtual_ratio=930.9，非 1.0）
- [x] 15.4 运行 `go build ./...` 全量检查无编译错误
- [x] 15.5 tasks.md 全部任务标记完成
