package polling import ( "context" "encoding/json" "strings" "sync" "time" "go.uber.org/zap" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/polling/carrierqueue" "github.com/break/junhong_cmp_fiber/pkg/constants" ) // sceneCacheTTL 是请求检查场景配置的进程内缓存时长。 const sceneCacheTTL = 30 * time.Second // SceneConfig 是一个请求检查场景的生效配置。 type SceneConfig struct { Scene string CheckTypes []string ConfirmDelays []time.Duration Cooldown time.Duration } // CheckRequester 按场景配置为卡请求检查(业务事件与读入口):把对应检查类型设为立即到期放入账户优先队列, // 同卡多次请求合并、冷却期内不请求、按场景追加延迟确认。只在按运营商账户调度时生效。 type CheckRequester struct { scheduling *CarrierScheduling db *gorm.DB logger *zap.Logger mu sync.Mutex scenes map[string]SceneConfig loadedAt time.Time } // NewCheckRequester 创建请求检查器。 func NewCheckRequester(scheduling *CarrierScheduling, db *gorm.DB, logger *zap.Logger) *CheckRequester { if logger == nil { logger = zap.NewNop() } return &CheckRequester{scheduling: scheduling, db: db, logger: logger} } // Active 判断当前是否按运营商账户调度(请求检查生效)。 func (r *CheckRequester) Active(ctx context.Context) bool { return r != nil && r.scheduling.Active(ctx) } // RequestCards 为一组卡按场景请求检查;expected 为延迟确认的期望结果(online / offline,可空)。 // 失败只记日志,不影响业务。 func (r *CheckRequester) RequestCards(ctx context.Context, scene string, cardIDs []uint, expected string) { if r == nil || len(cardIDs) == 0 { return } config, ok := r.sceneConfig(ctx, scene) if !ok || len(config.CheckTypes) == 0 { return } carriers, err := r.cardCarriers(ctx, cardIDs) if err != nil { r.logger.Warn("请求检查:查询卡所属运营商账户失败", zap.String("scene", scene), zap.Error(err)) return } store := r.scheduling.Store() for _, cardID := range cardIDs { carrierID, found := carriers[cardID] if !found { continue } if _, err := store.RequestCheck(ctx, carrierqueue.CheckRequest{ CardID: cardID, CarrierID: carrierID, CheckTypes: config.CheckTypes, Cooldown: config.Cooldown, ConfirmDelays: config.ConfirmDelays, Expected: expected, }); err != nil { r.logger.Warn("请求检查写入账户队列失败", zap.Uint("card_id", cardID), zap.String("scene", scene), zap.Error(err)) } } } // RequestDevice 为设备下全部有效绑定卡按场景请求检查。 func (r *CheckRequester) RequestDevice(ctx context.Context, scene string, deviceID uint) { if r == nil || deviceID == 0 { return } var cardIDs []uint if err := r.db.WithContext(ctx).Model(&model.DeviceSimBinding{}). Where("device_id = ? AND bind_status = ?", deviceID, constants.BindStatusBound).Pluck("iot_card_id", &cardIDs).Error; err != nil { r.logger.Warn("请求检查:查询设备绑定卡失败", zap.Uint("device_id", deviceID), zap.Error(err)) return } r.RequestCards(ctx, scene, cardIDs, "") } func (r *CheckRequester) cardCarriers(ctx context.Context, cardIDs []uint) (map[uint]uint, error) { var cards []model.IotCard if err := r.db.WithContext(ctx).Select("id", "carrier_id").Where("id IN ?", cardIDs).Find(&cards).Error; err != nil { return nil, err } result := make(map[uint]uint, len(cards)) for _, card := range cards { result[card.ID] = card.CarrierID } return result, nil } // sceneConfig 读取场景配置(缓存);场景未预置时不请求。 func (r *CheckRequester) sceneConfig(ctx context.Context, scene string) (SceneConfig, bool) { r.mu.Lock() defer r.mu.Unlock() if r.scenes == nil || time.Since(r.loadedAt) >= sceneCacheTTL { var rows []model.PollingCheckScene if err := r.db.WithContext(ctx).Find(&rows).Error; err != nil { r.logger.Warn("读取请求检查场景失败,沿用上一次配置", zap.Error(err)) } else { scenes := make(map[string]SceneConfig, len(rows)) for _, row := range rows { scenes[row.Scene] = ParseSceneConfig(row) } r.scenes, r.loadedAt = scenes, time.Now() } } config, ok := r.scenes[scene] return config, ok } // ParseSceneConfig 解析场景配置行。 func ParseSceneConfig(row model.PollingCheckScene) SceneConfig { config := SceneConfig{Scene: row.Scene} var types []string _ = json.Unmarshal(row.CheckTypes, &types) for _, checkType := range types { if constants.CheckTypeCallsGateway(strings.TrimSpace(checkType)) { config.CheckTypes = append(config.CheckTypes, strings.TrimSpace(checkType)) } } var delays []int _ = json.Unmarshal(row.ConfirmDelays, &delays) for _, seconds := range delays { if seconds > 0 { config.ConfirmDelays = append(config.ConfirmDelays, time.Duration(seconds)*time.Second) } } if row.Cooldown != nil && *row.Cooldown > 0 { config.Cooldown = time.Duration(*row.Cooldown) * time.Second } return config } // InvalidateScenes 使场景缓存立即失效(后台修改后调用)。 func (r *CheckRequester) InvalidateScenes() { if r == nil { return } r.mu.Lock() r.loadedAt = time.Time{} r.mu.Unlock() }