feat: 迁移预警规则管理和历史分类查询

This commit is contained in:
Yvan
2026-11-03 20:19:26 +08:00
parent cd42a1d3f0
commit 7203c34409
31 changed files with 1264 additions and 15 deletions
+55
View File
@@ -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" }
+97
View File
@@ -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)
}
+57
View File
@@ -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)
}
}
+25 -1
View File
@@ -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
}
+1
View File
@@ -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)),
+2 -2
View File
@@ -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 {