> **Delta Spec**:本文件仅描述对 `openspec/specs/polling-task-handlers/spec.md` 的变更部分。未提及的需求保持不变。 --- ### 变更:polling_realname_handler——实名逆转防抖 **原行为**:检测到实名状态 `1→0` 逆转时,立即更新 DB 并调用 `EvaluateAndAct` 触发停机。 **新行为**:检测到逆转时,通过 Redis 计数器累计,仅在**连续 3 次**检测到逆转后才触发 `EvaluateAndAct`;检测到 `true` 时重置计数器。DB 写入行为(`real_name_status` 同步)保持不变——只延迟停机决策,不延迟状态记录。 **Redis Key**:`polling:card:realname:reversal:{card_id}`(`RedisPollingRealnameReversalCountKey`) **TTL**:10 分钟(超时自动归零) **阈值常量**:`PollingRealnameReversalThreshold = 3` #### Scenario: 单次逆转不触发停机 - **GIVEN** card_id=4,`real_name_status=1`(DB),Gateway 返回 `false` - **WHEN** `polling_realname_handler` 处理该卡 - **THEN** DB 更新 `real_name_status=0`;Redis 计数器 `reversal:4` 变为 1(TTL=10min);**不调用 EvaluateAndAct**;卡保持在线 #### Scenario: 连续 3 次逆转才触发停机 - **GIVEN** card_id=4,前 2 次轮询 Gateway 均返回 false(计数器为 2) - **WHEN** 第 3 次轮询 Gateway 仍返回 false - **THEN** Redis 计数器变为 3(达到阈值);删除计数器;调用 `EvaluateAndAct(freshCard)`;触发停机,`stop_reason='not_realname'` #### Scenario: 中间出现 true 则重置计数 - **GIVEN** card_id=4,已记录 2 次逆转(计数器=2) - **WHEN** 下次轮询 Gateway 返回 true(`real_name_status` 重置为 1) - **THEN** 删除 Redis 计数器(归零);若状态从 0→1 则触发复机评估(逻辑不变) #### Scenario: 10 分钟未触发则计数自动归零 - **GIVEN** card_id=4 记录了 1 次逆转(计数器=1),之后 10 分钟内 Gateway 均返回 true - **WHEN** 10 分钟 TTL 到期 - **THEN** Redis key 自动过期,计数归零;后续逆转需重新从 1 开始累计 --- ### 变更:polling_cardstatus_handler——网关侧停机补写 stop_reason **原行为**:检测到 Gateway 报告卡为"停机"(在线→停机状态变化)时,仅更新 `network_status=0`,不设置 `stop_reason`。 **新行为**:在写入 `network_status=0` 的同时,若当前 `stop_reason` 为空,补写 `stop_reason='carrier_stopped'`。 > **说明**:只在状态变化时补写,若 `stop_reason` 已有值(如已被本系统停机)则不覆盖。 #### Scenario: 网关侧停机事件补写 carrier_stopped - **GIVEN** 卡在线(`network_status=1`,`stop_reason=''`),Gateway 本次返回"停机" - **WHEN** `polling_cardstatus_handler` 处理,检测到 `statusChanged=true`,`newNetworkStatus=0` - **THEN** DB 更新 `network_status=0, stop_reason='carrier_stopped'`;`isPollingStopReason('carrier_stopped')=false`;`EvaluateAndAct` 不触发自动复机 #### Scenario: 不覆盖已有 stop_reason - **GIVEN** 卡已停机(`network_status=0`,`stop_reason='not_realname'`),Gateway 本次仍返回"停机" - **WHEN** `polling_cardstatus_handler` 处理,`statusChanged=false`(状态未变) - **THEN** 不写入任何字段;`stop_reason` 保持 `not_realname` 不变 #### Scenario: carrier_stopped 的卡通过 EvaluateAndAct 调用时正确跳过复机 - **GIVEN** 卡停机,`stop_reason='carrier_stopped'`,套餐有效,已实名 - **WHEN** 其他轮询任务调用 `EvaluateAndAct(card)` - **THEN** `isPollingStopReason('carrier_stopped')=false`;直接返回 nil,不执行复机