package simwarning import ( "fmt" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "math" "time" ) func evaluateConfig(cfg warnConfig, card entity.TSimCardInfo, runTime time.Time) (*warningHit, string) { cfg.WarnType = normalizeWarnType(cfg.ItemType, cfg.WarnUnit) switch cfg.WarnType { case warnTypeCardTraffic: return evaluateTraffic(cfg, card, runTime) default: return nil, "unsupported warn type" } } func evaluateTraffic(cfg warnConfig, card entity.TSimCardInfo, runTime time.Time) (*warningHit, string) { cfg.WarnType = normalizeWarnType(warnItemTypeCard, cfg.WarnUnit) if cfg.WarnType != warnTypeCardTraffic { return nil, "unsupported traffic warn type" } if cfg.WarnUnit != warnUnitMb && cfg.WarnUnit != warnUnitPercent { return nil, "unsupported traffic warn unit" } if reason := validCommonCard(card); reason != "" { return nil, reason } if card.PackageTotalFlowMb <= 0 { return nil, "package total flow is empty" } usedFlowMb := card.UsedFlowMb if usedFlowMb < 0 { usedFlowMb = 0 } totalFlowMb := card.PackageTotalFlowMb remainFlowMb := totalFlowMb - usedFlowMb if remainFlowMb < 0 { remainFlowMb = 0 } usedPercent := usedFlowMb / totalFlowMb * 100 var ( stage int threshold float64 actual float64 kind string ) if cfg.WarnUnit == warnUnitPercent { first := normalizePercentThreshold(cfg.FirstWarn) second := normalizePercentThreshold(cfg.SecondWarn) third := normalizePercentThreshold(cfg.ThirdWarn) stage, threshold = matchGreaterStage(usedPercent, first, second, third) actual = usedPercent kind = warnKindTrafficPercent } else { stage, threshold = matchGreaterStage(usedFlowMb, cfg.FirstWarn, cfg.SecondWarn, cfg.ThirdWarn) actual = usedFlowMb kind = warnKindTrafficMb } if stage == 0 { return nil, "traffic threshold not reached" } level := warnLevelByStage(stage) if cfg.WarnUnit == warnUnitPercent && usedPercent >= 100 { level = warnLevelUrgent } remark := trafficRemark(cfg.WarnUnit, stage, threshold, usedFlowMb, usedPercent, totalFlowMb, remainFlowMb) hit := baseHit(cfg, card, kind) hit.WarnStage = stage hit.WarnLevel = level hit.ThresholdValue = threshold hit.ActualValue = round(actual, 3) hit.TotalFlowMb = round(totalFlowMb, 3) hit.RemainFlowMb = round(remainFlowMb, 3) hit.Remark = remark setTrafficDedupeWindow(&hit, runTime) return &hit, "" } func evaluatePoolTrafficPercent(cfg warnConfig, pool entity.TSimFlowPool, runTime time.Time) (*warningHit, string) { cfg.WarnType = normalizeWarnType(warnItemTypeFlowPool, cfg.WarnUnit) if cfg.WarnType != warnTypeFlowPoolPercent { return nil, "unsupported flow pool warn type" } if cfg.WarnUnit != warnUnitPercent { return nil, "flow pool only supports percent warning" } if pool.PoolStatus != 0 { return nil, "flow pool is not normal" } if pool.TotalFlowMb <= 0 { return nil, "flow pool total flow is empty" } usedFlowMb := pool.UsedFlowMb if usedFlowMb < 0 { usedFlowMb = 0 } totalFlowMb := pool.TotalFlowMb remainFlowMb := totalFlowMb - usedFlowMb if remainFlowMb < 0 { remainFlowMb = 0 } usedPercent := usedFlowMb / totalFlowMb * 100 first := normalizePercentThreshold(cfg.FirstWarn) second := normalizePercentThreshold(cfg.SecondWarn) third := normalizePercentThreshold(cfg.ThirdWarn) stage, threshold := matchGreaterStage(usedPercent, first, second, third) if stage == 0 { return nil, "flow pool traffic threshold not reached" } level := warnLevelByStage(stage) if usedPercent >= 100 { level = warnLevelUrgent } remark := fmt.Sprintf( "流量池流量已达到第%d档阈值,阈值:%.2f%%,实际使用率:%.2f%%,实际已用:%.3f MB,剩余:%.3f MB,总量:%.3f MB", stage, threshold, usedPercent, usedFlowMb, remainFlowMb, totalFlowMb, ) if usedPercent >= 100 { remark += ",已超出流量池总量" } hit := basePoolHit(cfg, pool, warnKindPoolTrafficPercent) hit.WarnStage = stage hit.WarnLevel = level hit.ThresholdValue = threshold hit.ActualValue = round(usedPercent, 3) hit.TotalFlowMb = round(totalFlowMb, 3) hit.RemainFlowMb = round(remainFlowMb, 3) hit.Remark = remark setTrafficDedupeWindow(&hit, runTime) return &hit, "" } func evaluateSystemExpireDay(cfg warnConfig, card entity.TSimCardInfo, runTime time.Time) (*warningHit, string) { cfg.WarnType = normalizeWarnType(warnItemTypeCard, cfg.WarnUnit) if cfg.WarnType != warnTypeExpireDay { return nil, "unsupported expire day warn type" } if cfg.WarnUnit != warnUnitDay { return nil, "unsupported expire day warn unit" } if reason := validCommonCard(card); reason != "" { return nil, reason } if card.FlowPkgEndAt == nil || card.FlowPkgEndAt.IsZero() { return nil, "package end time is empty" } endTime := card.FlowPkgEndAt.Time remainDays := diffDateDays(runTime, endTime) if remainDays < 0 { return nil, "package expired" } stage, threshold := matchLessStage(float64(remainDays), cfg.FirstWarn, cfg.SecondWarn, cfg.ThirdWarn) if stage == 0 { return nil, "expire day threshold not reached" } hit := baseHit(cfg, card, warnKindExpireDay) hit.WarnStage = stage hit.WarnLevel = warnLevelByStage(stage) hit.ThresholdValue = threshold hit.ActualValue = float64(remainDays) hit.TotalFlowMb = 0 hit.RemainFlowMb = 0 hit.Remark = fmt.Sprintf("套餐将在 %d 天后到期", remainDays) setExpireDayDedupeWindow(&hit, runTime) return &hit, "" } func validCommonCard(card entity.TSimCardInfo) string { if card.PackageStatus != consts.PackageStatusEffective { return "package is not effective" } if card.IsFlowPool != 0 { return "flow pool card is not supported" } if card.CurrentPackageCode == "" { return "current package code is empty" } return "" } func baseHit(cfg warnConfig, card entity.TSimCardInfo, kind string) warningHit { hit := warningHit{ Config: cfg, Card: card, Kind: kind, } if card.FlowPkgBeginAt != nil && !card.FlowPkgBeginAt.IsZero() { hit.PeriodBegin = card.FlowPkgBeginAt.Time } if card.FlowPkgEndAt != nil && !card.FlowPkgEndAt.IsZero() { hit.PeriodEnd = card.FlowPkgEndAt.Time } if hit.PeriodBegin.IsZero() && !hit.PeriodEnd.IsZero() { hit.PeriodBegin = warnPeriodFallbackBegin(hit.PeriodEnd.Location()) } return hit } func basePoolHit(cfg warnConfig, pool entity.TSimFlowPool, kind string) warningHit { return warningHit{ Config: cfg, Pool: pool, Kind: kind, } } func warnLevelByStage(stage int) int { switch stage { case warnStageFirst: return warnLevelNotice case warnStageSecond: return warnLevelWarning case warnStageThird: return warnLevelDanger default: return warnLevelNotice } } func trafficRemark(warnUnit, stage int, threshold, usedFlowMb, usedPercent, totalFlowMb, remainFlowMb float64) string { if warnUnit == warnUnitPercent { remark := fmt.Sprintf( "单卡流量已达到第%d档阈值,阈值:%.2f%%,实际使用率:%.2f%%,实际已用:%.3f MB,剩余:%.3f MB,总量:%.3f MB", stage, threshold, usedPercent, usedFlowMb, remainFlowMb, totalFlowMb, ) if usedPercent >= 100 { return remark + ",已超出套餐流量" } return remark } return fmt.Sprintf( "单卡流量已达到第%d档阈值,阈值:%.3f MB,实际已用:%.3f MB,剩余:%.3f MB,总量:%.3f MB", stage, threshold, usedFlowMb, remainFlowMb, totalFlowMb, ) } func matchGreaterStage(actual, first, second, third float64) (int, float64) { if third > 0 && actual >= third { return warnStageThird, third } if second > 0 && actual >= second { return warnStageSecond, second } if first > 0 && actual >= first { return warnStageFirst, first } return 0, 0 } func matchLessStage(actual, first, second, third float64) (int, float64) { if third > 0 && actual <= third { return warnStageThird, third } if second > 0 && actual <= second { return warnStageSecond, second } if first > 0 && actual <= first { return warnStageFirst, first } return 0, 0 } func normalizePercentThreshold(value float64) float64 { if value > 0 && value <= 1 { return value * 100 } return value } func diffDateDays(runTime, endTime time.Time) int { location := runTime.Location() runDate := time.Date(runTime.In(location).Year(), runTime.In(location).Month(), runTime.In(location).Day(), 0, 0, 0, 0, location) endLocal := endTime.In(location) endDate := time.Date(endLocal.Year(), endLocal.Month(), endLocal.Day(), 0, 0, 0, 0, location) return int(endDate.Sub(runDate).Hours() / 24) } func round(value float64, places int) float64 { pow := math.Pow10(places) return math.Round(value*pow) / pow } func warnPeriodFallbackBegin(location *time.Location) time.Time { if location == nil { location = time.Local } return time.Date(1970, 1, 1, 0, 0, 0, 0, location) } func setTrafficDedupeWindow(hit *warningHit, runTime time.Time) { monthBegin, monthEnd := calendarMonthWindow(runTime) hit.WindowBegin = monthBegin hit.WindowEnd = monthEnd hit.DedupeExpireAt = monthEnd } func calendarMonthWindow(runTime time.Time) (time.Time, time.Time) { if runTime.IsZero() { runTime = time.Now() } location := runTime.Location() local := runTime.In(location) begin := time.Date(local.Year(), local.Month(), 1, 0, 0, 0, 0, location) return begin, begin.AddDate(0, 1, 0) } func setExpireDayDedupeWindow(hit *warningHit, runTime time.Time) { if hit.PeriodEnd.IsZero() { return } stageBegin := expireDayStageBegin(runTime, hit.PeriodEnd, hit.ThresholdValue) stageEnd := expireDayStageEnd(runTime, *hit) if !stageEnd.After(stageBegin) { stageEnd = stageBegin.Add(24 * time.Hour) } hit.WindowBegin = stageBegin hit.WindowEnd = stageEnd hit.DedupeExpireAt = stageEnd } func expireDayStageBegin(runTime, endTime time.Time, threshold float64) time.Time { location := runTime.Location() endLocal := endTime.In(location) endDate := time.Date(endLocal.Year(), endLocal.Month(), endLocal.Day(), 0, 0, 0, 0, location) return endDate.AddDate(0, 0, -int(threshold)) } func expireDayStageEnd(runTime time.Time, hit warningHit) time.Time { if next, ok := nextExpireThreshold(hit); ok { return expireDayStageBegin(runTime, hit.PeriodEnd, next) } location := runTime.Location() endLocal := hit.PeriodEnd.In(location) endDate := time.Date(endLocal.Year(), endLocal.Month(), endLocal.Day(), 0, 0, 0, 0, location) return endDate.AddDate(0, 0, 1) } func nextExpireThreshold(hit warningHit) (float64, bool) { switch hit.WarnStage { case warnStageFirst: if hit.Config.SecondWarn > 0 { return hit.Config.SecondWarn, true } case warnStageSecond: if hit.Config.ThirdWarn > 0 { return hit.Config.ThirdWarn, true } } return 0, false }