package repositories import ( "fmt" "log" "github.com/niangaodev/art-code/config" ) // MigrateToBigInt performs schema migration to BigInt timestamps func MigrateToBigInt() { tables := []string{ "posts", "users", "tags", "works", "snippets", "settings", "about_profiles", "partners", "testimonials", "inquiries", "email_suffixes", "access_logs", "user_access_logs", "operation_logs", "permissions", "roles", "work_tech_stack", "work_gallery", "post_tags", "post_history", "code_types", } for _, table := range tables { log.Printf("Migrating table: %s", table) // 1. Add deleted_at if not exists if !columnExists(table, "deleted_at") && table != "post_tags" { // post_tags doesn't need deleted_at in my previous plan? I added to all in SQL file? Yes. // Actually post_tags in SQL file I modified: `created_at` bigint. No `deleted_at`. // So skip deleted_at for post_tags. if table != "post_tags" { execSQL(fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN `deleted_at` BIGINT NOT NULL DEFAULT 0", table)) } } // 2. Migrate created_at migrateColumn(table, "created_at") // 3. Migrate updated_at (if exists) if columnExists(table, "updated_at") { migrateColumn(table, "updated_at") } // 4. Migrate access_time (for user_access_logs) if table == "user_access_logs" && columnExists(table, "access_time") { migrateColumn(table, "access_time") } // 5. Remove 'date' from posts if exists if table == "posts" && columnExists(table, "date") { execSQL("ALTER TABLE `posts` DROP COLUMN `date`") // Drop index idx_date if exists? MySQL drops index on column drop usually. } } } func columnExists(tableName, colName string) bool { var count int64 err := config.DB.Raw(` SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ? `, tableName, colName).Scan(&count).Error if err != nil { log.Printf("Error checking column %s.%s: %v", tableName, colName, err) return false } return count > 0 } func isBigInt(tableName, colName string) bool { var dataType string err := config.DB.Raw(` SELECT DATA_TYPE FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ? `, tableName, colName).Scan(&dataType).Error if err != nil { return false } return dataType == "bigint" || dataType == "int" } func migrateColumn(table, col string) { if !columnExists(table, col) { return } if isBigInt(table, col) { return // Already migrated } log.Printf("Converting %s.%s to BIGINT...", table, col) // Rename old oldCol := col + "_old_dt" execSQL(fmt.Sprintf("ALTER TABLE `%s` CHANGE `%s` `%s` DATETIME", table, col, oldCol)) // Ensure it's treated as datetime for rename // Add new execSQL(fmt.Sprintf("ALTER TABLE `%s` ADD COLUMN `%s` BIGINT NOT NULL DEFAULT 0", table, col)) // Copy and Convert execSQL(fmt.Sprintf("UPDATE `%s` SET `%s` = UNIX_TIMESTAMP(`%s`) WHERE `%s` IS NOT NULL", table, col, oldCol, oldCol)) // Drop old execSQL(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN `%s`", table, oldCol)) } func execSQL(query string) { err := config.DB.Exec(query).Error if err != nil { // Log but continue (might fail if column doesn't exist etc) log.Printf("SQL Error: %v | Query: %s", err, query) } } // MigratePostHistoryFields adds column_id and tag_ids to post_history if missing. func MigratePostHistoryFields() { if !columnExists("post_history", "column_id") { log.Printf("Adding post_history.column_id...") execSQL("ALTER TABLE `post_history` ADD COLUMN `column_id` INT UNSIGNED NULL AFTER `category_id`") } if !columnExists("post_history", "tag_ids") { log.Printf("Adding post_history.tag_ids...") execSQL("ALTER TABLE `post_history` ADD COLUMN `tag_ids` JSON NULL COMMENT '标签ID快照' AFTER `column_id`") } } func tableExists(tableName string) bool { var count int64 err := config.DB.Raw(` SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? `, tableName).Scan(&count).Error if err != nil { log.Printf("Error checking table %s: %v", tableName, err) return false } return count > 0 } // MigratePostSnippets creates post_snippets junction table if missing. func MigratePostSnippets() { if tableExists("post_snippets") { return } log.Printf("Creating post_snippets table...") execSQL(`CREATE TABLE post_snippets ( post_id INT UNSIGNED NOT NULL COMMENT '文章ID', snippet_id BIGINT UNSIGNED NOT NULL COMMENT '代码片段ID', sort_order INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)', created_at BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)', PRIMARY KEY (post_id, snippet_id), INDEX idx_snippet_id (snippet_id), INDEX idx_sort_order (sort_order) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文章代码片段关联表'`) } // MigratePostUserID adds user_id to posts if missing. func MigratePostUserID() { if columnExists("posts", "user_id") { return } log.Printf("Adding posts.user_id...") execSQL("ALTER TABLE `posts` ADD COLUMN `user_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建者用户ID' AFTER `is_published`") execSQL("ALTER TABLE `posts` ADD INDEX `idx_user_id` (`user_id`)") } // MigrateUserAvatar adds avatar to users if missing. func MigrateUserAvatar() { if columnExists("users", "avatar") { return } log.Printf("Adding users.avatar...") execSQL("ALTER TABLE `users` ADD COLUMN `avatar` VARCHAR(500) NULL DEFAULT NULL COMMENT '头像URL' AFTER `email`") } // MigrateUserProfileFields adds profile fields to users if missing. func MigrateUserProfileFields() { if !columnExists("users", "bio") { log.Printf("Adding users.bio...") execSQL("ALTER TABLE `users` ADD COLUMN `bio` TEXT NULL COMMENT '个人介绍' AFTER `avatar`") } if !columnExists("users", "phone") { log.Printf("Adding users.phone...") execSQL("ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(20) NULL DEFAULT NULL COMMENT '手机号' AFTER `bio`") } if !columnExists("users", "wechat") { log.Printf("Adding users.wechat...") execSQL("ALTER TABLE `users` ADD COLUMN `wechat` VARCHAR(100) NULL DEFAULT NULL COMMENT '微信号' AFTER `phone`") } if !columnExists("users", "wechat_qrcode") { log.Printf("Adding users.wechat_qrcode...") execSQL("ALTER TABLE `users` ADD COLUMN `wechat_qrcode` VARCHAR(500) NULL DEFAULT NULL COMMENT '微信二维码图片URL' AFTER `wechat`") } }