## 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 全部任务标记完成