feat: 新增数据库迁移,重命名 device_no 为 virtual_no,新增 iot_card.virtual_no 和 package.virtual_ratio 字段
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m3s

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-03-14 18:27:28 +08:00
parent b5147d1acb
commit b9c3875c08
77 changed files with 5832 additions and 2393 deletions

View File

@@ -0,0 +1,107 @@
## 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 查 PackageUsagecard→iot_card_iddevice→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 全部任务标记完成