## 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'`
