package initialize import ( "fmt" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/logger" "github.com/pkg/errors" "gorm.io/gorm" ) type schemaTablePatch struct { table string ddl string } type schemaColumnPatch struct { table string column string ddl string } func EnsureSchemaCompatibility(ctx *svc.ServiceContext) error { tablePatches := []schemaTablePatch{ { table: "log_message", ddl: `CREATE TABLE IF NOT EXISTS log_message ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, platform VARCHAR(32) NOT NULL, app_version VARCHAR(32) NULL, os_name VARCHAR(32) NULL, os_version VARCHAR(32) NULL, device_id VARCHAR(64) NULL, user_id BIGINT NULL DEFAULT NULL, session_id VARCHAR(64) NULL, level TINYINT UNSIGNED NOT NULL DEFAULT 3, error_code VARCHAR(64) NULL, message TEXT NOT NULL, stack MEDIUMTEXT NULL, context JSON NULL, client_ip VARCHAR(45) NULL, user_agent VARCHAR(255) NULL, locale VARCHAR(16) NULL, digest VARCHAR(64) NULL, occurred_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_digest (digest), KEY idx_platform_time (platform, created_at), KEY idx_user_time (user_id, created_at), KEY idx_device_time (device_id, created_at), KEY idx_error_code (error_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, }, { table: "user_family", ddl: `CREATE TABLE IF NOT EXISTS user_family ( id BIGINT NOT NULL AUTO_INCREMENT, owner_user_id BIGINT NOT NULL COMMENT 'Owner User ID', max_members INT NOT NULL DEFAULT 5 COMMENT 'Max members in family', status TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=active, 0=disabled', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), deleted_at DATETIME(3) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_owner_user_id (owner_user_id), KEY idx_status (status), KEY idx_deleted_at (deleted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, }, { table: "user_family_member", ddl: `CREATE TABLE IF NOT EXISTS user_family_member ( id BIGINT NOT NULL AUTO_INCREMENT, family_id BIGINT NOT NULL COMMENT 'Family ID', user_id BIGINT NOT NULL COMMENT 'Member User ID', role TINYINT NOT NULL DEFAULT 2 COMMENT 'Role: 1=owner, 2=member', status TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=active, 2=left, 3=removed', join_source VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Join source', joined_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), left_at DATETIME(3) DEFAULT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), deleted_at DATETIME(3) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_user_id (user_id), KEY idx_family_status (family_id, status), KEY idx_deleted_at (deleted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`, }, } columnPatches := []schemaColumnPatch{ { table: "user", column: "rules", ddl: "ALTER TABLE `user` ADD COLUMN `rules` TEXT NULL COMMENT 'User rules for subscription';", }, { table: "user", column: "last_login_time", ddl: "ALTER TABLE `user` ADD COLUMN `last_login_time` DATETIME DEFAULT NULL COMMENT 'Last Login Time';", }, { table: "user", column: "member_status", ddl: "ALTER TABLE `user` ADD COLUMN `member_status` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'Member Status';", }, { table: "user", column: "remark", ddl: "ALTER TABLE `user` ADD COLUMN `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Remark';", }, { table: "user_subscribe", column: "note", ddl: "ALTER TABLE `user_subscribe` ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'User note for subscription';", }, { table: "redemption_code", column: "status", ddl: "ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=enabled, 0=disabled';", }, } for _, patch := range tablePatches { exists, err := tableExists(ctx.DB, patch.table) if err != nil { return errors.Wrapf(err, "check table %s failed", patch.table) } if exists { continue } if err = ctx.DB.Exec(patch.ddl).Error; err != nil { return errors.Wrapf(err, "create table %s failed", patch.table) } logger.Infof("[SchemaCompat] created missing table: %s", patch.table) } for _, patch := range columnPatches { exists, err := columnExists(ctx.DB, patch.table, patch.column) if err != nil { return errors.Wrapf(err, "check column %s.%s failed", patch.table, patch.column) } if exists { continue } if err = ctx.DB.Exec(patch.ddl).Error; err != nil { return errors.Wrapf(err, "add column %s.%s failed", patch.table, patch.column) } logger.Infof("[SchemaCompat] added missing column: %s.%s", patch.table, patch.column) } return nil } func tableExists(db *gorm.DB, table string) (bool, error) { var count int64 err := db.Raw("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", table).Scan(&count).Error if err != nil { return false, err } return count > 0, nil } func columnExists(db *gorm.DB, table, column string) (bool, error) { var count int64 err := db.Raw( "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?", table, column, ).Scan(&count).Error if err != nil { return false, err } return count > 0, nil } func _schemaCompatDebug(table, column string) string { if column == "" { return table } return fmt.Sprintf("%s.%s", table, column) }