Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-02-fix-polling-coverage-gaps/tasks.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

4.8 KiB
Raw Blame History

1. 数据库迁移

  • 1.1 创建迁移文件,在 tb_polling_config 添加 protect_check_interval INT NULL COMMENT '保护期一致性检查间隔NULL=不参与'
  • 1.2 在同一迁移文件中,在 tb_iot_card 添加 last_protect_check_at TIMESTAMP NULL COMMENT '上次保护期一致性检查时间'
  • 1.3 执行迁移并验证:通过 PostgreSQL MCP 确认两列已成功添加,类型和注释正确

2. Model 更新

  • 2.1 在 internal/model/polling.goPollingConfig struct 添加 ProtectCheckInterval *int 字段gorm tag 包含 column:protect_check_intervaljson tag 为 protect_check_interval,中文注释
  • 2.2 在 internal/model/iot_card.goIotCard struct 添加 LastProtectCheckAt *time.Time 字段gorm tag 包含 column:last_protect_check_atjson tag 为 last_protect_check_at,中文注释
  • 2.3 运行 lsp_diagnostics 确认两个 model 文件无错误

3. 轮询调度器protect 队列初始化

  • 3.1 在 internal/polling/scheduler.goinitCardsBatch 方法中,在 package 队列初始化块之后添加 protect 队列初始化:读取 cfg.ProtectCheckInterval,调用 calculateNextCheckTime(card.LastProtectCheckAt, *cfg.ProtectCheckInterval),写入 RedisPollingQueueProtectKey()
  • 3.2 在 initCardPolling 方法中同步添加 protect 队列初始化逻辑(与 initCardsBatch 对称)
  • 3.3 在 requeueCard 方法中添加 constants.TaskTypePollingProtect case读取 config.ProtectCheckInterval,入队 RedisPollingQueueProtectKey(),更新 last_protect_check_at
  • 3.4 在 HandleProtectConsistencyCheck 任务处理完成后调用 requeueCard(cardID, TaskTypePollingProtect) 使卡重新入队
  • 3.5 运行 lsp_diagnostics 确认 scheduler.gopolling_handler.go 无错误

4. 为 protect 配置设置检查间隔

  • 4.1 通过数据库执行 SQL为现有已启用的 tb_polling_config 配置行(status = 1 的行)设置 protect_check_interval = 60010 分钟)
  • 4.2 通过 PostgreSQL MCP 验证配置已更新,查询 SELECT id, config_name, protect_check_interval FROM tb_polling_config WHERE status = 1

5. 设备套餐耗尽触发停机

  • 5.1 在 internal/service/package/usage_service.goUsageService struct 中添加 deviceSimBindingStore *postgres.DeviceSimBindingStore 字段
  • 5.2 更新 NewUsageService 构造函数,接收 deviceSimBindingStore 参数并赋值
  • 5.3 更新 bootstrapUsageService 的初始化调用,传入 deviceSimBindingStore
  • 5.4 在 checkAndTriggerSuspensionif activeCount == 0 块内,补全 carrierType == "device" 分支:查询设备绑定卡列表(ListByDeviceID),对每张卡异步启动 goroutine 调用 s.stopResumeCallback.CheckAndStopCard;若 stopResumeCallback == nil 则仅记录 Warn 日志
  • 5.5 运行 lsp_diagnostics 确认 usage_service.go 无错误

6. 套餐过期主动触发停机

  • 6.1 在 internal/polling/package_activation_handler.goPackageActivationHandler struct 中添加 stopResumeCallback StopResumeCallback 字段(复用 usage_service.go 已有的同名接口)
  • 6.2 更新 NewPackageActivationHandler 构造函数,接收 stopResumeCallback 参数并赋值
  • 6.3 更新 SchedulerPackageActivationHandler 的初始化,传入 stopResumeCallback
  • 6.4 在 processExpiredPackage 中,updateCarrierSuspendedStatus 调用之后,若载体类型为 iot_cardstopResumeCallback != nil,异步调用 CheckAndStopCard(cardID);若载体类型为 device,查询绑定卡并逐一异步触发
  • 6.5 运行 lsp_diagnostics 确认 package_activation_handler.go 无错误

7. 死代码清理

  • 7.1 在 internal/polling/scheduler.gogetCardCondition 函数中,删除永不可达的 if card.NetworkStatus == 0 { return "suspended" }return "" 两行(已实名+离线永远走 "real_name" 分支)
  • 7.2 运行 lsp_diagnostics 确认 scheduler.go 无错误

8. 端到端验证

  • 8.1 重启服务,通过日志确认调度器初始化时 protect 队列有卡入队(关键日志:完成一批卡初始化
  • 8.2 通过 PostgreSQL MCP 查询一张卡的 last_protect_check_at,确认 protect 任务执行后字段有更新:SELECT id, iccid, last_protect_check_at FROM tb_iot_card WHERE last_protect_check_at IS NOT NULL LIMIT 5
  • 8.3 通过 PostgreSQL MCP 构造一条设备级套餐耗尽场景:确认设备套餐 status=2depleted绑定卡的 network_status 变为 0stop_reason = 'traffic_exhausted'
  • 8.4 通过 PostgreSQL MCP 查询一张已过期套餐(expires_at <= NOW)且无后续套餐的卡,确认其 network_status = 0stop_reason = 'traffic_exhausted'