feat: implement V2 alert center and scoped alert governance
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
datasystem "kra/internal/data/system"
|
||||
datatask "kra/internal/data/task"
|
||||
"kra/internal/modules/surface"
|
||||
"kra/pkg/module"
|
||||
)
|
||||
|
||||
var alertExemptionMenu = module.Menu{Name: "sw_ExemptChannels", Path: "ExemptChannels", ParentName: "sw_AlertCenter", Component: "view/sw/ExemptChannels.vue", Title: "免预警渠道", Icon: "CircleCheck", Sort: 3}
|
||||
var alertDetectionTask = module.TimedTask{Name: "AlertTrigger", Description: "每日检测预警并清理流失渠道豁免", Spec: "CRON_TZ=Asia/Shanghai 0 0 * * *", MethodName: "alertTriggerTask", Enabled: true}
|
||||
|
||||
func migrateAlertSurface(db *gorm.DB) error {
|
||||
if db.Migrator().HasTable("sys_timed_tasks") {
|
||||
var count int64
|
||||
q := db.Table("sys_timed_tasks").Where("method_name=? AND deleted_at IS NULL", alertDetectionTask.MethodName)
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := datatask.SeedDefaults(context.Background(), db, []module.TimedTask{alertDetectionTask}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 旧检测任务调整到上海零点;保留管理员主动停用的状态。
|
||||
if err := db.Table("sys_timed_tasks").Where("method_name=? AND deleted_at IS NULL", alertDetectionTask.MethodName).Updates(map[string]any{"spec": alertDetectionTask.Spec, "with_seconds": false}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !db.Migrator().HasTable("sys_authorities") {
|
||||
return nil
|
||||
}
|
||||
var count int64
|
||||
if err := db.Table("sys_authorities").Where("authority_id=888 AND deleted_at IS NULL").Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
apis := []module.API{}
|
||||
for _, api := range surface.APIsForPrefix("/sw/", 0) {
|
||||
switch api.Path {
|
||||
case "/sw/alertRecord/platforms", "/sw/alertRecord/ignore", "/sw/alertRule/gap", "/sw/alertExemption/list", "/sw/alertExemption/candidates", "/sw/alertExemption/add", "/sw/alertExemption/remove/:id":
|
||||
apis = append(apis, api)
|
||||
}
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := datasystem.EnsureAdminSurface(tx, module.Surface{Menus: []module.Menu{alertExemptionMenu}, APIs: apis}, 888); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := datasystem.GrantMenuAPIs(tx, "sw_AlertList", apis); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := datasystem.GrantMenuAPIs(tx, "sw_AlertRules", apis); err != nil {
|
||||
return err
|
||||
}
|
||||
var menuID uint
|
||||
if err := tx.Table("sys_base_menus").Select("id").Where("name=? AND deleted_at IS NULL", alertExemptionMenu.Name).Take(&menuID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var roles []uint
|
||||
if err := tx.Table("sys_users").Where("level=1 AND deleted_at IS NULL").Distinct().Pluck("authority_id", &roles).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, role := range roles {
|
||||
if err := tx.Exec("INSERT INTO sys_authority_menus(sys_authority_authority_id,sys_base_menu_id) SELECT ?,? WHERE NOT EXISTS (SELECT 1 FROM sys_authority_menus WHERE sys_authority_authority_id=? AND sys_base_menu_id=?)", role, menuID, role, menuID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return datasystem.GrantMenuAPIs(tx, alertExemptionMenu.Name, apis)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
datatask "kra/internal/data/task"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAlertScheduleUpgradePreservesDisabledTaskAndUsesShanghaiMidnight(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "alert-schedule.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pool, _ := db.DB()
|
||||
t.Cleanup(func() { _ = pool.Close() })
|
||||
for _, step := range datatask.Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Exec("INSERT INTO sys_timed_tasks(name,method_name,spec,with_seconds,enabled) VALUES('历史预警任务','alertTriggerTask','*/5 * * * *',0,0)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := migrateAlertSurface(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var rows []struct {
|
||||
Spec string
|
||||
Enabled bool
|
||||
}
|
||||
if err := db.Table("sys_timed_tasks").Where("method_name='alertTriggerTask'").Scan(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].Enabled || rows[0].Spec != alertDetectionTask.Spec {
|
||||
t.Fatalf("迁移不应重复或启用管理员停用的任务: %+v", rows)
|
||||
}
|
||||
schedule, err := cron.ParseStandard(rows[0].Spec)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next := schedule.Next(time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC))
|
||||
if next.UTC() != time.Date(2026, 11, 5, 16, 0, 0, 0, time.UTC) {
|
||||
t.Fatalf("没有按上海零点执行: %v", next)
|
||||
}
|
||||
}
|
||||
@@ -105,8 +105,10 @@ func Definition() module.Definition {
|
||||
return datatask.SeedDefaults(context.Background(), tx, tasks)
|
||||
})
|
||||
}})
|
||||
definition := module.Definition{Name: "business", Migrations: migrations, TimedTasks: tasks, Surface: module.Surface{
|
||||
migrations = append(migrations, migration.Step{ID: "202611050021_business_alert_surface", Migrate: migrateAlertSurface})
|
||||
definition := module.Definition{Name: "business", Migrations: migrations, TimedTasks: append([]module.TimedTask{alertDetectionTask}, tasks...), Surface: module.Surface{
|
||||
Menus: []module.Menu{
|
||||
alertExemptionMenu,
|
||||
performanceMenu,
|
||||
settingsMenu,
|
||||
{Name: "sw_RankingAnalysis", Path: "RankingAnalysis", ParentName: "sw_DataAnalysis", Component: "view/sw/RankingAnalysis.vue", Title: "排行榜", Icon: databusiness.MenuIcon("sw_RankingAnalysis"), Sort: 6},
|
||||
|
||||
@@ -28,6 +28,9 @@ func TestOtherSettingsUpgradeAddsStandaloneMenuForSuperAdmin(t *testing.T) {
|
||||
if err := db.Exec("INSERT INTO sys_base_menus(name,title,parent_id,sort) VALUES('sw_AlertCenter','预警中心',0,4),('permission','权限管理',0,5)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO sys_timed_tasks(name,method_name,spec,enabled) VALUES('旧检测任务','alertTriggerTask','*/5 * * * *',0)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
for _, step := range Definition().Migrations {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
@@ -64,4 +67,15 @@ func TestOtherSettingsUpgradeAddsStandaloneMenuForSuperAdmin(t *testing.T) {
|
||||
if err := db.Table("casbin_rule").Where("ptype='p' AND v1=? AND v0<>'888'", "/sw/settings/effectiveness").Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatal("不应向商务账号开放设置 API")
|
||||
}
|
||||
var alertTasks []struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
}
|
||||
if err := db.Table("sys_timed_tasks").Where("method_name='alertTriggerTask'").Scan(&alertTasks).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(alertTasks) != 1 || alertTasks[0].Name != "旧检测任务" || alertTasks[0].Enabled {
|
||||
t.Fatalf("整个升级链不应额外创建或启用预警任务: %+v", alertTasks)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,18 +25,18 @@ func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
|
||||
}
|
||||
}
|
||||
if got := catalog.MigrationSteps(); len(got) != 33 {
|
||||
t.Fatalf("module migrations = %d, want 33", len(got))
|
||||
if got := catalog.MigrationSteps(); len(got) != 35 {
|
||||
t.Fatalf("module migrations = %d, want 35", len(got))
|
||||
}
|
||||
var menus, apis int
|
||||
for _, definition := range catalog.Definitions {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 20 || apis != 82 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 20/82", menus, apis)
|
||||
if menus != 21 || apis != 90 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 21/90", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 4 {
|
||||
t.Fatalf("default timed tasks = %d, want 4", len(got))
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 5 {
|
||||
t.Fatalf("default timed tasks = %d, want 5", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user