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