## 1. 数据库迁移 - [ ] 1.1 创建迁移文件 `000120_add_polling_card_status.up.sql`:`tb_polling_config` 新增 `card_status_check_interval INT NULL`,`tb_iot_card` 新增 `last_card_status_check_at TIMESTAMPTZ NULL` - [ ] 1.2 创建对应回滚文件 `000120_add_polling_card_status.down.sql`:DROP COLUMN 两列 - [ ] 1.3 执行 `make migrate-up` 并验证:`make migrate-version` 确认 dirty=false,使用 PostgreSQL MCP 验证两列已创建 ## 2. 常量与配置 - [ ] 2.1 `pkg/constants/constants.go`:轮询任务类型常量块新增 `TaskTypePollingCardStatus = "polling:card_status"` - [ ] 2.2 `pkg/config/config.go`:新增 `PollingConfig struct { VerboseLog bool }`,在 `Config` 中加入 `Polling PollingConfig` 字段 - [ ] 2.3 `pkg/config/defaults/config.yaml`:在 polling 配置区新增 `verbose_log: false` - [ ] 2.4 验证:`LspDiagnostics` 检查 `pkg/constants/constants.go` 和 `pkg/config/config.go` 无报错 ## 3. 数据模型更新 - [ ] 3.1 `internal/model/polling.go`:`PollingConfig` 结构体新增 `CardStatusCheckInterval *int`(GORM column: `card_status_check_interval`) - [ ] 3.2 `internal/model/iot_card.go`:`IotCard` 结构体新增 `LastCardStatusCheckAt *time.Time`(GORM column: `last_card_status_check_at`) - [ ] 3.3 验证:`LspDiagnostics` 检查两个 model 文件无报错 ## 4. PollingBase 新增 verboseLog 支持 - [ ] 4.1 `internal/task/polling_base.go`:`PollingBase` 结构体新增 `verboseLog bool` 字段;`NewPollingBase` 函数签名新增 `verboseLog bool` 参数并赋值 - [ ] 4.2 `cmd/worker/main.go`:`task.NewPollingBase(...)` 调用新增 `cfg.Polling.VerboseLog` 参数 - [ ] 4.3 验证:`LspDiagnostics` 检查 `polling_base.go` 和 `main.go` 无报错 ## 5. 现有 Handler 添加 verbose 日志 - [ ] 5.1 `internal/task/polling_realname_handler.go`:在 `QueryRealnameStatus` 成功返回后,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、real_status、new_status、changed) - [ ] 5.2 `internal/task/polling_carddata_handler.go`:在 `QueryFlow` 成功返回后,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、gateway_flow_mb、increment_mb、is_cross_month) - [ ] 5.3 `internal/task/polling_package_handler.go`:在 `freshCard` 加载成功后、调用 `EvaluateAndAct` 之前,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、network_status、stop_reason) - [ ] 5.4 `internal/task/polling_protect_handler.go`:在 switch 块执行完毕、`UpdateFields(last_protect_check_at)` 之前,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、stop_protect、start_protect、network_status) - [ ] 5.5 验证:`LspDiagnostics` 检查上述 4 个 handler 文件无报错 ## 6. 卡状态轮询 Handler 实现 - [ ] 6.1 新建 `internal/task/polling_cardstatus_handler.go`:实现 `PollingCardStatusHandler`(字段:base、gateway、iotCardStore、stopResumeSvc),完整实现 `Handle` 方法 - [ ] 6.2 `Handle` 方法逻辑:获取并发信号量 → 获取卡信息(带缓存)→ 调用 `gateway.QueryCardStatus` → 映射状态("停机"→0,其他→1)→ verbose log 判断 → 比较状态是否变化 → 写 DB(`last_card_status_check_at` + 可选 `network_status`)→ 状态变化时更新缓存并调用 `EvaluateAndAct` → 更新统计 → 重入队 - [ ] 6.3 验证:`LspDiagnostics` 检查新文件无报错 ## 7. 轮询系统基础设施集成 - [ ] 7.1 `internal/task/polling_utils.go`:`getIntervalByTaskType` 新增 `case constants.TaskTypePollingCardStatus`,返回 `cfg.CardStatusCheckInterval` - [ ] 7.2 `internal/polling/queue_manager.go`:`allTaskTypes` slice 追加 `constants.TaskTypePollingCardStatus` - [ ] 7.3 `internal/polling/lifecycle_service.go`:`getEnabledTaskTypes` 新增 card_status 条件(`CardStatusCheckInterval != nil && *CardStatusCheckInterval > 0`);`calcInitialDelay` 新增对应 case - [ ] 7.4 `internal/polling/initializer.go`:在 `initBatch()` 的 if-block 链中新增一段,当 `cfg.CardStatusCheckInterval != nil && *cfg.CardStatusCheckInterval > 0` 时,以 `card.LastCardStatusCheckAt`(新增字段)为基准调用 `calculateNextCheckTime`,向 `TaskTypePollingCardStatus` 分片队列写入 ZAdd 条目;位置紧跟 ProtectCheckInterval 块之后,格式与其他 4 种类型一致 - [ ] 7.5 验证:`LspDiagnostics` 检查上述 4 个文件无报错 ## 8. Worker Handler 注册 - [ ] 8.1 `pkg/queue/handler.go`:在 `RegisterHandlers` 中创建 `cardStatusHandler := task.NewPollingCardStatusHandler(...)` 并注册 `h.mux.HandleFunc(constants.TaskTypePollingCardStatus, cardStatusHandler.Handle)` - [ ] 8.2 验证:`LspDiagnostics` 检查 `handler.go` 无报错;`go build ./cmd/worker/...` 成功编译 ## 9. 整体验证 - [ ] 9.1 执行 `go build ./...` 确认整个项目编译通过 - [ ] 9.2 使用 PostgreSQL MCP 验证迁移列已正确创建(类型、NULL 约束) - [ ] 9.3 检查 `make migrate-down && make migrate-up` 回滚和重新迁移均成功