# polling-protect-consistency Specification ## Purpose 新增第四种轮询任务类型(保护期一致性检查),用于确保设备保护期内绑定卡的网络状态与保护期方向保持一致,防止状态漂移。 ## Requirements ### Requirement: 保护期一致性检查轮询任务 系统 SHALL 新增第四种轮询任务类型(保护期一致性检查),作为独立任务处理器,不修改现有三种任务(实名检查/流量检查/套餐检查)的内部逻辑。 **任务类型标识**: `protect`(与现有 `realname`、`carddata`、`package` 并列) **Redis 队列 Key**: `RedisPollingQueueProtectKey()` → `"polling:queue:protect"` **触发频率**: 与流量检查任务同频(默认 10 分钟) **任务范围**: 仅检查"已绑定设备且设备当前有保护期"的卡,范围小,不会对未绑定设备的卡产生影响 **处理逻辑**: 1. 检查卡是否已实名(`real_name_status = 0` 则跳过,未实名卡不参与保护期逻辑) 2. 检查卡是否绑定设备(`is_standalone = true` 则跳过) 3. 读取设备保护期 Redis Key 4. 若设备有 **stop 保护期**,且卡当前网络状态为**开机**:强制调网关停机,更新卡 `network_status = 0` 5. 若设备有 **start 保护期**,且卡当前网络状态为**停机**:强制调网关复机,更新卡 `network_status = 1` 6. 状态已一致(开机 + stop 保护期已停 / 停机 + start 保护期已开):跳过 #### Scenario: stop 保护期内卡状态异常(开机) - **WHEN** 轮询任务检查一张已实名卡,发现绑定设备有 stop 保护期,但卡当前 network_status=1(开机) - **THEN** 任务强制调网关停机,更新卡 network_status=0,记录 Info 日志 #### Scenario: start 保护期内卡状态异常(停机) - **WHEN** 轮询任务检查一张已实名卡,发现绑定设备有 start 保护期,但卡当前 network_status=0(停机) - **THEN** 任务强制调网关复机,更新卡 network_status=1,记录 Info 日志 #### Scenario: 状态已一致,跳过 - **WHEN** 轮询任务检查一张卡,设备有 stop 保护期,卡已是停机状态 - **THEN** 任务跳过,不调网关,不更新 DB #### Scenario: 未实名卡跳过保护期逻辑 - **WHEN** 轮询任务遇到 real_name_status=0 的卡 - **THEN** 任务直接跳过,不检查保护期,不调网关 #### Scenario: 独立卡(未绑定设备)跳过 - **WHEN** 轮询任务遇到 is_standalone=true 的卡 - **THEN** 任务直接跳过,不查询设备保护期 --- ## 迭代更新(fix-polling-coverage-gaps) ### MODIFIED Requirement: 保护期一致性检查轮询任务 系统 SHALL 通过轮询调度器定时执行保护期一致性检查任务(`polling:protect`),检查绑定设备有保护期的已实名卡的网络状态,并在状态不一致时强制调网关修正。 **任务类型标识**: `protect`(与现有 `realname`、`carddata`、`package` 并列) **Redis 队列 Key**: `RedisPollingQueueProtectKey()` → `"polling:queue:protect"` **触发频率**(变更): 由 `PollingConfig.ProtectCheckInterval`(新增字段,单位秒)控制,`NULL` 或 `0` 表示该配置不参与 protect 检查,建议默认 600s(原:与流量检查任务同频固定 10 分钟) **初始化**:调度器启动时,`initCardsBatch` 和 `initCardPolling` SHALL 读取匹配配置的 `ProtectCheckInterval`,将卡加入 `polling:queue:protect` Sorted Set;任务完成后通过 `requeueCard` 按间隔重新入队,并更新 `last_protect_check_at` **数据模型变更**: - `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 任务不对该卡执行