343 lines
9.8 KiB
Go
343 lines
9.8 KiB
Go
package business
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/go-kratos/kratos/v3/errors"
|
|
"kra/internal/biz/system"
|
|
"math/big"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
var alertDecimal = regexp.MustCompile(`^[0-9]{1,12}(\.[0-9]{1,2})?$`)
|
|
|
|
var (
|
|
ErrAlertRuleNotFound = errors.NotFound("SW_ALERT_RULE_NOT_FOUND", "规则不存在")
|
|
ErrAlertRuleScope = errors.Forbidden("SW_ALERT_RULE_SCOPE", "无权限操作该规则")
|
|
ErrAlertRuleLevel = errors.Forbidden("SW_ALERT_RULE_LEVEL", "仅1级商务可操作预警规则")
|
|
ErrAlertRuleInput = errors.BadRequest("SW_ALERT_RULE_INPUT", "预警规则参数不正确")
|
|
ErrAlertRulePeriod = errors.BadRequest("SW_ALERT_RULE_PERIOD", "统计周期必须大于0")
|
|
ErrAlertRuleDirection = errors.BadRequest("SW_ALERT_RULE_DIRECTION", "环比方向必须为上升或下降")
|
|
ErrAlertRuleThreshold = errors.BadRequest("SW_ALERT_RULE_THRESHOLD", "环比阈值必须大于0且不超过9999")
|
|
ErrAlertRuleUnlogin = errors.BadRequest("SW_ALERT_RULE_UNLOGIN", "未登录天数必须大于0")
|
|
)
|
|
|
|
type LevelOneScope struct {
|
|
All, Empty bool
|
|
UserID uint
|
|
}
|
|
|
|
func LevelOneOwner(actor system.Actor, a *Account) LevelOneScope {
|
|
if actor.AuthorityID == 888 {
|
|
return LevelOneScope{All: true}
|
|
}
|
|
if a == nil {
|
|
return LevelOneScope{Empty: true}
|
|
}
|
|
if a.Level == 1 {
|
|
return LevelOneScope{UserID: a.ID}
|
|
}
|
|
if a.Level < 2 || a.Level > 3 {
|
|
return LevelOneScope{Empty: true}
|
|
}
|
|
parts := strings.Split(a.IDPath, ",")
|
|
if len(parts) < 2 {
|
|
return LevelOneScope{Empty: true}
|
|
}
|
|
id, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64)
|
|
if err != nil || id == 0 {
|
|
return LevelOneScope{Empty: true}
|
|
}
|
|
return LevelOneScope{UserID: uint(id)}
|
|
}
|
|
|
|
type AlertRule struct {
|
|
ConditionMode int
|
|
AmountDirection int
|
|
AmountThreshold *string
|
|
ID uint64
|
|
Name, Type string
|
|
Metric int
|
|
StatPeriodDays, MomDirection, UnloginDays *int
|
|
MomThreshold *string
|
|
MonitorScope int
|
|
WarningColor string
|
|
CreatorID uint
|
|
CreatorName string
|
|
Enabled, IsDeleted int
|
|
CreatedAt, UpdatedAt time.Time
|
|
}
|
|
type AlertRuleFilter struct {
|
|
Name, Type string
|
|
Metric *int
|
|
}
|
|
type AlertRuleRepo interface {
|
|
List(context.Context, LevelOneScope, AlertRuleFilter, ...ListOption) ([]*AlertRule, int64, error)
|
|
Find(context.Context, uint64) (*AlertRule, error)
|
|
Create(context.Context, *AlertRule) error
|
|
Update(context.Context, *AlertRule) error
|
|
SetDeleted(context.Context, uint64) error
|
|
SetEnabled(context.Context, uint64, int) error
|
|
TypesFromRecords(context.Context, LevelOneScope) ([]string, error)
|
|
}
|
|
type AlertRuleUsecase struct {
|
|
repo AlertRuleRepo
|
|
accounts *AccountUsecase
|
|
}
|
|
|
|
func NewAlertRuleUsecase(repo AlertRuleRepo, accounts *AccountUsecase) *AlertRuleUsecase {
|
|
return &AlertRuleUsecase{repo: repo, accounts: accounts}
|
|
}
|
|
|
|
func (uc *AlertRuleUsecase) List(ctx context.Context, filter AlertRuleFilter, options ...ListOption) ([]*AlertRule, int64, error) {
|
|
actor, a, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return uc.repo.List(ctx, LevelOneOwner(actor, a), filter, options...)
|
|
}
|
|
func (uc *AlertRuleUsecase) Find(ctx context.Context, id uint64) (*AlertRule, error) {
|
|
actor, a, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rule, err := uc.repo.Find(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
scope := LevelOneOwner(actor, a)
|
|
if !scope.All && (scope.Empty || scope.UserID != rule.CreatorID) {
|
|
return nil, ErrAlertRuleScope
|
|
}
|
|
return rule, nil
|
|
}
|
|
func (uc *AlertRuleUsecase) Types(ctx context.Context) ([]string, error) {
|
|
actor, a, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := uc.repo.TypesFromRecords(ctx, LevelOneOwner(actor, a))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := []string{}
|
|
seen := map[string]bool{}
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value != "" && !seen[value] {
|
|
seen[value] = true
|
|
result = append(result, value)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
func validateAlertRule(r *AlertRule) error {
|
|
if r == nil {
|
|
return ErrAlertRuleInput
|
|
}
|
|
r.Type = strings.TrimSpace(r.Type)
|
|
if r.ConditionMode == 0 {
|
|
r.ConditionMode = 1
|
|
}
|
|
if r.ConditionMode != 1 && r.ConditionMode != 2 {
|
|
return ErrAlertRuleInput
|
|
}
|
|
// 非当前指标的输入不参与业务配置,避免隐藏字段绕过校验写入存储。
|
|
if r.Metric == 3 {
|
|
r.StatPeriodDays = nil
|
|
r.MomDirection = nil
|
|
r.MomThreshold = nil
|
|
} else {
|
|
r.UnloginDays = nil
|
|
}
|
|
if r.ConditionMode == 2 {
|
|
r.MomDirection = nil
|
|
r.MomThreshold = nil
|
|
} else {
|
|
r.AmountThreshold = nil
|
|
r.AmountDirection = 1
|
|
}
|
|
if r.ConditionMode == 2 {
|
|
if r.Metric != 2 || r.StatPeriodDays == nil || *r.StatPeriodDays <= 0 {
|
|
return ErrAlertRuleInput
|
|
}
|
|
if r.AmountDirection != 1 && r.AmountDirection != 2 {
|
|
return ErrAlertRuleInput
|
|
}
|
|
if r.AmountThreshold == nil || !alertDecimal.MatchString(*r.AmountThreshold) {
|
|
return ErrAlertRuleInput
|
|
}
|
|
n, ok := new(big.Rat).SetString(*r.AmountThreshold)
|
|
if !ok || n.Sign() <= 0 || n.Cmp(big.NewRat(99999999999999, 100)) > 0 || !new(big.Rat).Mul(n, big.NewRat(100, 1)).IsInt() {
|
|
return ErrAlertRuleInput
|
|
}
|
|
amount := n.FloatString(2)
|
|
r.AmountThreshold = &amount
|
|
if r.Type == "" || len(utf16.Encode([]rune(r.Type))) > 50 {
|
|
return ErrAlertRuleInput
|
|
}
|
|
return nil
|
|
}
|
|
if r.Type == "" || len(utf16.Encode([]rune(r.Type))) > 50 {
|
|
return ErrAlertRuleInput
|
|
}
|
|
switch r.Metric {
|
|
case 1, 2:
|
|
if r.StatPeriodDays == nil || *r.StatPeriodDays <= 0 {
|
|
return ErrAlertRulePeriod
|
|
}
|
|
if r.MomDirection == nil || (*r.MomDirection != 1 && *r.MomDirection != 2) {
|
|
return ErrAlertRuleDirection
|
|
}
|
|
if r.MomThreshold == nil || !alertDecimal.MatchString(*r.MomThreshold) {
|
|
return ErrAlertRuleThreshold
|
|
}
|
|
n, ok := new(big.Rat).SetString(*r.MomThreshold)
|
|
if !ok || n.Sign() <= 0 || n.Cmp(big.NewRat(9999, 1)) > 0 {
|
|
return ErrAlertRuleThreshold
|
|
}
|
|
case 3:
|
|
if r.UnloginDays == nil || *r.UnloginDays <= 0 {
|
|
return ErrAlertRuleUnlogin
|
|
}
|
|
default:
|
|
return ErrAlertRuleInput
|
|
}
|
|
return nil
|
|
}
|
|
func (uc *AlertRuleUsecase) Save(ctx context.Context, input *AlertRule) (uint64, error) {
|
|
actor, a, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if actor.AuthorityID != 888 && a.Level != 1 {
|
|
return 0, ErrAlertRuleLevel
|
|
}
|
|
if input == nil {
|
|
return 0, ErrAlertRuleInput
|
|
}
|
|
rule := *input
|
|
if err := validateAlertRule(&rule); err != nil {
|
|
return 0, err
|
|
}
|
|
rule.MonitorScope = 1
|
|
if rule.ID == 0 {
|
|
rule.CreatorID, rule.Enabled, rule.IsDeleted = actor.UserID, 1, 0
|
|
if err := uc.repo.Create(ctx, &rule); err != nil {
|
|
return 0, err
|
|
}
|
|
return rule.ID, nil
|
|
}
|
|
existing, err := uc.repo.Find(ctx, rule.ID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if actor.AuthorityID != 888 && existing.CreatorID != actor.UserID {
|
|
return 0, ErrAlertRuleScope
|
|
}
|
|
// 原接口忽略未提供的可选指标字段,切换指标后保留历史配置值。
|
|
if rule.StatPeriodDays == nil {
|
|
rule.StatPeriodDays = existing.StatPeriodDays
|
|
}
|
|
if rule.MomDirection == nil {
|
|
rule.MomDirection = existing.MomDirection
|
|
}
|
|
if rule.MomThreshold == nil {
|
|
rule.MomThreshold = existing.MomThreshold
|
|
}
|
|
if rule.UnloginDays == nil {
|
|
rule.UnloginDays = existing.UnloginDays
|
|
}
|
|
if err := uc.repo.Update(ctx, &rule); err != nil {
|
|
return 0, err
|
|
}
|
|
return rule.ID, nil
|
|
}
|
|
func (uc *AlertRuleUsecase) checkWrite(ctx context.Context, id uint64) error {
|
|
actor, a, err := uc.accounts.actor(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if actor.AuthorityID != 888 && a.Level != 1 {
|
|
return ErrAlertRuleLevel
|
|
}
|
|
rule, err := uc.repo.Find(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if actor.AuthorityID != 888 && rule.CreatorID != actor.UserID {
|
|
return ErrAlertRuleScope
|
|
}
|
|
return nil
|
|
}
|
|
func (uc *AlertRuleUsecase) Delete(ctx context.Context, id uint64) error {
|
|
if err := uc.checkWrite(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
return uc.repo.SetDeleted(ctx, id)
|
|
}
|
|
func (uc *AlertRuleUsecase) Toggle(ctx context.Context, id uint64, enabled int) error {
|
|
if enabled != 0 && enabled != 1 {
|
|
return ErrAlertRuleInput
|
|
}
|
|
if err := uc.checkWrite(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
return uc.repo.SetEnabled(ctx, id, enabled)
|
|
}
|
|
|
|
func AlertRuleTypeLabel(value string) string {
|
|
switch value {
|
|
case "register_anomaly":
|
|
return "注册异常"
|
|
case "recharge_anomaly":
|
|
return "充值异常"
|
|
case "login_anomaly":
|
|
return "登录异常"
|
|
}
|
|
return value
|
|
}
|
|
func AlertMetricLabel(metric int) string {
|
|
return map[int]string{1: "注册量", 2: "充值金额", 3: "登录天数"}[metric]
|
|
}
|
|
func AlertDirectionLabel(direction *int) string {
|
|
if direction == nil {
|
|
return ""
|
|
}
|
|
return map[int]string{1: "上升", 2: "下降"}[*direction]
|
|
}
|
|
func optionalInt(value *int) string {
|
|
if value == nil {
|
|
return "null"
|
|
}
|
|
return strconv.Itoa(*value)
|
|
}
|
|
func (r *AlertRule) Description() string {
|
|
if r.Metric == 2 && r.ConditionMode == 2 && r.AmountThreshold != nil {
|
|
direction := "满"
|
|
if r.AmountDirection == 2 {
|
|
direction = "未满"
|
|
}
|
|
return fmt.Sprintf("渠道近 %s 天充值金额%s %s 元时触发预警(汇总子渠道)", optionalInt(r.StatPeriodDays), direction, *r.AmountThreshold)
|
|
}
|
|
if r.Metric == 3 {
|
|
return "渠道连续 " + optionalInt(r.UnloginDays) + " 天未登录时触发预警"
|
|
}
|
|
if r.Metric != 1 && r.Metric != 2 {
|
|
return ""
|
|
}
|
|
threshold := "null"
|
|
if r.MomThreshold != nil {
|
|
threshold = *r.MomThreshold
|
|
}
|
|
direction := AlertDirectionLabel(r.MomDirection)
|
|
if direction == "" {
|
|
direction = "null"
|
|
}
|
|
return fmt.Sprintf("渠道近 %s 天%s较上一同期环比%s %s%% 时触发", optionalInt(r.StatPeriodDays), AlertMetricLabel(r.Metric), direction, threshold)
|
|
}
|