package business import ( "fmt" "gorm.io/gorm" "kra/pkg/database/migration" "strings" ) func migrateAlertV2(db *gorm.DB) error { if err := migration.CreateMissingTables(db, &alertGapPO{}, &alertExemptionPO{}, &alertChannelLockPO{}, &alertRulePO{}, &alertRecordPO{}); err != nil { return err } for _, item := range []struct { model any fields []string }{ {&alertRulePO{}, []string{"ConditionMode", "AmountDirection", "AmountThreshold"}}, {&alertRecordPO{}, []string{"TotalPlayers", "TotalPay", "WarningColor", "IndependentRule"}}, } { if db.Dialector.Name() == "mysql" { statement := &gorm.Statement{DB: db} if err := statement.Parse(item.model); err != nil { return err } columns := []string{} args := []any{} for _, name := range item.fields { if !db.Migrator().HasColumn(item.model, name) { field := statement.Schema.LookUpField(name) definition := db.Migrator().FullDataTypeOf(field) columns = append(columns, "ADD COLUMN `"+field.DBName+"` "+definition.SQL) args = append(args, definition.Vars...) } } // MySQL 5.7 添加普通列会重建表;合并执行,避免每个字段重建一次。 if len(columns) > 0 { if err := db.Exec("ALTER TABLE `"+statement.Schema.Table+"` "+strings.Join(columns, ", ")+", ALGORITHM=INPLACE, LOCK=NONE", args...).Error; err != nil { return err } } continue } for _, field := range item.fields { if !db.Migrator().HasColumn(item.model, field) { if err := db.Migrator().AddColumn(item.model, field); err != nil { return err } } } } if err := migrateRegistrationIndex(db); err != nil { return err } if err := migrateAlertV2UniqueKey(db); err != nil { return err } return migrateQueryIndexes(db, alertV2QueryIndexes) } func migrateAlertV2UniqueKey(db *gorm.DB) error { if db.Dialector.Name() != "mysql" { return nil } // 旧唯一键将已忽略视为未闭环,也无法容纳同类的两条绝对值规则。 // 保留旧生成列,原子替换唯一键;不改写、合并或删除历史记录。 clauses := []string{} if !db.Migrator().HasColumn("sys_alert_record", "v2_open_key") { // MySQL 5.7 的在线虚拟列变更不能与索引变更放在同一 ALTER 中。 if err := db.Exec("ALTER TABLE sys_alert_record ADD COLUMN v2_open_key BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN status IN (1,2,3,4) THEN CASE WHEN independent_rule=1 THEN rule_id ELSE 0 END ELSE NULL END) VIRTUAL, ALGORITHM=INPLACE, LOCK=NONE").Error; err != nil { return err } } else { var column struct { GenerationExpression string Extra string } if err := db.Raw("SELECT generation_expression,extra FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='sys_alert_record' AND column_name='v2_open_key'").Scan(&column).Error; err != nil { return err } normalize := strings.NewReplacer("`", "", " ", "", "\n", "", "\t", "", "(", "", ")", "") if normalize.Replace(strings.ToLower(column.GenerationExpression)) != "casewhenstatusin1,2,3,4thencasewhenindependent_rule=1thenrule_idelse0endelsenullend" || !strings.Contains(strings.ToUpper(column.Extra), "VIRTUAL GENERATED") { return fmt.Errorf("sys_alert_record.v2_open_key 的现有定义不符,未修改原唯一索引") } } var existing []struct { ColumnName string NonUnique int SubPart *int } if err := db.Raw("SELECT column_name,non_unique,sub_part FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='sys_alert_record' AND index_name='uk_alert_record_v2_open' ORDER BY seq_in_index").Scan(&existing).Error; err != nil { return err } replaceV2 := false if len(existing) > 0 { columns := []string{} for _, column := range existing { if column.NonUnique != 0 || column.SubPart != nil { return fmt.Errorf("sys_alert_record.uk_alert_record_v2_open 必须是完整字段的唯一索引") } columns = append(columns, column.ColumnName) } layout := strings.Join(columns, ",") replaceV2 = layout == "platform_code,target_promote_id,rule_type,v2_open_key" if layout != "business_id,platform_code,target_promote_id,rule_type,v2_open_key" && !replaceV2 { return fmt.Errorf("sys_alert_record.uk_alert_record_v2_open 的现有索引列不符,未修改原唯一索引") } } for _, name := range []string{"uk_alert_record_unclosed", "uk_alert_record_daily"} { if db.Migrator().HasIndex("sys_alert_record", name) { clauses = append(clauses, "DROP INDEX `"+name+"`") } } if replaceV2 { clauses = append(clauses, "DROP INDEX uk_alert_record_v2_open") } if len(existing) == 0 || replaceV2 { clauses = append(clauses, "ADD UNIQUE INDEX uk_alert_record_v2_open(business_id,platform_code,target_promote_id,rule_type,v2_open_key)") } if len(clauses) == 0 { return nil } return db.Exec("ALTER TABLE sys_alert_record " + strings.Join(clauses, ", ") + ", ALGORITHM=INPLACE, LOCK=NONE").Error } var alertV2QueryIndexes = []businessQueryIndex{ {"sys_alert_record", "idx_alert_owner_status_time", []string{"business_id", "status", "created_at", "id"}}, {"sys_alert_record", "idx_alert_channel_type_time", []string{"platform_code", "target_promote_id", "rule_type", "created_at", "id"}}, {"sys_alert_record", "idx_alert_owner_time", []string{"business_id", "created_at", "id"}}, {"sys_alert_record", "idx_alert_created", []string{"created_at", "id"}}, {"sys_alert_operation_log", "idx_alert_log_time", []string{"alert_id", "created_at", "id"}}, {"tab_spend", "idx_alert_spend_window", []string{"source_db", "promote_id", "pay_status", "pay_time", "pay_amount"}}, {"tab_user", "idx_alert_user_window", []string{"source_db", "promote_id", "sw_register_time_unix", "puid"}}, {"tab_promote", "idx_alert_promote_tree", []string{"source_db", "top_promote_id", "id"}}, }