Files
nl-blogs/server/repositories/migration.go

399 lines
17 KiB
Go
Raw Normal View History

2026-01-16 13:01:21 +08:00
package repositories
import (
"fmt"
"log"
2026-07-04 17:58:19 +08:00
"time"
2026-01-16 13:01:21 +08:00
"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",
2026-06-24 17:06:22 +08:00
"work_tech_stack", "work_gallery", "post_tags", "post_history", "code_types",
2026-01-16 13:01:21 +08:00
}
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 {
2026-01-19 13:53:32 +08:00
var count int64
err := config.DB.Raw(`
2026-01-16 13:01:21 +08:00
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?
2026-01-19 13:53:32 +08:00
`, tableName, colName).Scan(&count).Error
2026-01-16 13:01:21 +08:00
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 {
2026-01-19 13:53:32 +08:00
var dataType string
err := config.DB.Raw(`
2026-01-16 13:01:21 +08:00
SELECT DATA_TYPE
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = ?
2026-01-19 13:53:32 +08:00
`, tableName, colName).Scan(&dataType).Error
2026-01-16 13:01:21 +08:00
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) {
2026-01-19 13:53:32 +08:00
err := config.DB.Exec(query).Error
2026-01-16 13:01:21 +08:00
if err != nil {
// Log but continue (might fail if column doesn't exist etc)
log.Printf("SQL Error: %v | Query: %s", err, query)
}
}
2026-06-25 16:57:06 +08:00
// 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='文章代码片段关联表'`)
}
2026-06-25 17:57:51 +08:00
// 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`")
}
2026-06-26 17:00:02 +08:00
// 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`")
}
}
2026-07-04 17:58:19 +08:00
2026-07-30 14:11:26 +08:00
// MigrateUserWxOpenID adds wx_openid for mini-program silent login.
func MigrateUserWxOpenID() {
if columnExists("users", "wx_openid") {
return
}
log.Printf("Adding users.wx_openid...")
execSQL("ALTER TABLE `users` ADD COLUMN `wx_openid` VARCHAR(64) NULL DEFAULT NULL COMMENT '微信小程序openid' AFTER `wechat_qrcode`")
execSQL("CREATE UNIQUE INDEX `idx_users_wx_openid` ON `users` (`wx_openid`)")
}
2026-07-04 17:58:19 +08:00
// 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))
}
}
}
2026-07-14 09:37:13 +08:00
// MigratePptTemplates 创建 PPT 模板表并补种系统默认模板
func MigratePptTemplates() {
log.Printf("Migrating ppt templates...")
if !tableExists("ppt_templates") {
execSQL(`CREATE TABLE ppt_templates (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '模板ID',
name VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板显示名称',
slug VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'URL唯一标识(系统默认为default)',
description TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '模板描述',
config LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'PPT样式配置JSON(配色/动画/切换/UI)',
is_system TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否系统内置模板(0否 1是,不可删除)',
is_default TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否演示默认模板(0否 1是)',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)',
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(0否 1是)',
deleted_at BIGINT NOT NULL DEFAULT 0 COMMENT '软删除时间戳',
created_at BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)',
updated_at BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)',
PRIMARY KEY (id),
UNIQUE INDEX idx_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PPT模板表'`)
} else {
ensurePptTemplateColumnComments()
}
EnsureSystemPptTemplate()
}
2026-07-14 10:05:33 +08:00
// MigrateOSSConfigFields 为 oss_configs 补齐各云厂商专用列(幂等)
func MigrateOSSConfigFields() {
log.Printf("Migrating oss_configs provider fields...")
if !tableExists("oss_configs") {
log.Printf("Table oss_configs does not exist, skip MigrateOSSConfigFields")
return
}
columns := []struct {
name string
ddl string
}{
{"oss_access_key_id", "ALTER TABLE `oss_configs` ADD COLUMN `oss_access_key_id` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 AccessKeyId(AES加密)' AFTER `domain`"},
{"oss_access_key_secret", "ALTER TABLE `oss_configs` ADD COLUMN `oss_access_key_secret` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 AccessKeySecret(AES加密)' AFTER `oss_access_key_id`"},
{"oss_endpoint", "ALTER TABLE `oss_configs` ADD COLUMN `oss_endpoint` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 Endpoint' AFTER `oss_access_key_secret`"},
{"oss_bucket", "ALTER TABLE `oss_configs` ADD COLUMN `oss_bucket` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云 Bucket' AFTER `oss_endpoint`"},
{"oss_domain", "ALTER TABLE `oss_configs` ADD COLUMN `oss_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '阿里云自定义域名' AFTER `oss_bucket`"},
{"qcloud_secret_id", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_secret_id` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 SecretId(AES加密)' AFTER `oss_domain`"},
{"qcloud_secret_key", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_secret_key` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 SecretKey(AES加密)' AFTER `qcloud_secret_id`"},
{"qcloud_region", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_region` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 Region' AFTER `qcloud_secret_key`"},
{"qcloud_bucket", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_bucket` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云 Bucket' AFTER `qcloud_region`"},
{"qcloud_domain", "ALTER TABLE `oss_configs` ADD COLUMN `qcloud_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '腾讯云自定义域名' AFTER `qcloud_bucket`"},
{"qiniu_access_key", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_access_key` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 AccessKey(AES加密)' AFTER `qcloud_domain`"},
{"qiniu_secret_key", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_secret_key` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 SecretKey(AES加密)' AFTER `qiniu_access_key`"},
{"qiniu_bucket", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_bucket` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 Bucket' AFTER `qiniu_secret_key`"},
{"qiniu_region", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_region` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛 Region' AFTER `qiniu_bucket`"},
{"qiniu_domain", "ALTER TABLE `oss_configs` ADD COLUMN `qiniu_domain` varchar(255) NOT NULL DEFAULT '' COMMENT '七牛自定义域名' AFTER `qiniu_region`"},
}
for _, col := range columns {
if !columnExists("oss_configs", col.name) {
log.Printf("Adding oss_configs.%s...", col.name)
execSQL(col.ddl)
}
}
}
2026-07-14 09:37:13 +08:00
// ensurePptTemplateColumnComments 为已存在但缺少字段备注的 ppt_templates 表补全 COMMENT
func ensurePptTemplateColumnComments() {
if !tableExists("ppt_templates") {
return
}
alters := []string{
"ALTER TABLE `ppt_templates` MODIFY COLUMN `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '模板ID'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板显示名称'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'URL唯一标识(系统默认为default)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '模板描述'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `config` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'PPT样式配置JSON(配色/动画/切换/UI)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `is_system` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否系统内置模板(0否 1是,不可删除)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否演示默认模板(0否 1是)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用(0否 1是)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `deleted_at` bigint NOT NULL DEFAULT 0 COMMENT '软删除时间戳'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `created_at` bigint NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `updated_at` bigint NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)'",
"ALTER TABLE `ppt_templates` COMMENT='PPT模板表'",
}
for _, sql := range alters {
execSQL(sql)
}
}