feat: 重建 Bell 预警处置生命周期 (#133)

This commit is contained in:
QiuSW
2026-08-29 11:56:41 +08:00
parent afc58f7bf4
commit f09a61e5fb
19 changed files with 796 additions and 34 deletions
@@ -0,0 +1,96 @@
package version_local
import (
"runtime"
"gorm.io/gorm"
"go-admin/app/bell/alert_lifecycle"
"go-admin/cmd/migrate/migration"
common "go-admin/common/models"
)
func init() {
_, fileName, _, _ := runtime.Caller(0)
migration.Migrate.SetVersion(migration.GetFilename(fileName), migrateBellAlertLifecycle)
}
func migrateBellAlertLifecycle(db *gorm.DB, version string) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.AutoMigrate(new(alert_lifecycle.Fact), new(alert_lifecycle.RejectionAudit)); err != nil {
return err
}
for _, statement := range alertLifecycleSQL {
if err := tx.Exec(statement).Error; err != nil {
return err
}
}
if err := seedAlertLifecycleAccess(tx); err != nil {
return err
}
return tx.Create(&common.Migration{Version: version}).Error
})
}
var alertLifecycleSQL = []string{
`ALTER TABLE bell_alerts ADD COLUMN acknowledged_by bigint, ADD COLUMN acknowledged_by_name varchar(128), ADD COLUMN acknowledged_at timestamptz, ADD COLUMN closed_by bigint, ADD COLUMN closed_by_name varchar(128), ADD COLUMN closed_at timestamptz, ADD COLUMN close_outcome varchar(32), ADD COLUMN close_note varchar(500)`,
`ALTER TABLE bell_alerts ADD CONSTRAINT bell_alert_ack_user_fk FOREIGN KEY (acknowledged_by) REFERENCES sys_user(user_id) ON UPDATE RESTRICT ON DELETE RESTRICT`,
`ALTER TABLE bell_alerts ADD CONSTRAINT bell_alert_close_user_fk FOREIGN KEY (closed_by) REFERENCES sys_user(user_id) ON UPDATE RESTRICT ON DELETE RESTRICT`,
`ALTER TABLE bell_alerts ADD CONSTRAINT bell_alert_close_outcome_check CHECK (close_outcome IS NULL OR close_outcome IN ('danger_confirmed','false_positive','site_normal','unable_to_confirm'))`,
`ALTER TABLE bell_alert_lifecycle_facts ADD CONSTRAINT bell_alert_lifecycle_alert_fk FOREIGN KEY (alert_id) REFERENCES bell_alerts(id) ON UPDATE RESTRICT ON DELETE RESTRICT`,
`ALTER TABLE bell_alert_lifecycle_facts ADD CONSTRAINT bell_alert_lifecycle_actor_fk FOREIGN KEY (actor_id) REFERENCES sys_user(user_id) ON UPDATE RESTRICT ON DELETE RESTRICT`,
`ALTER TABLE bell_alert_lifecycle_facts ADD CONSTRAINT bell_alert_lifecycle_transition_check CHECK (transition IN ('acknowledged','closed'))`,
`ALTER TABLE bell_alert_lifecycle_facts ADD CONSTRAINT bell_alert_lifecycle_outcome_check CHECK (outcome IS NULL OR outcome IN ('danger_confirmed','false_positive','site_normal','unable_to_confirm'))`,
`CREATE TRIGGER bell_alert_lifecycle_immutable BEFORE UPDATE OR DELETE ON bell_alert_lifecycle_facts FOR EACH ROW EXECUTE FUNCTION bell_reject_immutable_fact()`,
`CREATE TRIGGER bell_alert_lifecycle_rejections_immutable BEFORE UPDATE OR DELETE ON bell_alert_lifecycle_rejections FOR EACH ROW EXECUTE FUNCTION bell_reject_immutable_fact()`,
}
func seedAlertLifecycleAccess(tx *gorm.DB) error {
if err := tx.Exec(`SELECT setval(pg_get_serial_sequence('sys_menu','menu_id'), GREATEST((SELECT max(menu_id) FROM sys_menu),1)); SELECT setval(pg_get_serial_sequence('sys_api','id'), GREATEST((SELECT max(id) FROM sys_api),1))`).Error; err != nil {
return err
}
var alertMenuID int
if err := tx.Table("sys_menu").Select("menu_id").Where("permission = ?", "bell:alert:list").Scan(&alertMenuID).Error; err != nil || alertMenuID == 0 {
return gorm.ErrRecordNotFound
}
ackMenu, err := insertMenu(tx, alertMenuID, "", "开始处理", "", "", "F", "bell:alert:ack", "POST", "", 1)
if err != nil {
return err
}
closeMenu, err := insertMenu(tx, alertMenuID, "", "记录结果", "", "", "F", "bell:alert:close", "POST", "", 2)
if err != nil {
return err
}
specs := []struct{ title, path, action string }{{"预警处理时间线", "/api/v1/bell/alerts/:id/lifecycle", "GET"}, {"开始处理预警", "/api/v1/bell/alerts/:id/ack", "POST"}, {"记录结果并完成", "/api/v1/bell/alerts/:id/close", "POST"}}
apis := make([]apiSeed, 0, len(specs))
for _, spec := range specs {
item, itemErr := insertAPI(tx, spec.title, spec.path, spec.action)
if itemErr != nil {
return itemErr
}
apis = append(apis, item)
}
for _, link := range []struct {
menu int
api apiSeed
}{{alertMenuID, apis[0]}, {ackMenu.ID, apis[1]}, {closeMenu.ID, apis[2]}} {
if err := tx.Exec("INSERT INTO sys_menu_api_rule(sys_menu_menu_id,sys_api_id) VALUES(?,?) ON CONFLICT DO NOTHING", link.menu, link.api.ID).Error; err != nil {
return err
}
}
var operatorRoleID int
if err := tx.Table("sys_role").Select("role_id").Where("role_key = ?", "operator").Scan(&operatorRoleID).Error; err != nil || operatorRoleID == 0 {
return gorm.ErrRecordNotFound
}
for _, menuID := range []int{ackMenu.ID, closeMenu.ID} {
if err := tx.Exec("INSERT INTO sys_role_menu(role_id,menu_id) VALUES(?,?) ON CONFLICT DO NOTHING", operatorRoleID, menuID).Error; err != nil {
return err
}
}
for _, item := range apis {
if err := tx.Exec("INSERT INTO casbin_rule(ptype,v0,v1,v2,v3,v4,v5) VALUES('p','operator',?,?, '', '', '') ON CONFLICT DO NOTHING", item.Path, item.Action).Error; err != nil {
return err
}
}
return nil
}