## MODIFIED Requirements

### Requirement: 保护期一致性检查轮询任务

系统 SHALL 通过轮询调度器定时执行保护期一致性检查任务（`polling:protect`），检查绑定设备有保护期的已实名卡的网络状态，并在状态不一致时强制调网关修正。

**任务类型标识**: `protect`（与现有 `realname`、`carddata`、`package` 并列）

**Redis 队列 Key**: `RedisPollingQueueProtectKey()` → `"polling:queue:protect"`

**触发频率**: 由 `PollingConfig.ProtectCheckInterval`（新增字段，单位秒）控制，`NULL` 或 `0` 表示该配置不参与 protect 检查，建议默认 600s

**初始化**：调度器启动时，`initCardsBatch` 和 `initCardPolling` SHALL 读取匹配配置的 `ProtectCheckInterval`，将卡加入 `polling:queue:protect` Sorted Set；任务完成后通过 `requeueCard` 按间隔重新入队

**数据模型变更**：
- `tb_polling_config` 新增 `protect_check_interval INT NULL` 字段
- `tb_iot_card` 新增 `last_protect_check_at TIMESTAMP NULL` 字段（记录上次检查时间，用于计算下次入队 score）

**处理逻辑**（与原 spec 一致，不变）:
1. 卡未实名（`real_name_status = 0`）→ 跳过
2. 卡未绑定设备（`is_standalone = true`）→ 跳过
3. 设备有 stop 保护期且卡在线（`network_status = 1`）→ 调网关停机，更新 `network_status = 0`
4. 设备有 start 保护期且卡停机（`network_status = 0`）→ 调网关复机，更新 `network_status = 1`
5. 状态已一致 → 跳过

#### Scenario: stop 保护期内卡状态异常（开机）

- **WHEN** protect 轮询任务检查一张已实名、已绑定设备的卡，发现设备有 stop 保护期，且卡当前 `network_status = 1`（开机）
- **THEN** 任务调网关停机，更新卡 `network_status = 0`，更新 `last_protect_check_at`，记录 Info 日志

#### Scenario: start 保护期内卡状态异常（停机）

- **WHEN** protect 轮询任务检查一张已实名、已绑定设备的卡，发现设备有 start 保护期，且卡当前 `network_status = 0`（停机）
- **THEN** 任务调网关复机，更新卡 `network_status = 1`，更新 `last_protect_check_at`，记录 Info 日志

#### Scenario: 状态已一致，跳过

- **WHEN** protect 轮询任务检查一张卡，设备有 stop 保护期，卡已是停机状态（`network_status = 0`）
- **THEN** 任务跳过，不调网关，不更新 DB，更新 `last_protect_check_at`

#### Scenario: 未实名卡跳过保护期逻辑

- **WHEN** protect 轮询任务遇到 `real_name_status = 0` 的卡
- **THEN** 任务直接跳过，不检查保护期，不调网关

#### Scenario: 独立卡（未绑定设备）跳过

- **WHEN** protect 轮询任务遇到 `is_standalone = true` 的卡
- **THEN** 任务直接跳过，不查询设备保护期

#### Scenario: PollingConfig 未配置 ProtectCheckInterval，卡不入 protect 队列

- **WHEN** 调度器初始化时，卡匹配到的 PollingConfig 的 `protect_check_interval` 为 NULL 或 0
- **THEN** 该卡不被加入 `polling:queue:protect`，protect 任务不对该卡执行
