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>
4.8 KiB
4.8 KiB
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.go的PollingConfigstruct 添加ProtectCheckInterval *int字段,gorm tag 包含column:protect_check_interval,json tag 为protect_check_interval,中文注释 - 2.2 在
internal/model/iot_card.go的IotCardstruct 添加LastProtectCheckAt *time.Time字段,gorm tag 包含column:last_protect_check_at,json tag 为last_protect_check_at,中文注释 - 2.3 运行
lsp_diagnostics确认两个 model 文件无错误
3. 轮询调度器:protect 队列初始化
- 3.1 在
internal/polling/scheduler.go的initCardsBatch方法中,在 package 队列初始化块之后添加 protect 队列初始化:读取cfg.ProtectCheckInterval,调用calculateNextCheckTime(card.LastProtectCheckAt, *cfg.ProtectCheckInterval),写入RedisPollingQueueProtectKey() - 3.2 在
initCardPolling方法中同步添加 protect 队列初始化逻辑(与initCardsBatch对称) - 3.3 在
requeueCard方法中添加constants.TaskTypePollingProtectcase:读取config.ProtectCheckInterval,入队RedisPollingQueueProtectKey(),更新last_protect_check_at - 3.4 在
HandleProtectConsistencyCheck任务处理完成后调用requeueCard(cardID, TaskTypePollingProtect)使卡重新入队 - 3.5 运行
lsp_diagnostics确认scheduler.go和polling_handler.go无错误
4. 为 protect 配置设置检查间隔
- 4.1 通过数据库执行 SQL,为现有已启用的
tb_polling_config配置行(status = 1的行)设置protect_check_interval = 600(10 分钟) - 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.go的UsageServicestruct 中添加deviceSimBindingStore *postgres.DeviceSimBindingStore字段 - 5.2 更新
NewUsageService构造函数,接收deviceSimBindingStore参数并赋值 - 5.3 更新
bootstrap中UsageService的初始化调用,传入deviceSimBindingStore - 5.4 在
checkAndTriggerSuspension的if 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.go的PackageActivationHandlerstruct 中添加stopResumeCallback StopResumeCallback字段(复用usage_service.go已有的同名接口) - 6.2 更新
NewPackageActivationHandler构造函数,接收stopResumeCallback参数并赋值 - 6.3 更新
Scheduler中PackageActivationHandler的初始化,传入stopResumeCallback - 6.4 在
processExpiredPackage中,updateCarrierSuspendedStatus调用之后,若载体类型为iot_card且stopResumeCallback != nil,异步调用CheckAndStopCard(cardID);若载体类型为device,查询绑定卡并逐一异步触发 - 6.5 运行
lsp_diagnostics确认package_activation_handler.go无错误
7. 死代码清理
- 7.1 在
internal/polling/scheduler.go的getCardCondition函数中,删除永不可达的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=2(depleted)后,绑定卡的
network_status变为 0,stop_reason = 'traffic_exhausted' - 8.4 通过 PostgreSQL MCP 查询一张已过期套餐(
expires_at <= NOW)且无后续套餐的卡,确认其network_status = 0,stop_reason = 'traffic_exhausted'