Files
junhong_cmp_fiber/internal/polling/pre_reset.go
邹进 98017ac053
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m37s
feat: 分层轮询、重置日前加密与激活前流量结算
- 轮询配置新增套餐状态、资产状态、重置日前窗口与窗口内流量间隔(迁移 248),间隔三态:正数按间隔、0 不检查、空继承
- 配置按 priority, id 排序合并;在库与已销售无套餐未匹配时告警不入队
- 新增优先级整体重排、匹配预览、立即重算到期时间(异步只提前)、分层统计与重置日前加密统计接口
- 已切换日快照计量的卡流量按观测时间归属,激活前用量只记已结算
- 订单购买与实名激活补齐资产级激活锁,激活前对已切换卡结算一次流量,失败只告警不阻塞,结算不触发通道阈值判定
2026-10-24 17:06:38 +09:00

131 lines
4.7 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
}
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
}