feat: 迁移预警规则管理和历史分类查询
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package business
|
||||
|
||||
import "time"
|
||||
|
||||
type alertRulePO struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
RuleName string `gorm:"size:100;not null"`
|
||||
RuleType string `gorm:"size:50;not null"`
|
||||
MonitorMetric int `gorm:"type:tinyint;not null"`
|
||||
StatPeriodDays *int `gorm:"type:int"`
|
||||
MomDirection *int `gorm:"type:tinyint"`
|
||||
MomThreshold *string `gorm:"type:decimal(7,2)"`
|
||||
UnloginDays *int `gorm:"type:int"`
|
||||
MonitorScope int `gorm:"type:tinyint;not null;default:1"`
|
||||
WarningColor string `gorm:"size:20;not null"`
|
||||
CreatorID uint `gorm:"index;not null"`
|
||||
Enabled int `gorm:"type:tinyint;not null;default:1"`
|
||||
IsDeleted int `gorm:"type:tinyint;not null;default:0"`
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (alertRulePO) TableName() string { return "sys_alert_rule" }
|
||||
|
||||
type alertRecordPO struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
PlatformCode, PlatformName string `gorm:"size:50"`
|
||||
RuleID uint64
|
||||
RuleName string `gorm:"size:100"`
|
||||
RuleType string `gorm:"size:50"`
|
||||
MonitorMetric *int
|
||||
TargetPromoteID uint64
|
||||
TargetPromoteName string `gorm:"size:100"`
|
||||
BusinessID uint `gorm:"index"`
|
||||
BusinessName string `gorm:"size:100"`
|
||||
AlertContent string `gorm:"type:text"`
|
||||
Status int `gorm:"type:tinyint;default:1;index"`
|
||||
AlertDate *time.Time `gorm:"type:date"`
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (alertRecordPO) TableName() string { return "sys_alert_record" }
|
||||
|
||||
type alertLogPO struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
AlertID uint64 `gorm:"index"`
|
||||
OperatorID uint
|
||||
OperatorName string `gorm:"size:50"`
|
||||
LogType int
|
||||
ActionDetail string `gorm:"size:100"`
|
||||
NoteContent string `gorm:"type:text"`
|
||||
AttachmentIDs *string `gorm:"type:json"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (alertLogPO) TableName() string { return "sys_alert_operation_log" }
|
||||
@@ -0,0 +1,97 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
"time"
|
||||
)
|
||||
|
||||
type alertRuleRepo struct{ data Provider }
|
||||
|
||||
func NewAlertRuleRepo(data Provider) business.AlertRuleRepo { return &alertRuleRepo{data: data} }
|
||||
func toBizAlertRule(po alertRulePO) *business.AlertRule {
|
||||
return &business.AlertRule{ID: po.ID, Name: po.RuleName, Type: po.RuleType, Metric: po.MonitorMetric, StatPeriodDays: po.StatPeriodDays, MomDirection: po.MomDirection, MomThreshold: po.MomThreshold, UnloginDays: po.UnloginDays, MonitorScope: po.MonitorScope, WarningColor: po.WarningColor, CreatorID: po.CreatorID, Enabled: po.Enabled, IsDeleted: po.IsDeleted, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt}
|
||||
}
|
||||
func newAlertRule(r *business.AlertRule) alertRulePO {
|
||||
return alertRulePO{ID: r.ID, RuleName: r.Name, RuleType: r.Type, MonitorMetric: r.Metric, StatPeriodDays: r.StatPeriodDays, MomDirection: r.MomDirection, MomThreshold: r.MomThreshold, UnloginDays: r.UnloginDays, MonitorScope: r.MonitorScope, WarningColor: r.WarningColor, CreatorID: r.CreatorID, Enabled: r.Enabled, IsDeleted: r.IsDeleted}
|
||||
}
|
||||
func applyOwnerScope(db *gorm.DB, scope business.LevelOneScope, column string) *gorm.DB {
|
||||
if scope.All {
|
||||
return db
|
||||
}
|
||||
if scope.Empty {
|
||||
return db.Where("1=0")
|
||||
}
|
||||
return db.Where(column+" = ?", scope.UserID)
|
||||
}
|
||||
func (r *alertRuleRepo) List(ctx context.Context, scope business.LevelOneScope, filter business.AlertRuleFilter, options ...business.ListOption) ([]*business.AlertRule, int64, error) {
|
||||
db := applyOwnerScope(r.data.DB().WithContext(ctx).Table("sys_alert_rule r").Where("r.is_deleted = ?", 0), scope, "r.creator_id")
|
||||
if filter.Type != "" {
|
||||
db = db.Where("r.rule_type = ?", filter.Type)
|
||||
}
|
||||
if filter.Name != "" {
|
||||
db = db.Where("r.rule_name LIKE ?", "%"+filter.Name+"%")
|
||||
}
|
||||
if filter.Metric != nil {
|
||||
db = db.Where("r.monitor_metric = ?", *filter.Metric)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, business.ErrStorage
|
||||
}
|
||||
var pos []struct {
|
||||
Rule alertRulePO `gorm:"embedded"`
|
||||
CreatorName string
|
||||
}
|
||||
o := business.NewListOptions(options...)
|
||||
if err := db.Select("r.*,u.username AS creator_name").Joins("LEFT JOIN sys_users u ON u.id=r.creator_id AND u.deleted_at IS NULL").Order("r.created_at DESC,r.id DESC").Offset(o.Offset).Limit(o.Limit).Scan(&pos).Error; err != nil {
|
||||
return nil, 0, business.ErrStorage
|
||||
}
|
||||
rows := make([]*business.AlertRule, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
rule := toBizAlertRule(po.Rule)
|
||||
rule.CreatorName = po.CreatorName
|
||||
rows = append(rows, rule)
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
func (r *alertRuleRepo) Find(ctx context.Context, id uint64) (*business.AlertRule, error) {
|
||||
var po alertRulePO
|
||||
err := r.data.DB().WithContext(ctx).First(&po, "id = ?", id).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, business.ErrAlertRuleNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
rule := toBizAlertRule(po)
|
||||
if err := r.data.DB().WithContext(ctx).Table("sys_users").Where("id = ? AND deleted_at IS NULL", rule.CreatorID).Select("username").Scan(&rule.CreatorName).Error; err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
func (r *alertRuleRepo) Create(ctx context.Context, rule *business.AlertRule) error {
|
||||
po := newAlertRule(rule)
|
||||
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
|
||||
return storageError(err)
|
||||
}
|
||||
rule.ID, rule.CreatedAt, rule.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
|
||||
return nil
|
||||
}
|
||||
func (r *alertRuleRepo) Update(ctx context.Context, rule *business.AlertRule) error {
|
||||
// 编辑不覆盖启停、删除和创建人字段,避免并发启停被旧配置覆盖。
|
||||
return storageError(r.data.DB().WithContext(ctx).Model(&alertRulePO{}).Where("id = ?", rule.ID).Updates(map[string]any{"rule_name": rule.Name, "rule_type": rule.Type, "monitor_metric": rule.Metric, "stat_period_days": rule.StatPeriodDays, "mom_direction": rule.MomDirection, "mom_threshold": rule.MomThreshold, "unlogin_days": rule.UnloginDays, "monitor_scope": 1, "warning_color": rule.WarningColor, "updated_at": time.Now()}).Error)
|
||||
}
|
||||
func (r *alertRuleRepo) SetDeleted(ctx context.Context, id uint64) error {
|
||||
return storageError(r.data.DB().WithContext(ctx).Model(&alertRulePO{}).Where("id = ?", id).Updates(map[string]any{"is_deleted": 1, "updated_at": time.Now()}).Error)
|
||||
}
|
||||
func (r *alertRuleRepo) SetEnabled(ctx context.Context, id uint64, enabled int) error {
|
||||
return storageError(r.data.DB().WithContext(ctx).Model(&alertRulePO{}).Where("id = ?", id).Updates(map[string]any{"enabled": enabled, "updated_at": time.Now()}).Error)
|
||||
}
|
||||
func (r *alertRuleRepo) TypesFromRecords(ctx context.Context, scope business.LevelOneScope) ([]string, error) {
|
||||
rows := []string{}
|
||||
err := applyOwnerScope(r.data.DB().WithContext(ctx).Table("sys_alert_record").Where("rule_type IS NOT NULL AND TRIM(rule_type) <> ''"), scope, "business_id").Distinct("rule_type").Order("rule_type").Pluck("rule_type", &rows).Error
|
||||
return rows, storageError(err)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAlertRulesKeepHistoryAndDoNotOverwriteConcurrentToggle(t *testing.T) {
|
||||
db := prepareAccounts(t)
|
||||
if err := db.Create(&accountPO{ID: 7, Username: "owner", Level: 1}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewAlertRuleRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
threshold := "20.50"
|
||||
period, direction := 7, 2
|
||||
rule := &business.AlertRule{Name: "规则", Type: "register_anomaly", Metric: 1, StatPeriodDays: &period, MomDirection: &direction, MomThreshold: &threshold, CreatorID: 7, Enabled: 1, MonitorScope: 1, WarningColor: "red"}
|
||||
if err := r.Create(ctx, rule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.SetEnabled(ctx, rule.ID, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rule.Name = "已修改"
|
||||
if err := r.Update(ctx, rule); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := r.Find(ctx, rule.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded.Enabled != 0 || loaded.CreatorID != 7 || loaded.CreatorName != "owner" {
|
||||
t.Fatalf("编辑覆盖了独立状态或归属: %+v", loaded)
|
||||
}
|
||||
rows, total, err := r.List(ctx, business.LevelOneScope{UserID: 7}, business.AlertRuleFilter{})
|
||||
if err != nil || total != 1 || len(rows) != 1 {
|
||||
t.Fatalf("规则列表错误: %v", err)
|
||||
}
|
||||
if err := db.Create(&alertRecordPO{BusinessID: 7, RuleType: "register_anomaly"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Create(&alertRecordPO{BusinessID: 99, RuleType: "other"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := r.SetDeleted(ctx, rule.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, total, err = r.List(ctx, business.LevelOneScope{UserID: 7}, business.AlertRuleFilter{})
|
||||
if err != nil || total != 0 {
|
||||
t.Fatal("删除后仍出现在规则列表")
|
||||
}
|
||||
types, err := r.TypesFromRecords(ctx, business.LevelOneScope{UserID: 7})
|
||||
if err != nil || len(types) != 1 || types[0] != "register_anomaly" {
|
||||
t.Fatalf("删除规则后丢失历史分类或跨商务泄漏: %+v %v", types, err)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error {
|
||||
return []migration.Step{{ID: "202611030006_business_alert_schema", Migrate: migrateAlertSchema}, {ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &resetCodePO{}, &resetTokenPO{})
|
||||
}}, {ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &bindingLockPO{})
|
||||
@@ -35,3 +35,27 @@ func Migrations() []migration.Step {
|
||||
})
|
||||
}}}
|
||||
}
|
||||
|
||||
func migrateAlertSchema(db *gorm.DB) error {
|
||||
if err := migration.CreateMissingTables(db, &alertRulePO{}, &alertRecordPO{}, &alertLogPO{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if db.Dialector.Name() != "mysql" {
|
||||
return nil
|
||||
}
|
||||
columns, err := db.Migrator().ColumnTypes(&alertRulePO{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, column := range columns {
|
||||
if column.Name() == "mom_threshold" {
|
||||
precision, scale, ok := column.DecimalSize()
|
||||
if ok && precision >= 7 && scale >= 2 {
|
||||
return nil
|
||||
}
|
||||
// 原页面允许 9999%,旧 decimal(5,2) 装不下;扩大精度且不改变已有值。
|
||||
return db.Migrator().AlterColumn(&alertRulePO{}, "MomThreshold")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ var ProviderSet = wire.NewSet(
|
||||
databusiness.NewPlatformAccountRepo,
|
||||
databusiness.NewPasswordResetRepo,
|
||||
databusiness.NewBusinessPromoteRepo,
|
||||
databusiness.NewAlertRuleRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
@@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if versions != 19 {
|
||||
t.Fatalf("migration versions = %d, want 19", versions)
|
||||
if versions != 20 {
|
||||
t.Fatalf("migration versions = %d, want 20", versions)
|
||||
}
|
||||
var communicationRows []dataintegration.ConfigPO
|
||||
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
||||
|
||||
Reference in New Issue
Block a user