feat: implement V2 alert center and scoped alert governance

This commit is contained in:
Yvan
2026-11-05 20:08:29 +08:00
parent f8e26c5baf
commit 999c9a6315
57 changed files with 2711 additions and 216 deletions
+116 -25
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"sort"
"strconv"
"time"
@@ -16,8 +17,17 @@ type alertRecordRepo struct{ data Provider }
func NewAlertRecordRepo(data Provider) business.AlertRecordRepo { return &alertRecordRepo{data: data} }
func (r *alertRecordRepo) Platforms(ctx context.Context, scope business.AlertRecordScope) ([]string, error) {
rows := []string{}
if scope.Owner.Empty {
return rows, nil
}
err := alertBindingQuery(r.data.DB().WithContext(ctx), scope.Viewer).Distinct().Order("b.source_db").Pluck("b.source_db", &rows).Error
return rows, storageError(err)
}
func toBizAlertRecord(po alertRecordPO) *business.AlertRecord {
return &business.AlertRecord{ID: po.ID, RuleID: po.RuleID, PlatformCode: po.PlatformCode, PlatformName: po.PlatformName, RuleName: po.RuleName, RuleType: po.RuleType, MonitorMetric: po.MonitorMetric, TargetPromoteID: po.TargetPromoteID, TargetPromoteName: po.TargetPromoteName, BusinessID: po.BusinessID, BusinessName: po.BusinessName, AlertContent: po.AlertContent, Status: po.Status, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt}
return &business.AlertRecord{ID: po.ID, TotalPlayers: po.TotalPlayers, TotalPay: po.TotalPay, WarningColor: po.WarningColor, RuleID: po.RuleID, PlatformCode: po.PlatformCode, PlatformName: po.PlatformName, RuleName: po.RuleName, RuleType: po.RuleType, MonitorMetric: po.MonitorMetric, TargetPromoteID: po.TargetPromoteID, TargetPromoteName: po.TargetPromoteName, BusinessID: po.BusinessID, BusinessName: po.BusinessName, AlertContent: po.AlertContent, Status: po.Status, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt}
}
func alertBindingQuery(db *gorm.DB, scope business.UserScope) *gorm.DB {
@@ -50,11 +60,11 @@ func prepareAlertScope(db *gorm.DB, scope business.AlertRecordScope) (empty, ava
if s.Empty {
return true, true, nil
}
var count int64
if err := alertBindingQuery(db, s).Count(&count).Error; err != nil {
var bindings []struct{ UserID uint }
if err := alertBindingQuery(db, s).Select("b.user_id").Limit(1).Scan(&bindings).Error; err != nil {
return false, false, business.ErrStorage
}
if count == 0 {
if len(bindings) == 0 {
return true, true, nil
}
}
@@ -97,6 +107,14 @@ func (r *alertRecordRepo) List(ctx context.Context, scope business.AlertRecordSc
return page, nil
}
}
lossAvailable, err := businessTablesAvailable(db, "tab_promote", "tab_spend")
if err != nil {
return nil, err
}
if filter.Lost != nil && !lossAvailable {
return nil, business.ErrAlertChannelsUnavailable
}
lossWindow := business.AlertLossAt(time.Now())
base := func() *gorm.DB {
q := alertScopedQuery(db, scope)
if len(filter.PlatformCodes) > 0 {
@@ -110,24 +128,49 @@ func (r *alertRecordRepo) List(ctx context.Context, scope business.AlertRecordSc
}
return q
}
q := base()
for _, f := range []struct{ column, value string }{{"target_promote_name", filter.TargetPromoteName}, {"rule_name", filter.RuleName}, {"business_name", filter.BusinessName}} {
if f.value != "" {
q = q.Where("r."+f.column+" LIKE ?", "%"+f.value+"%")
filtered := func() *gorm.DB {
q := base()
for _, f := range []struct{ column, value string }{{"target_promote_name", filter.TargetPromoteName}, {"rule_name", filter.RuleName}, {"business_name", filter.BusinessName}} {
if f.value != "" {
q = q.Where("r."+f.column+" LIKE ?", "%"+f.value+"%")
}
}
if filter.RuleType != "" {
q = q.Where("r.rule_type = ?", filter.RuleType)
}
if filter.MonitorMetric != nil {
q = q.Where("r.monitor_metric = ?", *filter.MonitorMetric)
}
if filter.AlertContent != "" {
q = q.Where("r.alert_content LIKE ?", "%"+filter.AlertContent+"%")
}
if filter.Lost != nil {
q = q.Where("EXISTS (?)", db.Table("tab_promote known").Select("1").Where("known.source_db=r.platform_code AND known.id=r.target_promote_id"))
q = q.Where("EXISTS (?) = ?", alertLostQuery(db, "r.platform_code", "r.target_promote_id", lossWindow), *filter.Lost)
}
return q
}
if filter.RuleType != "" {
q = q.Where("r.rule_type = ?", filter.RuleType)
}
if filter.MonitorMetric != nil {
q = q.Where("r.monitor_metric = ?", *filter.MonitorMetric)
}
q := filtered()
if filter.Status != nil {
q = q.Where("r.status = ?", *filter.Status)
}
if err := q.Count(&page.Total).Error; err != nil {
// 六态卡片继承业务筛选,忽略状态自身,使切换状态时其他计数仍可见。
var counts []struct {
Status int
Count int64
}
if err := filtered().Select("r.status,COUNT(*) AS count").Group("r.status").Scan(&counts).Error; err != nil {
return nil, business.ErrStorage
}
for _, c := range counts {
page.StatusCount[c.Status] = c.Count
}
for status, count := range page.StatusCount {
if filter.Status == nil || status == *filter.Status {
page.Total += count
}
}
o := business.NewListOptions(options...)
var pos []alertRecordPO
if err := q.Select("r.*").Order("r.created_at DESC,r.id DESC").Offset(o.Offset).Limit(o.Limit).Scan(&pos).Error; err != nil {
@@ -136,20 +179,68 @@ func (r *alertRecordRepo) List(ctx context.Context, scope business.AlertRecordSc
for _, po := range pos {
page.Rows = append(page.Rows, toBizAlertRecord(po))
}
// 顶部汇总忽略名称、类型、指标与状态筛选,只保留数据范围、平台和指定渠道。
var counts []struct {
Status int
Count int64
}
if err := base().Select("r.status,COUNT(*) AS count").Group("r.status").Scan(&counts).Error; err != nil {
return nil, business.ErrStorage
}
for _, c := range counts {
page.StatusCount[c.Status] = c.Count
if lossAvailable && len(pos) > 0 {
keys := make([]business.ChannelKey, 0, len(pos))
for _, p := range pos {
keys = append(keys, business.ChannelKey{SourceDB: p.PlatformCode, ID: p.TargetPromoteID})
}
states, err := loadAlertLoss(db, keys, lossWindow)
if err != nil {
return nil, err
}
for _, row := range page.Rows {
row.Lost = states[business.ChannelKey{SourceDB: row.PlatformCode, ID: row.TargetPromoteID}]
}
}
return page, nil
}
func (r *alertRecordRepo) Ignore(ctx context.Context, scope business.AlertRecordScope, ids []uint64, log *business.AlertLog) error {
ids = append([]uint64(nil), ids...)
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return storageError(r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
empty, available, err := prepareAlertScope(tx, scope)
if err != nil {
return err
}
if empty {
return business.ErrAlertRecordScope
}
if !available {
return business.ErrAlertChannelsUnavailable
}
var rows []alertRecordPO
if err := alertScopedQuery(tx, scope).Select("r.*").Where("r.id IN ?", ids).Order("r.id").Clauses(clause.Locking{Strength: "UPDATE"}).Scan(&rows).Error; err != nil {
return err
}
unique := map[uint64]bool{}
for _, id := range ids {
unique[id] = true
}
if len(rows) != len(unique) {
return business.ErrAlertRecordScope
}
now := time.Now()
logs := []alertLogPO{}
changed := []uint64{}
for _, row := range rows {
if row.Status == business.AlertIgnored {
continue
}
changed = append(changed, row.ID)
logs = append(logs, alertLogPO{AlertID: row.ID, OperatorID: log.OperatorID, OperatorName: log.OperatorName, LogType: log.LogType, ActionDetail: log.ActionDetail, CreatedAt: now})
}
if len(changed) == 0 {
return nil
}
if err := tx.Model(&alertRecordPO{}).Where("id IN ?", changed).Updates(map[string]any{"status": business.AlertIgnored, "updated_at": now}).Error; err != nil {
return err
}
return tx.CreateInBatches(logs, 100).Error
}))
}
func findScopedAlert(db *gorm.DB, scope business.AlertRecordScope, id uint64, lock bool) (*alertRecordPO, error) {
empty, available, err := prepareAlertScope(db, scope)
if err != nil {