package business import ( "context" "strings" "time" "github.com/go-kratos/kratos/v3/errors" ) var ( ErrAlertRecordInput = errors.BadRequest("SW_ALERT_RECORD_INPUT", "预警记录参数不正确") ErrAlertRecordNotFound = errors.NotFound("SW_ALERT_RECORD_NOT_FOUND", "预警记录不存在") ErrAlertRecordScope = errors.Forbidden("SW_ALERT_RECORD_SCOPE", "无权限查看该预警记录") ErrAlertAuditLevel = errors.Forbidden("SW_ALERT_AUDIT_LEVEL", "仅1级商务可审核预警") ErrAlertChannelsUnavailable = errors.BadRequest("SW_ALERT_CHANNELS_UNAVAILABLE", "渠道数据尚未接入,暂时无法核对预警可见范围") ErrAlertRecordChanged = errors.Conflict("SW_ALERT_RECORD_CHANGED", "预警状态已变化,请刷新后重试") ErrAlertNoteEmpty = errors.BadRequest("SW_ALERT_NOTE_EMPTY", "备注内容不能为空") ) type AlertRecord struct { OwnerScope *UserScope // 检测写入策略,仅供任务与仓储使用,不向 HTTP 返回。 GapDays int IndependentRule bool TotalPlayers *int64 TotalPay, WarningColor *string Lost *bool ID, RuleID, TargetPromoteID uint64 PlatformCode, PlatformName, RuleName, RuleType string MonitorMetric *int TargetPromoteName string BusinessID uint BusinessName, AlertContent string Status int CreatedAt, UpdatedAt time.Time } type AlertLog struct { ID, AlertID uint64 OperatorID uint OperatorName string LogType int ActionDetail, NoteContent string // 写入兼容旧文件 ID 与 URL,读取由仓储批量解析成附件 URL。 AttachmentIDs, Attachments []string CreatedAt time.Time } type AlertRecordScope struct { Owner LevelOneScope Viewer UserScope Selected *UserScope } type AlertRecordFilter struct { AlertContent string Lost *bool PlatformCodes []string TargetPromoteID *uint64 PlatformBusierID *uint64 BusinessID *uint TargetPromoteName, RuleName, RuleType, BusinessName string MonitorMetric, Status *int } type AlertRecordPage struct { Rows []*AlertRecord Total int64 StatusCount map[int]int64 Available bool } type AlertRecordDetail struct { Record *AlertRecord Logs []*AlertLog CanAudit bool } type AlertRecordRepo interface { Platforms(context.Context, AlertRecordScope) ([]string, error) Ignore(context.Context, AlertRecordScope, []uint64, *AlertLog) error List(context.Context, AlertRecordScope, AlertRecordFilter, ...ListOption) (*AlertRecordPage, error) Detail(context.Context, AlertRecordScope, uint64) (*AlertRecordDetail, error) // 在同一事务和行锁内校验当前记录、执行业务回调、写入状态与日志。 WithRecord(context.Context, AlertRecordScope, uint64, func(*AlertRecord) ([]*AlertLog, error)) error } func (uc *AlertRecordUsecase) Platforms(ctx context.Context) ([]string, error) { scope, _, err := uc.scope(ctx, nil) if err != nil { return nil, err } return uc.repo.Platforms(ctx, scope) } type AlertAction string const ( AlertUnprocessed = 1 AlertProcessing = 2 AlertPendingAudit = 3 AlertAuditRejected = 4 AlertProcessed = 5 AlertIgnored = 6 ) const ( AlertStart AlertAction = "start" AlertSubmit AlertAction = "submit" AlertApprove AlertAction = "pass" AlertReject AlertAction = "reject" AlertReprocess AlertAction = "reprocess" ) type AlertRecordUsecase struct { repo AlertRecordRepo accounts *AccountUsecase } func NewAlertRecordUsecase(repo AlertRecordRepo, accounts *AccountUsecase) *AlertRecordUsecase { return &AlertRecordUsecase{repo: repo, accounts: accounts} } func alertUserScope(a *Account) UserScope { if a == nil { return UserScope{Empty: true} } return UserScope{SelfID: a.ID, IncludeSelf: true, OnlySelf: a.Level >= 3, DescendantPath: descendantPath(a)} } func (uc *AlertRecordUsecase) scope(ctx context.Context, selected *uint) (AlertRecordScope, *Account, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return AlertRecordScope{}, nil, err } scope := AlertRecordScope{Owner: LevelOneOwner(actor, account), Viewer: alertUserScope(account)} if actor.AuthorityID == 888 { scope.Viewer = UserScope{All: true} } if selected != nil { target, err := uc.accounts.repo.Find(ctx, *selected) if err != nil && !errors.IsNotFound(err) { return scope, nil, err } // 指定商务只增加渠道交集条件,不能替换查看者范围或预警归属一级商务。 s := alertUserScope(target) scope.Selected = &s } return scope, account, nil } func (uc *AlertRecordUsecase) List(ctx context.Context, filter AlertRecordFilter, options ...ListOption) (*AlertRecordPage, error) { scope, _, err := uc.scope(ctx, filter.BusinessID) if err != nil { return nil, err } return uc.repo.List(ctx, scope, filter, options...) } func (uc *AlertRecordUsecase) Detail(ctx context.Context, id uint64) (*AlertRecordDetail, error) { scope, account, err := uc.scope(ctx, nil) if err != nil { return nil, err } detail, err := uc.repo.Detail(ctx, scope, id) if err != nil { return nil, err } // 审核只看商务等级,与规则管理的超管例外不同。 detail.CanAudit = account.Level == 1 return detail, nil } func alertTransition(status int, action AlertAction) (int, error) { var from, to int var message string switch action { case AlertStart: from, to, message = AlertUnprocessed, AlertProcessing, "仅未处理状态的预警可开始处理" case AlertSubmit: from, to, message = AlertProcessing, AlertPendingAudit, "仅处理中状态的预警可提交审核" case AlertApprove: from, to, message = AlertPendingAudit, AlertProcessed, "仅待审核状态的预警可审核通过" case AlertReject: from, to, message = AlertPendingAudit, AlertAuditRejected, "仅待审核状态的预警可审核驳回" case AlertReprocess: from, to, message = AlertAuditRejected, AlertProcessing, "仅审核驳回状态的预警可重新处理" default: return status, ErrAlertRecordInput } if status != from { return status, errors.BadRequest("SW_ALERT_RECORD_STATUS", message) } return to, nil } func (uc *AlertRecordUsecase) Process(ctx context.Context, id uint64, action AlertAction, note string) error { scope, account, err := uc.scope(ctx, nil) if err != nil { return err } if (action == AlertApprove || action == AlertReject) && account.Level != 1 { return ErrAlertAuditLevel } return uc.repo.WithRecord(ctx, scope, id, func(record *AlertRecord) ([]*AlertLog, error) { status, err := alertTransition(record.Status, action) if err != nil { return nil, err } record.Status = status logs := []*AlertLog{{AlertID: id, OperatorID: account.ID, OperatorName: account.Username, LogType: 1, ActionDetail: string(action)}} if strings.TrimSpace(note) != "" { logs = append(logs, &AlertLog{AlertID: id, OperatorID: account.ID, OperatorName: account.Username, LogType: 2, NoteContent: note}) } return logs, nil }) } func (uc *AlertRecordUsecase) AddNote(ctx context.Context, id uint64, note string, attachments []string) error { if strings.TrimSpace(note) == "" { return ErrAlertNoteEmpty } scope, account, err := uc.scope(ctx, nil) if err != nil { return err } return uc.repo.WithRecord(ctx, scope, id, func(record *AlertRecord) ([]*AlertLog, error) { if record.Status == AlertProcessed || record.Status == AlertIgnored { return nil, ErrAlertRecordChanged } return []*AlertLog{{AlertID: id, OperatorID: account.ID, OperatorName: account.Username, LogType: 2, NoteContent: note, AttachmentIDs: attachments}}, nil }) } func AlertStatusLabel(status int) string { return map[int]string{1: "未处理", 2: "处理中", 3: "待审核", 4: "审核驳回", 5: "已处理", 6: "已忽略"}[status] } func (uc *AlertRecordUsecase) Ignore(ctx context.Context, ids []uint64) error { actor, account, err := uc.accounts.actor(ctx) if err != nil { return err } if actor.AuthorityID != 888 && account.Level != 1 { return ErrAlertGovernanceLevel } if len(ids) == 0 || len(ids) > 500 { return ErrAlertRecordInput } scope, _, err := uc.scope(ctx, nil) if err != nil { return err } return uc.repo.Ignore(ctx, scope, ids, &AlertLog{OperatorID: account.ID, OperatorName: account.Username, LogType: 1, ActionDetail: "ignore"}) } func AlertLogTypeLabel(logType int) string { return map[int]string{1: "状态变更操作", 2: "添加独立备注"}[logType] }