Files
kra-new/internal/data/business/alert_record.go
T

434 lines
14 KiB
Go

package business
import (
"context"
"encoding/json"
"errors"
"sort"
"strconv"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz/business"
)
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, 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 {
q := db.Table("sys_user_bind_platform b").Where("b.status = ? AND b.source_db IN ? AND b.platform_busier_id IS NOT NULL", "0", []string{"lx", "hw", "yq"})
if !scope.All {
q = q.Joins("JOIN sys_users u ON u.id=b.user_id AND u.deleted_at IS NULL")
q = applyUserScope(q, scope, "u.")
}
return q
}
func alertChannelScopes(scope business.AlertRecordScope) []business.UserScope {
var scopes []business.UserScope
if !scope.Viewer.All {
scopes = append(scopes, scope.Viewer)
}
if scope.Selected != nil {
scopes = append(scopes, *scope.Selected)
}
return scopes
}
// 先判断没有绑定的确定空集,再检查渠道表;缺表不能放宽权限或伪造统计。
func prepareAlertScope(db *gorm.DB, scope business.AlertRecordScope) (empty, available bool, err error) {
if scope.Owner.Empty {
return true, true, nil
}
scopes := alertChannelScopes(scope)
for _, s := range scopes {
if s.Empty {
return true, true, 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 len(bindings) == 0 {
return true, true, nil
}
}
if len(scopes) == 0 {
return false, true, nil
}
available, err = businessTableAvailable(db, "tab_promote")
return false, available, err
}
func alertScopedQuery(db *gorm.DB, scope business.AlertRecordScope) *gorm.DB {
q := applyOwnerScope(db.Table("sys_alert_record r"), scope.Owner, "r.business_id")
for _, s := range alertChannelScopes(scope) {
bindings := alertBindingQuery(db, s).Select("1").Where("b.source_db=p.source_db AND b.platform_busier_id=p.busier_id")
// 两个范围独立取交集;只限制一级渠道,停用渠道的历史预警仍可见。
channels := db.Table("tab_promote p").Select("1").Where("p.id=r.target_promote_id AND p.source_db=r.platform_code AND p.promote_level = ?", 1).Where("EXISTS (?)", bindings)
q = q.Where("EXISTS (?)", channels)
}
return q
}
func (r *alertRecordRepo) List(ctx context.Context, scope business.AlertRecordScope, filter business.AlertRecordFilter, options ...business.ListOption) (*business.AlertRecordPage, error) {
db := r.data.DB().WithContext(ctx)
page := &business.AlertRecordPage{Rows: []*business.AlertRecord{}, StatusCount: map[int]int64{}, Available: true}
empty, available, err := prepareAlertScope(db, scope)
if err != nil {
return nil, err
}
page.Available = available
if empty || !available {
return page, nil
}
if filter.PlatformBusierID != nil {
available, err := businessTableAvailable(db, "tab_promote")
if err != nil {
return nil, err
}
if !available {
page.Available = false
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 {
q = q.Where("r.platform_code IN ?", filter.PlatformCodes)
}
if filter.TargetPromoteID != nil {
q = q.Where("r.target_promote_id = ?", *filter.TargetPromoteID)
}
if filter.PlatformBusierID != nil {
q = q.Where("EXISTS (?)", db.Table("tab_promote bp").Select("1").Where("bp.id=r.target_promote_id AND bp.source_db=r.platform_code AND bp.busier_id=?", *filter.PlatformBusierID))
}
return q
}
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
}
q := filtered()
if filter.Status != nil {
q = q.Where("r.status = ?", *filter.Status)
}
// 六态卡片继承业务筛选,忽略状态自身,使切换状态时其他计数仍可见。
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 {
return nil, business.ErrStorage
}
for _, po := range pos {
page.Rows = append(page.Rows, toBizAlertRecord(po))
}
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 {
return nil, err
}
if empty {
return nil, business.ErrAlertRecordScope
}
if !available {
return nil, business.ErrAlertChannelsUnavailable
}
// 锁定读取本身带权限条件,避免先读旧快照的权限再锁到已换归属的记录。
q := alertScopedQuery(db, scope).Select("r.*")
if lock {
q = q.Clauses(clause.Locking{Strength: "UPDATE"})
}
var po alertRecordPO
if err := q.Where("r.id = ?", id).Take(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
var count int64
if err := db.Model(&alertRecordPO{}).Where("id = ?", id).Count(&count).Error; err != nil {
return nil, business.ErrStorage
}
if count > 0 {
return nil, business.ErrAlertRecordScope
}
return nil, business.ErrAlertRecordNotFound
}
return nil, business.ErrStorage
}
return &po, nil
}
func (r *alertRecordRepo) Detail(ctx context.Context, scope business.AlertRecordScope, id uint64) (*business.AlertRecordDetail, error) {
var detail *business.AlertRecordDetail
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po, err := findScopedAlert(tx, scope, id, false)
if err != nil {
return err
}
logs, err := alertLogs(tx, id)
if err != nil {
return err
}
detail = &business.AlertRecordDetail{Record: toBizAlertRecord(*po), Logs: logs}
return nil
})
return detail, storageError(err)
}
func (r *alertRecordRepo) WithRecord(ctx context.Context, scope business.AlertRecordScope, id uint64, fn func(*business.AlertRecord) ([]*business.AlertLog, error)) error {
return storageError(r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po, err := findScopedAlert(tx, scope, id, true)
if err != nil {
return err
}
record := toBizAlertRecord(*po)
logs, err := fn(record)
if err != nil {
return err
}
now := time.Now()
if record.Status != po.Status {
result := tx.Model(&alertRecordPO{}).Where("id = ? AND status = ?", id, po.Status).Updates(map[string]any{"status": record.Status, "updated_at": now})
if result.Error != nil {
return result.Error
}
if result.RowsAffected != 1 {
return business.ErrAlertRecordChanged
}
}
for _, log := range logs {
attachments := encodeAlertAttachments(log.AttachmentIDs)
row := alertLogPO{AlertID: id, OperatorID: log.OperatorID, OperatorName: log.OperatorName, LogType: log.LogType, ActionDetail: log.ActionDetail, NoteContent: log.NoteContent, AttachmentIDs: attachments, CreatedAt: now}
if err := tx.Create(&row).Error; err != nil {
return err
}
}
return nil
}))
}
func encodeAlertAttachments(values []string) *string {
if len(values) == 0 {
return nil
}
ids := make([]int64, 0, len(values))
for _, value := range values {
id, err := strconv.ParseInt(value, 10, 64)
if err != nil {
ids = nil
break
}
ids = append(ids, id)
}
var raw []byte
if ids != nil {
raw, _ = json.Marshal(ids)
} else {
raw, _ = json.Marshal(values)
}
value := string(raw)
return &value
}
func decodeAlertAttachments(raw *string) (values []string, ids []int64) {
if raw == nil {
return nil, nil
}
var entries []json.RawMessage
if json.Unmarshal([]byte(*raw), &entries) != nil {
return nil, nil
}
allNumeric := true
for _, entry := range entries {
var value string
if json.Unmarshal(entry, &value) != nil {
var n json.Number
if json.Unmarshal(entry, &n) != nil || n == "" {
return nil, nil
}
value = n.String()
}
values = append(values, value)
id, err := strconv.ParseInt(value, 10, 64)
if err != nil {
allNumeric = false
} else {
ids = append(ids, id)
}
}
if !allNumeric {
ids = nil
}
return values, ids
}
func alertLogs(db *gorm.DB, alertID uint64) ([]*business.AlertLog, error) {
var pos []alertLogPO
if err := db.Where("alert_id = ?", alertID).Order("created_at DESC,id DESC").Find(&pos).Error; err != nil {
return nil, business.ErrStorage
}
logs := make([]*business.AlertLog, 0, len(pos))
idsByLog := map[uint64][]int64{}
ids := map[int64]bool{}
for _, po := range pos {
values, numeric := decodeAlertAttachments(po.AttachmentIDs)
log := &business.AlertLog{ID: po.ID, AlertID: po.AlertID, OperatorID: po.OperatorID, OperatorName: po.OperatorName, LogType: po.LogType, ActionDetail: po.ActionDetail, NoteContent: po.NoteContent, CreatedAt: po.CreatedAt, Attachments: []string{}}
if len(numeric) > 0 {
idsByLog[po.ID] = numeric
for _, id := range numeric {
ids[id] = true
}
} else if len(values) > 0 {
log.Attachments = values
}
logs = append(logs, log)
}
if len(ids) == 0 {
return logs, nil
}
keys := make([]int64, 0, len(ids))
for id := range ids {
keys = append(keys, id)
}
// 一次批量查询代替旧实现逐日志、逐附件查询。
urls := map[int64]string{}
for start := 0; start < len(keys); start += 500 {
var files []struct {
ID int64
URL string
}
if err := db.Table("media_file_upload_and_downloads").Select("id,url").Where("id IN ? AND deleted_at IS NULL", keys[start:min(start+500, len(keys))]).Scan(&files).Error; err != nil {
return nil, business.ErrStorage
}
for _, file := range files {
urls[file.ID] = file.URL
}
}
for _, log := range logs {
seen := map[int64]bool{}
for _, id := range idsByLog[log.ID] {
if url, exists := urls[id]; exists && !seen[id] {
log.Attachments = append(log.Attachments, url)
seen[id] = true
}
}
}
return logs, nil
}