Files
iotlcm/admin-svr/server/internal/logic/simwarning/rules.go

380 lines
10 KiB
Go

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
}