Files
junhong_cmp_fiber/openspec/specs/polling-protect-consistency/spec.md
huang 8980e6d999
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
fix: 补全三处轮询覆盖缺口
问题一:protect 轮询任务从未被调度器初始化入队
PollingConfig 新增 protect_check_interval 字段(NULL=不参与),
调度器 initCardsBatch/initCardPolling/requeueCard 补全 protect 队列
初始化逻辑,IotCard 新增 last_protect_check_at 记录上次检查时间。
迁移文件:000102_add_polling_protect_fields

问题二:设备套餐流量耗尽时绑定卡未被停机
UsageService.checkAndTriggerSuspension 新增 carrier_type=device 分支,
通过 DeviceSimBindingStore.ListByDeviceID 查询绑定卡,对每张卡异步
触发 CheckAndStopCard,同步注入 Bootstrap 依赖。

问题三:主套餐过期后不立即停机,依赖下次轮询兜底
PackageActivationHandler.processExpiredPackage 在 updateCarrierSuspended
Status 后,若载体无后续生效套餐,立即异步调用 CheckAndStopCard(iot_card
类型直接触发,device 类型遍历绑定卡),消除停机延迟窗口。

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-02 14:36:24 +08:00

110 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 任务不对该卡执行