package polling import ( "context" "strconv" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" ) // shanghaiLocation 返回业务时区(上海)。 func shanghaiLocation() *time.Location { location, err := time.LoadLocation("Asia/Shanghai") if err != nil { return time.FixedZone("CST", 8*3600) } return location } // nextResetAt 返回 now 之后最近一次流量重置时刻:重置日当天 0 点(上海时区)。 func nextResetAt(now time.Time, resetDay int) time.Time { if resetDay < 1 || resetDay > 28 { resetDay = 1 } local := now.In(shanghaiLocation()) candidate := time.Date(local.Year(), local.Month(), resetDay, 0, 0, 0, 0, shanghaiLocation()) if !candidate.After(local) { candidate = candidate.AddDate(0, 1, 0) } return candidate } // PreResetNextDue 计算重置日前加密后的流量检查下次到期时间。 // - 当前在窗口 [reset-window, reset) 内:下次 = now + 窗口间隔;越过重置时刻则改为 reset - 收尾提前量,已过则按正常间隔; // - 当前在窗口前、按正常间隔算出的下次越过窗口起点:截断到窗口起点,保证按时进入窗口; // - 其余按正常间隔。 func PreResetNextDue(now time.Time, normalInterval, window, windowInterval time.Duration, reset time.Time, tail time.Duration) time.Time { normal := now.Add(normalInterval) windowStart := reset.Add(-window) if !now.Before(windowStart) && now.Before(reset) { next := now.Add(windowInterval) if next.Before(reset) { return next } last := reset.Add(-tail) if last.After(now) { return last } return normal } if now.Before(windowStart) && normal.After(windowStart) { return windowStart } return normal } // NextCheckAt 返回卡某检查类型的下次到期时间:流量检查在来源配置开启重置日前加密时按窗口规则计算,其余为 now + 间隔。 func (m *PollingConfigManager) NextCheckAt(ctx context.Context, card *model.IotCard, taskType string, info TaskTypeInterval, now time.Time) time.Time { normal := now.Add(time.Duration(info.Interval) * time.Second) if taskType != constants.TaskTypePollingCarddata || card == nil || info.ConfigID == 0 { return normal } cfg := m.configByID(info.ConfigID) if cfg == nil || cfg.PreResetWindow == nil || cfg.PreResetCarddataInterval == nil { return normal } resetDay := 1 if m.tier != nil { resetDay = m.tier.ResetDay(ctx, card.CarrierID) } return PreResetNextDue(now, time.Duration(info.Interval)*time.Second, time.Duration(*cfg.PreResetWindow)*time.Second, time.Duration(*cfg.PreResetCarddataInterval)*time.Second, nextResetAt(now, resetDay), constants.PollingPreResetTailMinutes*time.Minute) } func (m *PollingConfigManager) configByID(id uint) *model.PollingConfig { m.mu.RLock() defer m.mu.RUnlock() for _, cfg := range m.configs { if cfg.ID == id { return cfg } } return nil } // RecordPreResetCheck 在流量查询成功后记录重置日前加密效果:处于该卡来源配置的加密窗口内时, // 把卡记入窗口已查集合,并记下本次查询距重置时刻的分钟数(同卡取最后一次)。统计失败不影响轮询。 func (m *PollingConfigManager) RecordPreResetCheck(ctx context.Context, card *model.IotCard, now time.Time) { if m == nil || m.redis == nil || card == nil { return } info, ok := m.MergedTaskIntervals(card)[constants.TaskTypePollingCarddata] if !ok { return } cfg := m.configByID(info.ConfigID) if cfg == nil || cfg.PreResetWindow == nil || cfg.PreResetCarddataInterval == nil { return } resetDay := 1 if m.tier != nil { resetDay = m.tier.ResetDay(ctx, card.CarrierID) } reset := nextResetAt(now, resetDay) if now.Before(reset.Add(-time.Duration(*cfg.PreResetWindow) * time.Second)) { return } // 只统计有生效套餐的卡,与统计接口"有生效套餐但窗口内未查到"的口径一致。 if facts := m.TierFactsOf(card); !facts.Known || facts.PackageState != constants.PollingPackageStateActive { return } date := reset.Format("20060102") member := strconv.FormatUint(uint64(card.ID), 10) minutes := int(reset.Sub(now).Minutes()) ttl := constants.PollingPreResetStatTTLSeconds * time.Second pipe := m.redis.Pipeline() pipe.PFAdd(ctx, constants.RedisPollingPreResetCheckedKey(card.CarrierID, date), member) pipe.Expire(ctx, constants.RedisPollingPreResetCheckedKey(card.CarrierID, date), ttl) pipe.HSet(ctx, constants.RedisPollingPreResetOffsetKey(card.CarrierID, date), member, minutes) pipe.Expire(ctx, constants.RedisPollingPreResetOffsetKey(card.CarrierID, date), ttl) _, _ = pipe.Exec(ctx) } // ResetMoments 返回某运营商账户最近一次已过去的重置时刻与下一次重置时刻。 func (m *PollingConfigManager) ResetMoments(ctx context.Context, carrierID uint, now time.Time) (time.Time, time.Time) { resetDay := 1 if m.tier != nil { resetDay = m.tier.ResetDay(ctx, carrierID) } next := nextResetAt(now, resetDay) return next.AddDate(0, -1, 0), next }