package polling import ( "context" "sync" "time" "go.uber.org/zap" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" ) // TierFacts 是分层轮询匹配所需、卡表之外的业务事实。 type TierFacts struct { PackageState string // 套餐状态,见 constants.PollingPackageState* Known bool // 是否成功解析;解析失败时要求套餐状态的配置不匹配 } type tierCacheEntry struct { facts TierFacts loadedAt time.Time } // TierResolver 计算卡的套餐状态(绑定设备的卡按设备、独立卡按卡,与停复机"是否有有效套餐"口径一致), // 并读取运营商账户的流量重置日。结果在进程内短时缓存,层级最迟在缓存过期后的下一次执行时更新。 type TierResolver struct { usage *postgres.PackageUsageStore bindings *postgres.DeviceSimBindingStore carriers *postgres.CarrierStore logger *zap.Logger mu sync.Mutex cache map[uint]tierCacheEntry resetDays map[uint]int resetsAt time.Time } // NewTierResolver 创建分层事实解析器。 func NewTierResolver(usage *postgres.PackageUsageStore, bindings *postgres.DeviceSimBindingStore, carriers *postgres.CarrierStore, logger *zap.Logger) *TierResolver { if logger == nil { logger = zap.NewNop() } return &TierResolver{usage: usage, bindings: bindings, carriers: carriers, logger: logger, cache: map[uint]tierCacheEntry{}, resetDays: map[uint]int{}} } // Resolve 返回卡的分层事实;缓存未过期时直接返回。 func (r *TierResolver) Resolve(ctx context.Context, card *model.IotCard) TierFacts { if r == nil || card == nil { return TierFacts{} } if facts, ok := r.cached(card.ID); ok { return facts } r.Prefill(ctx, []*model.IotCard{card}) facts, _ := r.cached(card.ID) return facts } // Prefill 批量解析一组卡的分层事实并写入缓存,供全量初始化等批处理避免逐卡查库。 func (r *TierResolver) Prefill(ctx context.Context, cards []*model.IotCard) { if r == nil || r.usage == nil || len(cards) == 0 { return } var standaloneIDs, boundCardIDs []uint for _, card := range cards { if card == nil { continue } if card.IsStandalone { standaloneIDs = append(standaloneIDs, card.ID) } else { boundCardIDs = append(boundCardIDs, card.ID) } } results := make(map[uint]TierFacts, len(cards)) if len(standaloneIDs) > 0 { states, err := r.packageStates(ctx, constants.AssetTypeIotCard, standaloneIDs) if err != nil { r.logger.Warn("批量查询卡套餐状态失败,相关卡本次不按套餐状态匹配", zap.Error(err)) } for _, id := range standaloneIDs { if err == nil { results[id] = TierFacts{PackageState: states[id], Known: true} } } } if len(boundCardIDs) > 0 && r.bindings != nil { bindCtx, cancel := context.WithTimeout(ctx, constants.PollingTierQueryTimeout) bindings, err := r.bindings.GetActiveBindingsByCardIDs(bindCtx, boundCardIDs) cancel() if err != nil { r.logger.Warn("批量查询卡绑定设备失败,相关卡本次不按套餐状态匹配", zap.Error(err)) } else { deviceOf := make(map[uint]uint, len(bindings)) deviceIDs := make([]uint, 0, len(bindings)) for _, binding := range bindings { deviceOf[binding.IotCardID] = binding.DeviceID deviceIDs = append(deviceIDs, binding.DeviceID) } deviceStates, devErr := r.packageStates(ctx, constants.AssetTypeDevice, deviceIDs) cardStates, cardErr := r.packageStates(ctx, constants.AssetTypeIotCard, boundCardIDs) for _, id := range boundCardIDs { // 有有效设备绑定按设备计算;绑定已失效的卡按卡自身计算(与 resolvePackageCarrier 口径一致)。 if deviceID, bound := deviceOf[id]; bound { if devErr == nil { results[id] = TierFacts{PackageState: deviceStates[deviceID], Known: true} } } else if cardErr == nil { results[id] = TierFacts{PackageState: cardStates[id], Known: true} } } } } // 只缓存查询成功的卡:查询失败的卡本次按未知处理,下次重新查询,不把"未知"留在缓存里。 now := time.Now() r.mu.Lock() for id, facts := range results { r.cache[id] = tierCacheEntry{facts: facts, loadedAt: now} } r.mu.Unlock() } // packageStates 以单独的超时查询一批载体的套餐状态,避免多次查询共用一个超时。 func (r *TierResolver) packageStates(ctx context.Context, carrierType string, ids []uint) (map[uint]string, error) { queryCtx, cancel := context.WithTimeout(ctx, constants.PollingTierQueryTimeout) defer cancel() return r.usage.PackageStatesByCarriers(queryCtx, carrierType, ids) } // Invalidate 清除某卡的分层事实缓存(套餐变化后可调用,未调用时最迟缓存过期后更新)。 func (r *TierResolver) Invalidate(cardID uint) { if r == nil { return } r.mu.Lock() delete(r.cache, cardID) r.mu.Unlock() } func (r *TierResolver) cached(cardID uint) (TierFacts, bool) { r.mu.Lock() defer r.mu.Unlock() entry, ok := r.cache[cardID] if !ok || time.Since(entry.loadedAt) > constants.PollingTierFactsCacheSeconds*time.Second { return TierFacts{}, false } return entry.facts, true } // ResetDay 返回运营商账户的流量重置日(1~28),缓存 10 分钟;读取失败按 1 号。 func (r *TierResolver) ResetDay(ctx context.Context, carrierID uint) int { if r == nil || r.carriers == nil { return 1 } r.mu.Lock() if time.Since(r.resetsAt) > 10*time.Minute { r.resetDays = map[uint]int{} r.resetsAt = time.Now() } day, ok := r.resetDays[carrierID] r.mu.Unlock() if ok { return day } day = r.carriers.GetDataResetDay(ctx, carrierID) if day < 1 || day > 28 { day = 1 } r.mu.Lock() r.resetDays[carrierID] = day r.mu.Unlock() return day }