Files
nl-blogs/server/repositories/migration.go
2026-07-04 17:58:19 +08:00

296 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repositories
import (
"fmt"
"log"
"time"
"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`")
}
}
// MigrateVideoModule 创建视频模块表及作品视频字段
func MigrateVideoModule() {
log.Printf("Migrating video module...")
if !tableExists("video_categories") {
execSQL(`CREATE TABLE video_categories (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL,
description TEXT NULL,
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
deleted_at BIGINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE INDEX idx_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频分类表'`)
}
if !tableExists("video_albums") {
execSQL(`CREATE TABLE video_albums (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
description TEXT NULL,
cover VARCHAR(500) NULL,
category_id INT UNSIGNED NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
deleted_at BIGINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
INDEX idx_category_id (category_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频专辑表'`)
}
if !tableExists("videos") {
execSQL(`CREATE TABLE videos (
id VARCHAR(50) NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT NULL,
video_url VARCHAR(500) NOT NULL,
cover VARCHAR(500) NULL,
poster VARCHAR(500) NULL,
category_id INT UNSIGNED NOT NULL DEFAULT 0,
duration INT NOT NULL DEFAULT 0,
is_published TINYINT(1) NOT NULL DEFAULT 1,
deleted_at BIGINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (id),
INDEX idx_category_id (category_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频表'`)
}
if !tableExists("album_videos") {
execSQL(`CREATE TABLE album_videos (
album_id INT UNSIGNED NOT NULL,
video_id VARCHAR(50) NOT NULL,
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (album_id, video_id),
INDEX idx_video_id (video_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='专辑视频关联表'`)
}
if !columnExists("works", "hero_video") {
execSQL("ALTER TABLE `works` ADD COLUMN `hero_video` VARCHAR(500) NULL DEFAULT NULL COMMENT '演示视频URL' AFTER `hero_img`")
}
if !columnExists("works", "video_id") {
execSQL("ALTER TABLE `works` ADD COLUMN `video_id` VARCHAR(50) NULL DEFAULT NULL COMMENT '关联视频库ID' AFTER `hero_video`")
}
// 插入 videos 权限(若不存在)
now := time.Now().Unix()
actions := []string{"read", "create", "update", "delete"}
for _, action := range actions {
var count int64
config.DB.Raw("SELECT COUNT(*) FROM permissions WHERE resource = ? AND action = ? AND deleted_at = 0", "videos", action).Scan(&count)
if count == 0 {
execSQL(fmt.Sprintf("INSERT INTO permissions (name, resource, action, deleted_at, created_at, updated_at) VALUES ('videos:%s', 'videos', '%s', 0, %d, %d)", action, action, now, now))
}
}
// 为 role_id=1 的管理员角色授予 videos 权限
type permRow struct{ ID uint }
var rows []permRow
config.DB.Raw("SELECT id FROM permissions WHERE resource = 'videos' AND deleted_at = 0").Scan(&rows)
for _, row := range rows {
var count int64
config.DB.Raw("SELECT COUNT(*) FROM role_permissions WHERE role_id = 1 AND permission_id = ?", row.ID).Scan(&count)
if count == 0 {
execSQL(fmt.Sprintf("INSERT INTO role_permissions (role_id, permission_id) VALUES (1, %d)", row.ID))
}
}
}