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

5.7 KiB
Raw Blame History

polling-protect-consistency Specification

Purpose

新增第四种轮询任务类型(保护期一致性检查),用于确保设备保护期内绑定卡的网络状态与保护期方向保持一致,防止状态漂移。

Requirements

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

系统 SHALL 新增第四种轮询任务类型(保护期一致性检查),作为独立任务处理器,不修改现有三种任务(实名检查/流量检查/套餐检查)的内部逻辑。

任务类型标识: protect(与现有 realnamecarddatapackage 并列)

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(与现有 realnamecarddatapackage 并列)

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

触发频率(变更): 由 PollingConfig.ProtectCheckInterval(新增字段,单位秒)控制,NULL0 表示该配置不参与 protect 检查,建议默认 600s与流量检查任务同频固定 10 分钟)

初始化:调度器启动时,initCardsBatchinitCardPolling 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:protectprotect 任务不对该卡执行