Files
kra-new/internal/biz/business/alert_governance.go
T

199 lines
6.0 KiB
Go

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
}