## Context 轮询系统由 `Scheduler`(调度器)+ 4 种 `PollingHandler` 任务类型构成,以卡(`IotCard`)为单位驱动停复机、流量扣减、套餐激活等核心业务。本次修复 3 个审计发现的覆盖盲点: | 编号 | 问题 | 严重度 | |------|------|--------| | G1 | `polling:protect` 队列初始化缺失,保护期一致性检查完全失效 | P1 | | G2 | `checkAndTriggerSuspension` device 分支为空,设备套餐耗尽不停机 | P1 | | G3 | 套餐过期后不主动停机,依赖 carddata 轮询兜底(最长 60s 窗口) | P2 | | G4 | `getCardCondition` 的 `"suspended"` 分支是死代码 | P2 | ## Goals / Non-Goals **Goals:** - 使 `polling:protect` 任务能够按配置间隔定时执行 - 设备级套餐(`device_id` 绑定)流量耗尽时自动停机 - 套餐过期时立即触发停机,消除 60s 延迟窗口 - 清理 `getCardCondition` 死代码 **Non-Goals:** - 设备维度的主动轮询(查询设备实时状态) - 轮询性能优化(并发数、批量大小调整) - 新增轮询任务类型 ## Decisions ### 决策 1:`protect_check_interval` 放在 `PollingConfig` 还是全局配置 **选择**:放在 `PollingConfig`,与现有 `realname_check_interval`/`carddata_check_interval`/`package_check_interval` 保持一致。 **理由**:不同运营商/卡类型的保护期检查频率可能不同(例如高价值卡检查更频繁);统一用 PollingConfig 管理,不引入新的配置层级。 **放弃方案**:全局配置(`viper`)——灵活性不足,无法按卡类型差异化。 --- ### 决策 2:设备套餐耗尽停机的触发方式 **选择**:在 `UsageService.checkAndTriggerSuspension` 补全 device 分支,查询设备绑定的所有卡并逐一调用 `StopResumeService.CheckAndStopCard`(异步 goroutine)。 **理由**: - 和现有 iot_card 分支对称,逻辑清晰 - 通过 `DeviceSimBindingStore.ListByDeviceID` 获取绑定卡列表,不引入跨包依赖 - 异步执行避免长事务 **放弃方案**:在 `checkStopResume` 里增加 device 判断——`checkStopResume` 以卡为单位工作,不感知 device 维度的套餐,逻辑不自然。 --- ### 决策 3:套餐过期主动停机的触发位置 **选择**:在 `PackageActivationHandler.processExpiredPackage` 的 `updateCarrierSuspendedStatus` 后,若载体仍无生效套餐,异步调用 `StopResumeCallback.CheckAndStopCard`。 **理由**:`processExpiredPackage` 已经在判断"是否还有生效套餐",复用这个判断结果,减少重复查询。直接在此处触发停机是最短的调用路径。 **放弃方案**:新增一个独立的停机触发任务——多一个异步任务链路,延迟不减反增。 --- ### 决策 4:`IotCard.LastProtectCheckAt` 字段类型 **选择**:`*time.Time`(nullable),与现有 `LastRealNameCheckAt`/`LastDataCheckAt` 保持一致,表示"未检查过"。 ## Risks / Trade-offs - **重复停机风险**:套餐过期时 `processExpiredPackage` 会触发停机,60s 后 `checkStopResume` 也会尝试停机。`CheckAndStopCard` 内部已有"卡已停机则跳过"的幂等保护,不会重复调网关。→ **已有幂等保护,风险可控** - **DB 迁移风险**:新增两列均为 nullable,无需迁移存量数据,迁移后旧代码路径不受影响。→ **向下兼容,风险低** - **`StopResumeCallback` 接口循环依赖**:`PackageActivationHandler`(polling 包)需要调用 `StopResumeService`(iot_card 包)。当前已通过接口注入解耦(`StopResumeCallback`),直接复用该模式。→ **无新依赖** ## Migration Plan 1. 执行数据库迁移:`tb_polling_config` 添加 `protect_check_interval INT`;`tb_iot_card` 添加 `last_protect_check_at TIMESTAMP` 2. 部署新版本(调度器重启后自动重新初始化轮询队列,protect 队列开始填充) 3. 在 `tb_polling_config` 为需要保护期检查的配置行设置 `protect_check_interval`(建议 600s) 4. 回滚策略:回滚代码后 protect 队列不再填充,已入队任务执行后不再重新入队,不影响其他三种任务 ## Open Questions - `protect_check_interval` 的默认推荐值:当前代码注释写"与流量检查同频(默认 10 分钟)",是否沿用 600s?→ 建议 600s,与原设计一致