fix: 修正轮询配置多匹配逻辑,支持同卡匹配多个配置并按 priority 合并 interval
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m27s

核心变更:
- MatchConfig 改为 MatchConfigs,返回所有匹配配置
- MergedTaskIntervals 按 task type 合并各配置,选取最高优先级(非 nil 且最小 priority 值)
- hasAnyEnabledInterval 过滤所有 interval 均为 NULL 的配置
- calcInitialDelay 重构为纯函数,接收 interval 参数
- 移除 getEnabledTaskTypes 和 getIntervalByTaskType(被 MergedTaskIntervals 替代)
- scheduler.go 新增心跳 key + 顶层 panic recovery + Init 完成守卫
- initializer.go 批量失败日志升级为 Error,逐条检查 Pipeline 命令错误
- 数据迁移:禁用 id=29 的轮询配置(所有 interval 均为 NULL)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 14:27:47 +08:00
parent 5065d925ad
commit 5d9be1d7e4
15 changed files with 770 additions and 231 deletions

View File

@@ -157,7 +157,8 @@ func (p *PollingInitializer) run(ctx context.Context) {
}
if initErr := p.initBatch(ctx, cards); initErr != nil {
p.logger.Warn("批量初始化失败", zap.Error(initErr))
p.logger.Error("批量初始化失败,该批次卡未入轮询队列",
zap.Uint("batch_start_id", lastID), zap.Error(initErr))
}
lastID = cards[len(cards)-1].ID
@@ -206,16 +207,24 @@ func (p *PollingInitializer) initBatch(ctx context.Context, cards []*model.IotCa
if cmdCount == 0 {
return
}
if _, execErr := pipe.Exec(ctx); execErr != nil {
p.logger.Warn("Pipeline flush 失败,继续下一批", zap.Error(execErr))
cmds, execErr := pipe.Exec(ctx)
if execErr != nil {
p.logger.Error("Pipeline flush 失败,部分卡可能未入队", zap.Error(execErr))
}
// 逐条检查,记录具体失败命令(不中断批次)
for _, cmd := range cmds {
if cmdErr := cmd.Err(); cmdErr != nil && cmdErr != redis.Nil {
p.logger.Warn("Pipeline 单条命令失败",
zap.String("cmd", cmd.Name()), zap.Error(cmdErr))
}
}
pipe = p.redis.Pipeline()
cmdCount = 0
}
for _, card := range cards {
cfg := p.configMgr.MatchConfig(card)
if cfg == nil {
intervals := p.configMgr.MergedTaskIntervals(card)
if len(intervals) == 0 {
skippedCards++
continue
}
@@ -224,37 +233,10 @@ func (p *PollingInitializer) initBatch(ctx context.Context, cards []*model.IotCa
shardID := int(card.ID) % p.queueMgr.shardCount
cardIDStr := fmt.Sprintf("%d", card.ID)
if cfg.RealnameCheckInterval != nil && *cfg.RealnameCheckInterval > 0 {
nextCheck := calculateNextCheckTime(card.LastRealNameCheckAt, *cfg.RealnameCheckInterval, now)
pipe.ZAdd(ctx, constants.RedisPollingShardQueueKey(shardID, constants.TaskTypePollingRealname), redis.Z{
Score: float64(nextCheck.Unix()), Member: cardIDStr,
})
cmdCount++
}
if cfg.CarddataCheckInterval != nil && *cfg.CarddataCheckInterval > 0 {
nextCheck := calculateNextCheckTime(card.LastDataCheckAt, *cfg.CarddataCheckInterval, now)
pipe.ZAdd(ctx, constants.RedisPollingShardQueueKey(shardID, constants.TaskTypePollingCarddata), redis.Z{
Score: float64(nextCheck.Unix()), Member: cardIDStr,
})
cmdCount++
}
if cfg.PackageCheckInterval != nil && *cfg.PackageCheckInterval > 0 {
nextCheck := calculateNextCheckTime(card.LastDataCheckAt, *cfg.PackageCheckInterval, now)
pipe.ZAdd(ctx, constants.RedisPollingShardQueueKey(shardID, constants.TaskTypePollingPackage), redis.Z{
Score: float64(nextCheck.Unix()), Member: cardIDStr,
})
cmdCount++
}
if cfg.ProtectCheckInterval != nil && *cfg.ProtectCheckInterval > 0 {
nextCheck := calculateNextCheckTime(card.LastProtectCheckAt, *cfg.ProtectCheckInterval, now)
pipe.ZAdd(ctx, constants.RedisPollingShardQueueKey(shardID, constants.TaskTypePollingProtect), redis.Z{
Score: float64(nextCheck.Unix()), Member: cardIDStr,
})
cmdCount++
}
if cfg.CardStatusCheckInterval != nil && *cfg.CardStatusCheckInterval > 0 {
nextCheck := calculateNextCheckTime(card.LastCardStatusCheckAt, *cfg.CardStatusCheckInterval, now)
pipe.ZAdd(ctx, constants.RedisPollingShardQueueKey(shardID, constants.TaskTypePollingCardStatus), redis.Z{
for taskType, info := range intervals {
lastCheckAt := lastCheckAtByTaskType(card, taskType)
nextCheck := calculateNextCheckTime(lastCheckAt, info.Interval, now)
pipe.ZAdd(ctx, constants.RedisPollingShardQueueKey(shardID, taskType), redis.Z{
Score: float64(nextCheck.Unix()), Member: cardIDStr,
})
cmdCount++
@@ -294,6 +276,22 @@ func (p *PollingInitializer) initBatch(ctx context.Context, cards []*model.IotCa
return nil
}
// lastCheckAtByTaskType 根据 task type 返回对应的上次检查时间字段
func lastCheckAtByTaskType(card *model.IotCard, taskType string) *time.Time {
switch taskType {
case constants.TaskTypePollingRealname:
return card.LastRealNameCheckAt
case constants.TaskTypePollingCarddata, constants.TaskTypePollingPackage:
return card.LastDataCheckAt
case constants.TaskTypePollingProtect:
return card.LastProtectCheckAt
case constants.TaskTypePollingCardStatus:
return card.LastCardStatusCheckAt
default:
return nil
}
}
// calculateNextCheckTime 计算下次检查时间
func calculateNextCheckTime(lastCheckAt *time.Time, intervalSeconds int, now time.Time) time.Time {
if lastCheckAt == nil {