Files
junhong_cmp_fiber/internal/polling/pre_reset.go
邹进 ec5c1996a7
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
fix: 重算到期时间不抵消失败退避,重置日前统计只计有生效套餐的卡
- 上次检查时间只在查询成功时更新,按"上次检查 + 新间隔"会把失败或限流延后的卡拉到立即执行;
  过期或从未检查时候选改为"现在 + 新间隔",只提前排队超过一个新间隔的成员
- 重置日前加密统计只记录有生效套餐的卡,与"有生效套餐但窗口内未查到"口径一致
2026-10-24 17:28:18 +09:00

135 lines
4.9 KiB
Go

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
}