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) }