package business import ( "context" "kra/internal/biz/system" "strings" "time" "github.com/go-kratos/kratos/v3/errors" ) var ( ErrAlertGovernanceLevel = errors.Forbidden("SW_ALERT_GOVERNANCE_LEVEL", "仅一级商务或超管可操作预警治理") ErrAlertExemptExists = errors.Conflict("SW_ALERT_EXEMPT_EXISTS", "渠道已在免预警名单中") ErrAlertExemptNotFound = errors.NotFound("SW_ALERT_EXEMPT_NOT_FOUND", "免预警记录不存在或已移出") ) type AlertGap struct { OwnerID uint Enabled bool Days int } type AlertExemption struct { ID uint64 ChannelKey ChannelName string OwnerID, AddedBy uint AddedName, Reason string AddedAt time.Time RemovedBy uint RemovedName, RemoveReason string RemovedAt *time.Time } type AlertExemptionFilter struct { Name string Active *bool } type AlertExemptionTarget struct { Name string Account *Account } type AlertChannelOption struct { ChannelKey Name string } type AlertLossWindow struct { LoginBefore, PayBegin, PayEnd int64 MinPay string } func AlertLossAt(now time.Time) AlertLossWindow { now = now.In(time.FixedZone("Asia/Shanghai", 8*3600)) return AlertLossWindow{LoginBefore: now.AddDate(0, 0, -15).Unix(), PayBegin: now.AddDate(0, 0, -30).Unix(), PayEnd: now.Unix(), MinPay: "500"} } func (w AlertLossWindow) LoginExpired(lastLogin, created int64) bool { if lastLogin <= 0 { lastLogin = created } return lastLogin > 0 && lastLogin < w.LoginBefore } func (w AlertLossWindow) IsLost(lastLogin, created int64, pay string) (bool, error) { if w.LoginExpired(lastLogin, created) { return true, nil } amount, err := portraitNumber(pay) if err != nil { return false, err } minimum, err := portraitNumber(w.MinPay) if err != nil { return false, err } return amount.Cmp(minimum) < 0, nil } type AlertGovernanceRepo interface { Candidates(context.Context, UserScope, string, int) ([]*AlertChannelOption, error) Gap(context.Context, uint) (*AlertGap, error) SaveGap(context.Context, *AlertGap) error Exemptions(context.Context, LevelOneScope, AlertExemptionFilter, ...ListOption) ([]*AlertExemption, int64, error) // 在渠道锁及事务内读取当前归属,由领域回调构造豁免记录后写入。 AddExemption(context.Context, UserScope, ChannelKey, func(*AlertExemptionTarget) (*AlertExemption, error)) error RemoveExemption(context.Context, LevelOneScope, uint64, uint, string, string, time.Time) error LostExemptions(context.Context, AlertLossWindow) ([]*AlertExemption, error) } func (uc *AlertGovernanceUsecase) Candidates(ctx context.Context, keyword string, limit int) ([]*AlertChannelOption, error) { scope, a, err := uc.owner(ctx, true) if err != nil { return nil, err } viewer := alertUserScope(a) if scope.All { viewer = UserScope{All: true} } return uc.repo.Candidates(ctx, viewer, strings.TrimSpace(keyword), limit) } type AlertGovernanceUsecase struct { repo AlertGovernanceRepo accounts *AccountUsecase } func NewAlertGovernanceUsecase(repo AlertGovernanceRepo, accounts *AccountUsecase) *AlertGovernanceUsecase { return &AlertGovernanceUsecase{repo, accounts} } func (uc *AlertGovernanceUsecase) owner(ctx context.Context, write bool) (LevelOneScope, *Account, error) { actor, account, err := uc.accounts.actor(ctx) if err != nil { return LevelOneScope{}, nil, err } if write && actor.AuthorityID != 888 && account.Level != 1 { return LevelOneScope{}, nil, ErrAlertGovernanceLevel } return LevelOneOwner(actor, account), account, nil } func (uc *AlertGovernanceUsecase) Gap(ctx context.Context) (*AlertGap, error) { scope, a, err := uc.owner(ctx, false) if err != nil { return nil, err } if scope.Empty { return nil, ErrAlertRuleScope } owner := scope.UserID if scope.All { owner = a.ID } return uc.repo.Gap(ctx, owner) } func (uc *AlertGovernanceUsecase) SaveGap(ctx context.Context, enabled bool, days int) error { _, a, err := uc.owner(ctx, true) if err != nil { return err } if days < 1 { return ErrAlertRuleInput } return uc.repo.SaveGap(ctx, &AlertGap{OwnerID: a.ID, Enabled: enabled, Days: days}) } func (uc *AlertGovernanceUsecase) List(ctx context.Context, filter AlertExemptionFilter, opts ...ListOption) ([]*AlertExemption, int64, error) { scope, _, err := uc.owner(ctx, true) if err != nil { return nil, 0, err } return uc.repo.Exemptions(ctx, scope, filter, opts...) } func (uc *AlertGovernanceUsecase) Add(ctx context.Context, key ChannelKey, reason string) error { scope, a, err := uc.owner(ctx, true) if err != nil { return err } reason = strings.TrimSpace(reason) if key.ID == 0 || reason == "" { return ErrAlertRecordInput } key.SourceDB, err = NormalizePlatform(key.SourceDB) if err != nil { return err } viewer := alertUserScope(a) if scope.All { viewer = UserScope{All: true} } return uc.repo.AddExemption(ctx, viewer, key, func(target *AlertExemptionTarget) (*AlertExemption, error) { owner := LevelOneOwnerForAccount(target.Account) if owner.Empty { return nil, ErrAlertRecordScope } return &AlertExemption{ChannelKey: key, ChannelName: target.Name, OwnerID: owner.UserID, AddedBy: a.ID, AddedName: a.Username, Reason: reason, AddedAt: time.Now()}, nil }) } func LevelOneOwnerForAccount(a *Account) LevelOneScope { // 系统账号特权只用于授权,不用于渠道的商务归属。 return LevelOneOwner(system.Actor{}, a) } func (uc *AlertGovernanceUsecase) Remove(ctx context.Context, id uint64) error { scope, a, err := uc.owner(ctx, true) if err != nil { return err } return uc.repo.RemoveExemption(ctx, scope, id, a.ID, a.Username, "手动移出", time.Now()) } func (uc *AlertGovernanceUsecase) Reconcile(ctx context.Context, now time.Time) error { rows, err := uc.repo.LostExemptions(ctx, AlertLossAt(now)) if err != nil { return err } for _, row := range rows { if err := uc.repo.RemoveExemption(ctx, LevelOneScope{All: true}, row.ID, 0, "系统(自动移出)", "进入激活池 · 自动移出", now); err != nil && !errors.IsNotFound(err) { return err } } return nil }