434 lines
14 KiB
Go
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
|
|
}
|