## 1. 数据库迁移

- [x] 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`
- [x] 1.2 创建对应回滚文件 `000120_add_polling_card_status.down.sql`：DROP COLUMN 两列
- [x] 1.3 执行 `make migrate-up` 并验证：`make migrate-version` 确认 dirty=false，使用 PostgreSQL MCP 验证两列已创建

## 2. 常量与配置

- [x] 2.1 `pkg/constants/constants.go`：轮询任务类型常量块新增 `TaskTypePollingCardStatus = "polling:card_status"`
- [x] 2.2 `pkg/config/config.go`：新增 `PollingConfig struct { VerboseLog bool }`，在 `Config` 中加入 `Polling PollingConfig` 字段
- [x] 2.3 `pkg/config/defaults/config.yaml`：在 polling 配置区新增 `verbose_log: false`
- [x] 2.4 验证：`LspDiagnostics` 检查 `pkg/constants/constants.go` 和 `pkg/config/config.go` 无报错

## 3. 数据模型更新

- [x] 3.1 `internal/model/polling.go`：`PollingConfig` 结构体新增 `CardStatusCheckInterval *int`（GORM column: `card_status_check_interval`）
- [x] 3.2 `internal/model/iot_card.go`：`IotCard` 结构体新增 `LastCardStatusCheckAt *time.Time`（GORM column: `last_card_status_check_at`）
- [x] 3.3 验证：`LspDiagnostics` 检查两个 model 文件无报错

## 4. PollingBase 新增 verboseLog 支持

- [x] 4.1 `internal/task/polling_base.go`：`PollingBase` 结构体新增 `verboseLog bool` 字段；`NewPollingBase` 函数签名新增 `verboseLog bool` 参数并赋值
- [x] 4.2 `cmd/worker/main.go`：`task.NewPollingBase(...)` 调用新增 `cfg.Polling.VerboseLog` 参数
- [x] 4.3 验证：`LspDiagnostics` 检查 `polling_base.go` 和 `main.go` 无报错

## 5. 现有 Handler 添加 verbose 日志

- [x] 5.1 `internal/task/polling_realname_handler.go`：在 `QueryRealnameStatus` 成功返回后，判断 `h.base.verboseLog` 为 true 时输出 Info 日志（card_id、iccid、real_status、new_status、changed）
- [x] 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）
- [x] 5.3 `internal/task/polling_package_handler.go`：在 `freshCard` 加载成功后、调用 `EvaluateAndAct` 之前，判断 `h.base.verboseLog` 为 true 时输出 Info 日志（card_id、iccid、network_status、stop_reason）
- [x] 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）
- [x] 5.5 验证：`LspDiagnostics` 检查上述 4 个 handler 文件无报错

## 6. 卡状态轮询 Handler 实现

- [x] 6.1 新建 `internal/task/polling_cardstatus_handler.go`：实现 `PollingCardStatusHandler`（字段：base、gateway、iotCardStore、stopResumeSvc），完整实现 `Handle` 方法
- [x] 6.2 `Handle` 方法逻辑：获取并发信号量 → 获取卡信息（带缓存）→ 调用 `gateway.QueryCardStatus` → 映射状态（"停机"→0，其他→1）→ verbose log 判断 → 比较状态是否变化 → 写 DB（`last_card_status_check_at` + 可选 `network_status`）→ 状态变化时更新缓存并调用 `EvaluateAndAct` → 更新统计 → 重入队
- [x] 6.3 验证：`LspDiagnostics` 检查新文件无报错

## 7. 轮询系统基础设施集成

- [x] 7.1 `internal/task/polling_utils.go`：`getIntervalByTaskType` 新增 `case constants.TaskTypePollingCardStatus`，返回 `cfg.CardStatusCheckInterval`
- [x] 7.2 `internal/polling/queue_manager.go`：`allTaskTypes` slice 追加 `constants.TaskTypePollingCardStatus`
- [x] 7.3 `internal/polling/lifecycle_service.go`：`getEnabledTaskTypes` 新增 card_status 条件（`CardStatusCheckInterval != nil && *CardStatusCheckInterval > 0`）；`calcInitialDelay` 新增对应 case
- [x] 7.4 `internal/polling/initializer.go`：在 `initBatch()` 的 if-block 链中新增一段，当 `cfg.CardStatusCheckInterval != nil && *cfg.CardStatusCheckInterval > 0` 时，以 `card.LastCardStatusCheckAt`（新增字段）为基准调用 `calculateNextCheckTime`，向 `TaskTypePollingCardStatus` 分片队列写入 ZAdd 条目；位置紧跟 ProtectCheckInterval 块之后，格式与其他 4 种类型一致
- [x] 7.5 验证：`LspDiagnostics` 检查上述 4 个文件无报错

## 8. Worker Handler 注册

- [x] 8.1 `pkg/queue/handler.go`：在 `RegisterHandlers` 中创建 `cardStatusHandler := task.NewPollingCardStatusHandler(...)` 并注册 `h.mux.HandleFunc(constants.TaskTypePollingCardStatus, cardStatusHandler.Handle)`
- [x] 8.2 验证：`LspDiagnostics` 检查 `handler.go` 无报错；`go build ./cmd/worker/...` 成功编译

## 9. 整体验证

- [x] 9.1 执行 `go build ./...` 确认整个项目编译通过
- [x] 9.2 使用 PostgreSQL MCP 验证迁移列已正确创建（类型、NULL 约束）
- [x] 9.3 检查 `make migrate-down && make migrate-up` 回滚和重新迁移均成功
