package business import ( "errors" "fmt" "strings" "gorm.io/gorm" ) // 保留旧 register_time 字符串及其 CAST 语义,用虚拟列索引避免逐行扫描主账号。 // MySQL 可将查询中相同的 CAST 表达式匹配到该索引,既有查询无需更改统计口径。 func migrateRegistrationIndex(db *gorm.DB) error { if db.Dialector.Name() != "mysql" || !db.Migrator().HasTable("tab_user") { return nil } const column = "sw_register_time_unix" const index = "idx_sw_user_registration" if !db.Migrator().HasColumn("tab_user", column) { if err := db.Exec("ALTER TABLE tab_user ADD COLUMN sw_register_time_unix BIGINT UNSIGNED GENERATED ALWAYS AS (CAST(register_time AS UNSIGNED)) VIRTUAL, ALGORITHM=INPLACE, LOCK=NONE").Error; err != nil { return err } } else { var expression string if err := db.Raw("SELECT generation_expression FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='tab_user' AND column_name=?", column).Scan(&expression).Error; err != nil { return err } normalized := strings.NewReplacer("`", "", " ", "", "\n", "", "\t", "").Replace(strings.ToLower(expression)) if normalized != "cast(register_timeasunsigned)" { return fmt.Errorf("tab_user.%s 的现有定义与注册时间转换不符", column) } } if !db.Migrator().HasIndex("tab_user", index) { return addRegistrationIndex(db) } var columns []string if err := db.Raw("SELECT column_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='tab_user' AND index_name=? ORDER BY seq_in_index", index).Scan(&columns).Error; err != nil { return err } if strings.Join(columns, ",") != "source_db,puid,sw_register_time_unix,id" { return fmt.Errorf("tab_user.%s 的现有索引列不符", index) } return nil } func addRegistrationIndex(db *gorm.DB) error { return withLegacyRegistrationCast(db, func(conn *gorm.DB) error { return conn.Exec("ALTER TABLE tab_user ADD INDEX idx_sw_user_registration(source_db,puid,sw_register_time_unix,id), ALGORITHM=INPLACE, LOCK=NONE").Error }) } func withLegacyRegistrationCast(db *gorm.DB, migrate func(*gorm.DB) error) error { // 旧 SELECT CAST 会以警告方式接受尾部空格等历史值。建索引属于 DDL, // 严格模式会把同一警告升级为错误;仅在固定连接的索引构建期间保持旧转换行为。 return db.Connection(func(conn *gorm.DB) (result error) { // Keep the bound connection's base handle free of statement errors so // restoration still executes after a failed ALTER TABLE. fresh := func() *gorm.DB { return conn.Session(&gorm.Session{NewDB: true}) } var previous string if err := fresh().Raw("SELECT @@session.sql_mode").Scan(&previous).Error; err != nil { return err } modes := []string{} for _, mode := range strings.Split(previous, ",") { if mode != "STRICT_TRANS_TABLES" && mode != "STRICT_ALL_TABLES" { modes = append(modes, mode) } } if err := fresh().Exec("SET SESSION sql_mode=?", strings.Join(modes, ",")).Error; err != nil { return err } defer func() { if err := fresh().Exec("SET SESSION sql_mode=?", previous).Error; err != nil { result = errors.Join(result, err) } }() return migrate(fresh()) }) }