All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m11s
问题一: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>
82 lines
4.3 KiB
Markdown
82 lines
4.3 KiB
Markdown
## 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,与原设计一致
|