## 1. 数据库迁移 - [x] 1.1 创建迁移文件,在 `tb_polling_config` 添加 `protect_check_interval INT NULL COMMENT '保护期一致性检查间隔(秒),NULL=不参与'` - [x] 1.2 在同一迁移文件中,在 `tb_iot_card` 添加 `last_protect_check_at TIMESTAMP NULL COMMENT '上次保护期一致性检查时间'` - [x] 1.3 执行迁移并验证:通过 PostgreSQL MCP 确认两列已成功添加,类型和注释正确 ## 2. Model 更新 - [x] 2.1 在 `internal/model/polling.go` 的 `PollingConfig` struct 添加 `ProtectCheckInterval *int` 字段,gorm tag 包含 `column:protect_check_interval`,json tag 为 `protect_check_interval`,中文注释 - [x] 2.2 在 `internal/model/iot_card.go` 的 `IotCard` struct 添加 `LastProtectCheckAt *time.Time` 字段,gorm tag 包含 `column:last_protect_check_at`,json tag 为 `last_protect_check_at`,中文注释 - [x] 2.3 运行 `lsp_diagnostics` 确认两个 model 文件无错误 ## 3. 轮询调度器:protect 队列初始化 - [x] 3.1 在 `internal/polling/scheduler.go` 的 `initCardsBatch` 方法中,在 package 队列初始化块之后添加 protect 队列初始化:读取 `cfg.ProtectCheckInterval`,调用 `calculateNextCheckTime(card.LastProtectCheckAt, *cfg.ProtectCheckInterval)`,写入 `RedisPollingQueueProtectKey()` - [x] 3.2 在 `initCardPolling` 方法中同步添加 protect 队列初始化逻辑(与 `initCardsBatch` 对称) - [x] 3.3 在 `requeueCard` 方法中添加 `constants.TaskTypePollingProtect` case:读取 `config.ProtectCheckInterval`,入队 `RedisPollingQueueProtectKey()`,更新 `last_protect_check_at` - [x] 3.4 在 `HandleProtectConsistencyCheck` 任务处理完成后调用 `requeueCard(cardID, TaskTypePollingProtect)` 使卡重新入队 - [x] 3.5 运行 `lsp_diagnostics` 确认 `scheduler.go` 和 `polling_handler.go` 无错误 ## 4. 为 protect 配置设置检查间隔 - [x] 4.1 通过数据库执行 SQL,为现有已启用的 `tb_polling_config` 配置行(`status = 1` 的行)设置 `protect_check_interval = 600`(10 分钟) - [x] 4.2 通过 PostgreSQL MCP 验证配置已更新,查询 `SELECT id, config_name, protect_check_interval FROM tb_polling_config WHERE status = 1` ## 5. 设备套餐耗尽触发停机 - [x] 5.1 在 `internal/service/package/usage_service.go` 的 `UsageService` struct 中添加 `deviceSimBindingStore *postgres.DeviceSimBindingStore` 字段 - [x] 5.2 更新 `NewUsageService` 构造函数,接收 `deviceSimBindingStore` 参数并赋值 - [x] 5.3 更新 `bootstrap` 中 `UsageService` 的初始化调用,传入 `deviceSimBindingStore` - [x] 5.4 在 `checkAndTriggerSuspension` 的 `if activeCount == 0` 块内,补全 `carrierType == "device"` 分支:查询设备绑定卡列表(`ListByDeviceID`),对每张卡异步启动 goroutine 调用 `s.stopResumeCallback.CheckAndStopCard`;若 `stopResumeCallback == nil` 则仅记录 Warn 日志 - [x] 5.5 运行 `lsp_diagnostics` 确认 `usage_service.go` 无错误 ## 6. 套餐过期主动触发停机 - [x] 6.1 在 `internal/polling/package_activation_handler.go` 的 `PackageActivationHandler` struct 中添加 `stopResumeCallback StopResumeCallback` 字段(复用 `usage_service.go` 已有的同名接口) - [x] 6.2 更新 `NewPackageActivationHandler` 构造函数,接收 `stopResumeCallback` 参数并赋值 - [x] 6.3 更新 `Scheduler` 中 `PackageActivationHandler` 的初始化,传入 `stopResumeCallback` - [x] 6.4 在 `processExpiredPackage` 中,`updateCarrierSuspendedStatus` 调用之后,若载体类型为 `iot_card` 且 `stopResumeCallback != nil`,异步调用 `CheckAndStopCard(cardID)`;若载体类型为 `device`,查询绑定卡并逐一异步触发 - [x] 6.5 运行 `lsp_diagnostics` 确认 `package_activation_handler.go` 无错误 ## 7. 死代码清理 - [x] 7.1 在 `internal/polling/scheduler.go` 的 `getCardCondition` 函数中,删除永不可达的 `if card.NetworkStatus == 0 { return "suspended" }` 和 `return ""` 两行(已实名+离线永远走 `"real_name"` 分支) - [x] 7.2 运行 `lsp_diagnostics` 确认 `scheduler.go` 无错误 ## 8. 端到端验证 - [x] 8.1 重启服务,通过日志确认调度器初始化时 protect 队列有卡入队(关键日志:`完成一批卡初始化`) - [x] 8.2 通过 PostgreSQL MCP 查询一张卡的 `last_protect_check_at`,确认 protect 任务执行后字段有更新:`SELECT id, iccid, last_protect_check_at FROM tb_iot_card WHERE last_protect_check_at IS NOT NULL LIMIT 5` - [x] 8.3 通过 PostgreSQL MCP 构造一条设备级套餐耗尽场景:确认设备套餐 status=2(depleted)后,绑定卡的 `network_status` 变为 0,`stop_reason = 'traffic_exhausted'` - [x] 8.4 通过 PostgreSQL MCP 查询一张已过期套餐(`expires_at <= NOW`)且无后续套餐的卡,确认其 `network_status = 0`,`stop_reason = 'traffic_exhausted'`