59eb34976a
iotlcm-admin-svr:管理后台服务。 - Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件) - 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、 MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位; 私网 IP → 文档地址段;库名 xiujie_admin → iotlcm - 真实手机号/邮箱 → 合法 11 位编造号 / example.com - 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、 私有依赖 go get 行、明文 registry 登录口令 - 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名 - Dockerfile 改用公共基础镜像 - 拼错的 RADEME.md → README.md,README 重写 - 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用 - 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
380 lines
10 KiB
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
|
|
}
|