## Context

轮询系统由 `Scheduler`（调度器）+ 4 种 `PollingHandler` 任务类型构成，以卡（`IotCard`）为单位驱动停复机、流量扣减、套餐激活等核心业务。本次修复 3 个审计发现的覆盖盲点：

| 编号 | 问题 | 严重度 |
|------|------|--------|
| G1 | `polling:protect` 队列初始化缺失，保护期一致性检查完全失效 | P1 |
| G2 | `checkAndTriggerSuspension` device 分支为空，设备套餐耗尽不停机 | P1 |
| G3 | 套餐过期后不主动停机，依赖 carddata 轮询兜底（最长 60s 窗口） | P2 |
| G4 | `getCardCondition` 的 `"suspended"` 分支是死代码 | P2 |

## Goals / Non-Goals

**Goals:**
- 使 `polling:protect` 任务能够按配置间隔定时执行
- 设备级套餐（`device_id` 绑定）流量耗尽时自动停机
- 套餐过期时立即触发停机，消除 60s 延迟窗口
- 清理 `getCardCondition` 死代码

**Non-Goals:**
- 设备维度的主动轮询（查询设备实时状态）
- 轮询性能优化（并发数、批量大小调整）
- 新增轮询任务类型

## Decisions

### 决策 1：`protect_check_interval` 放在 `PollingConfig` 还是全局配置

**选择**：放在 `PollingConfig`，与现有 `realname_check_interval`/`carddata_check_interval`/`package_check_interval` 保持一致。

**理由**：不同运营商/卡类型的保护期检查频率可能不同（例如高价值卡检查更频繁）；统一用 PollingConfig 管理，不引入新的配置层级。

**放弃方案**：全局配置（`viper`）——灵活性不足，无法按卡类型差异化。

---

### 决策 2：设备套餐耗尽停机的触发方式

**选择**：在 `UsageService.checkAndTriggerSuspension` 补全 device 分支，查询设备绑定的所有卡并逐一调用 `StopResumeService.CheckAndStopCard`（异步 goroutine）。

**理由**：
- 和现有 iot_card 分支对称，逻辑清晰
- 通过 `DeviceSimBindingStore.ListByDeviceID` 获取绑定卡列表，不引入跨包依赖
- 异步执行避免长事务

**放弃方案**：在 `checkStopResume` 里增加 device 判断——`checkStopResume` 以卡为单位工作，不感知 device 维度的套餐，逻辑不自然。

---

### 决策 3：套餐过期主动停机的触发位置

**选择**：在 `PackageActivationHandler.processExpiredPackage` 的 `updateCarrierSuspendedStatus` 后，若载体仍无生效套餐，异步调用 `StopResumeCallback.CheckAndStopCard`。

**理由**：`processExpiredPackage` 已经在判断"是否还有生效套餐"，复用这个判断结果，减少重复查询。直接在此处触发停机是最短的调用路径。

**放弃方案**：新增一个独立的停机触发任务——多一个异步任务链路，延迟不减反增。

---

### 决策 4：`IotCard.LastProtectCheckAt` 字段类型

**选择**：`*time.Time`（nullable），与现有 `LastRealNameCheckAt`/`LastDataCheckAt` 保持一致，表示"未检查过"。

## Risks / Trade-offs

- **重复停机风险**：套餐过期时 `processExpiredPackage` 会触发停机，60s 后 `checkStopResume` 也会尝试停机。`CheckAndStopCard` 内部已有"卡已停机则跳过"的幂等保护，不会重复调网关。→ **已有幂等保护，风险可控**

- **DB 迁移风险**：新增两列均为 nullable，无需迁移存量数据，迁移后旧代码路径不受影响。→ **向下兼容，风险低**

- **`StopResumeCallback` 接口循环依赖**：`PackageActivationHandler`（polling 包）需要调用 `StopResumeService`（iot_card 包）。当前已通过接口注入解耦（`StopResumeCallback`），直接复用该模式。→ **无新依赖**

## Migration Plan

1. 执行数据库迁移：`tb_polling_config` 添加 `protect_check_interval INT`；`tb_iot_card` 添加 `last_protect_check_at TIMESTAMP`
2. 部署新版本（调度器重启后自动重新初始化轮询队列，protect 队列开始填充）
3. 在 `tb_polling_config` 为需要保护期检查的配置行设置 `protect_check_interval`（建议 600s）
4. 回滚策略：回滚代码后 protect 队列不再填充，已入队任务执行后不再重新入队，不影响其他三种任务

## Open Questions

- `protect_check_interval` 的默认推荐值：当前代码注释写"与流量检查同频（默认 10 分钟）"，是否沿用 600s？→ 建议 600s，与原设计一致
