usage_alert.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package model
  2. import (
  3. "time"
  4. )
  5. // GroupUsageAlertItem 用量告警项
  6. type GroupUsageAlertItem struct {
  7. GroupID string
  8. ThreeDayAvgAmount float64 // 前三天的平均用量
  9. TodayAmount float64
  10. Ratio float64
  11. }
  12. // calculateSpikeThreshold 根据前三天平均用量计算动态告警倍率
  13. // 用量越大,倍率越低,避免误报;用量越小,倍率越高,捕获异常
  14. func calculateSpikeThreshold(avgAmount float64) float64 {
  15. switch {
  16. case avgAmount < 100:
  17. return 5.0
  18. case avgAmount < 300:
  19. return 4.0
  20. case avgAmount < 1000:
  21. return 3.0
  22. case avgAmount < 2000:
  23. return 2.5
  24. case avgAmount < 5000:
  25. return 2.0
  26. default:
  27. return 1.5
  28. }
  29. }
  30. // GetGroupUsageAlert 获取用量突升异常的用户
  31. // 新的检测逻辑:
  32. // 1. 基准阈值:当天用量必须 >= threshold(如 100)才开始检测
  33. // 2. 动态倍率:根据前三天平均用量分段计算告警倍率
  34. // - 前三天平均用量 < 100:倍率 5.0(小用户突增 5 倍很异常)
  35. // - 前三天平均用量 [100, 300):倍率 4.0
  36. // - 前三天平均用量 [300, 1000):倍率 3.0
  37. // - 前三天平均用量 [1000, 2000):倍率 2.5
  38. // - 前三天平均用量 [2000, 5000):倍率 2.0
  39. // - 前三天平均用量 >= 5000:倍率 1.5(大用户增长 1.5 倍就值得关注)
  40. //
  41. // 3. 前三天平均用量必须 >= minAvgThreshold(如 3)才进行检测
  42. // 4. 不在白名单中
  43. func GetGroupUsageAlert(
  44. threshold, minAvgThreshold float64,
  45. whitelist []string,
  46. ) ([]GroupUsageAlertItem, error) {
  47. now := time.Now()
  48. // 计算当天的时间范围(0点到当前时间)
  49. todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
  50. todayEnd := now
  51. // 计算前三天的时间范围(从3天前的0点到昨天的23:59:59)
  52. threeDaysAgoStart := todayStart.Add(-72 * time.Hour) // 3天前的0点
  53. yesterdayEnd := todayStart.Add(-time.Second) // 昨天的23:59:59
  54. // 查询当天用量达到阈值的用户(第一步筛选)
  55. type TodayUsage struct {
  56. GroupID string
  57. UsedAmount float64
  58. }
  59. var todayUsages []TodayUsage
  60. err := LogDB.
  61. Model(&GroupSummary{}).
  62. Select("group_id, SUM(used_amount) as used_amount").
  63. Where("hour_timestamp BETWEEN ? AND ?", todayStart.Unix(), todayEnd.Unix()).
  64. Group("group_id").
  65. Having("SUM(used_amount) >= ?", threshold).
  66. Find(&todayUsages).Error
  67. if err != nil {
  68. return nil, err
  69. }
  70. if len(todayUsages) == 0 {
  71. return nil, nil
  72. }
  73. // 提取 group_id 列表
  74. groupIDs := make([]string, len(todayUsages))
  75. todayUsageMap := make(map[string]float64)
  76. for i, usage := range todayUsages {
  77. groupIDs[i] = usage.GroupID
  78. todayUsageMap[usage.GroupID] = usage.UsedAmount
  79. }
  80. // 查询这些用户的前三天用量
  81. type ThreeDayUsage struct {
  82. GroupID string
  83. UsedAmount float64
  84. }
  85. var threeDayUsages []ThreeDayUsage
  86. err = LogDB.
  87. Model(&GroupSummary{}).
  88. Select("group_id, SUM(used_amount) as used_amount").
  89. Where("group_id IN ?", groupIDs).
  90. Where("hour_timestamp BETWEEN ? AND ?", threeDaysAgoStart.Unix(), yesterdayEnd.Unix()).
  91. Group("group_id").
  92. Find(&threeDayUsages).Error
  93. if err != nil {
  94. return nil, err
  95. }
  96. // 构建前三天平均用量映射
  97. threeDayAvgUsageMap := make(map[string]float64)
  98. for _, usage := range threeDayUsages {
  99. // 计算平均值:总用量除以3
  100. threeDayAvgUsageMap[usage.GroupID] = usage.UsedAmount / 3.0
  101. }
  102. // 构建白名单映射,用于快速查找
  103. whitelistMap := make(map[string]bool)
  104. for _, groupID := range whitelist {
  105. whitelistMap[groupID] = true
  106. }
  107. // 筛选出符合条件的用户
  108. var alerts []GroupUsageAlertItem
  109. for groupID, todayAmount := range todayUsageMap {
  110. // 跳过白名单中的用户
  111. if whitelistMap[groupID] {
  112. continue
  113. }
  114. // 获取前三天平均用量(如果没有前三天的数据,默认为 0)
  115. threeDayAvgAmount := threeDayAvgUsageMap[groupID]
  116. // 过滤掉前三天平均用量低于阈值的用户
  117. if threeDayAvgAmount <= minAvgThreshold || threeDayAvgAmount == 0 {
  118. continue
  119. }
  120. // 计算实际比率
  121. ratio := todayAmount / threeDayAvgAmount
  122. // 根据前三天平均用量计算动态告警倍率
  123. requiredRatio := calculateSpikeThreshold(threeDayAvgAmount)
  124. // 检查是否满足告警条件
  125. if ratio >= requiredRatio {
  126. alerts = append(alerts, GroupUsageAlertItem{
  127. GroupID: groupID,
  128. ThreeDayAvgAmount: threeDayAvgAmount,
  129. TodayAmount: todayAmount,
  130. Ratio: ratio,
  131. })
  132. }
  133. }
  134. return alerts, nil
  135. }