feat: 完成数据库迁移系统开发
- 创建完整的迁移框架 (pkg/migration/) - 版本管理系统,时间戳版本号 (YYYYMMDD_HHMMSS) - 事务安全的上下迁移机制 (Up/Down) - 迁移状态跟踪和记录 (migration_records 表) - 命令行迁移工具 (cmd/migrate/main.go) - 生产环境迁移脚本 (scripts/production-migrate.sh) - 生产环境初始化脚本 (scripts/init-production-db.sh) - 迁移测试脚本 (scripts/test-migration.sh) - Makefile 集成 (migrate-up, migrate-down, migrate-status) - 5个预定义迁移 (基础表、默认数据、元数据、收藏、用户资料) - 自动备份机制、预览模式、详细日志 - 完整文档 (docs/DATABASE_MIGRATION.md) 任务13完成,项目完成率达到42.5%
This commit is contained in:
321
backend/pkg/migration/migrations.go
Normal file
321
backend/pkg/migration/migrations.go
Normal file
@ -0,0 +1,321 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetAllMigrations 返回所有迁移定义
|
||||
func GetAllMigrations() []Migration {
|
||||
return []Migration{
|
||||
{
|
||||
Version: "20250101_000001",
|
||||
Description: "Create initial tables - users, categories, photos",
|
||||
Timestamp: time.Date(2025, 1, 1, 0, 0, 1, 0, time.UTC),
|
||||
UpSQL: `
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
avatar VARCHAR(255) DEFAULT '',
|
||||
status INTEGER DEFAULT 1, -- 1:启用 0:禁用
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 分类表
|
||||
CREATE TABLE IF NOT EXISTS category (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 照片表
|
||||
CREATE TABLE IF NOT EXISTS photo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
thumbnail_path VARCHAR(500),
|
||||
user_id INTEGER NOT NULL,
|
||||
category_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
FOREIGN KEY (category_id) REFERENCES category(id)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_username ON user(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_email ON user(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_user_id ON photo(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_category_id ON photo(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_created_at ON photo(created_at);
|
||||
`,
|
||||
DownSQL: `
|
||||
-- 删除索引
|
||||
DROP INDEX IF EXISTS idx_photo_created_at;
|
||||
DROP INDEX IF EXISTS idx_photo_category_id;
|
||||
DROP INDEX IF EXISTS idx_photo_user_id;
|
||||
DROP INDEX IF EXISTS idx_user_email;
|
||||
DROP INDEX IF EXISTS idx_user_username;
|
||||
|
||||
-- 删除表(注意外键约束,先删除依赖表)
|
||||
DROP TABLE IF EXISTS photo;
|
||||
DROP TABLE IF EXISTS category;
|
||||
DROP TABLE IF EXISTS user;
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: "20250101_000002",
|
||||
Description: "Insert default admin user and categories",
|
||||
Timestamp: time.Date(2025, 1, 1, 0, 0, 2, 0, time.UTC),
|
||||
UpSQL: `
|
||||
-- 插入默认管理员用户
|
||||
INSERT OR IGNORE INTO user (username, password, email, avatar, status)
|
||||
VALUES ('admin', '$2a$10$K8H7YjN5hOcE0zWTz1YuAuYqFyQ9cqUdFHJgJdKxA5wGv3LUQHgKq', 'admin@example.com', '', 1);
|
||||
-- 密码是 admin123 的 bcrypt 哈希
|
||||
|
||||
-- 插入默认分类
|
||||
INSERT OR IGNORE INTO category (name, description)
|
||||
VALUES
|
||||
('风景', '自然风景摄影作品'),
|
||||
('人像', '人物肖像摄影作品'),
|
||||
('建筑', '建筑摄影作品'),
|
||||
('街拍', '街头摄影作品');
|
||||
`,
|
||||
DownSQL: `
|
||||
-- 删除默认数据(保留用户数据的完整性)
|
||||
DELETE FROM photo WHERE category_id IN (SELECT id FROM category WHERE name IN ('风景', '人像', '建筑', '街拍'));
|
||||
DELETE FROM category WHERE name IN ('风景', '人像', '建筑', '街拍');
|
||||
DELETE FROM user WHERE username = 'admin' AND email = 'admin@example.com';
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: "20250111_000001",
|
||||
Description: "Add photo metadata fields - EXIF data, tags, location",
|
||||
Timestamp: time.Date(2025, 1, 11, 0, 0, 1, 0, time.UTC),
|
||||
UpSQL: `
|
||||
-- 为照片表添加元数据字段
|
||||
ALTER TABLE photo ADD COLUMN exif_data TEXT DEFAULT '{}';
|
||||
ALTER TABLE photo ADD COLUMN tags VARCHAR(500) DEFAULT '';
|
||||
ALTER TABLE photo ADD COLUMN location VARCHAR(200) DEFAULT '';
|
||||
ALTER TABLE photo ADD COLUMN camera_model VARCHAR(100) DEFAULT '';
|
||||
ALTER TABLE photo ADD COLUMN lens_model VARCHAR(100) DEFAULT '';
|
||||
ALTER TABLE photo ADD COLUMN focal_length INTEGER DEFAULT 0;
|
||||
ALTER TABLE photo ADD COLUMN aperture VARCHAR(10) DEFAULT '';
|
||||
ALTER TABLE photo ADD COLUMN shutter_speed VARCHAR(20) DEFAULT '';
|
||||
ALTER TABLE photo ADD COLUMN iso INTEGER DEFAULT 0;
|
||||
ALTER TABLE photo ADD COLUMN flash_used BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE photo ADD COLUMN orientation INTEGER DEFAULT 1;
|
||||
|
||||
-- 创建标签索引(用于搜索)
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_tags ON photo(tags);
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_location ON photo(location);
|
||||
`,
|
||||
DownSQL: `
|
||||
-- 删除索引
|
||||
DROP INDEX IF EXISTS idx_photo_location;
|
||||
DROP INDEX IF EXISTS idx_photo_tags;
|
||||
|
||||
-- 注意:SQLite 不支持 DROP COLUMN,需要重建表
|
||||
-- 创建临时表(不包含新添加的列)
|
||||
CREATE TABLE photo_temp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
thumbnail_path VARCHAR(500),
|
||||
user_id INTEGER NOT NULL,
|
||||
category_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
FOREIGN KEY (category_id) REFERENCES category(id)
|
||||
);
|
||||
|
||||
-- 复制数据
|
||||
INSERT INTO photo_temp (id, title, description, file_path, thumbnail_path, user_id, category_id, created_at, updated_at)
|
||||
SELECT id, title, description, file_path, thumbnail_path, user_id, category_id, created_at, updated_at FROM photo;
|
||||
|
||||
-- 删除原表
|
||||
DROP TABLE photo;
|
||||
|
||||
-- 重命名临时表
|
||||
ALTER TABLE photo_temp RENAME TO photo;
|
||||
|
||||
-- 重新创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_user_id ON photo(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_category_id ON photo(category_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_photo_created_at ON photo(created_at);
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: "20250111_000002",
|
||||
Description: "Create photo collections and favorites system",
|
||||
Timestamp: time.Date(2025, 1, 11, 0, 0, 2, 0, time.UTC),
|
||||
UpSQL: `
|
||||
-- 照片集合表
|
||||
CREATE TABLE IF NOT EXISTS collection (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
cover_photo_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
FOREIGN KEY (cover_photo_id) REFERENCES photo(id)
|
||||
);
|
||||
|
||||
-- 照片集合关联表(多对多)
|
||||
CREATE TABLE IF NOT EXISTS collection_photo (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
collection_id INTEGER NOT NULL,
|
||||
photo_id INTEGER NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (collection_id) REFERENCES collection(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (photo_id) REFERENCES photo(id) ON DELETE CASCADE,
|
||||
UNIQUE(collection_id, photo_id)
|
||||
);
|
||||
|
||||
-- 用户收藏表
|
||||
CREATE TABLE IF NOT EXISTS user_favorite (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
photo_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (photo_id) REFERENCES photo(id) ON DELETE CASCADE,
|
||||
UNIQUE(user_id, photo_id)
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_user_id ON collection(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_public ON collection(is_public);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_photo_collection ON collection_photo(collection_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_collection_photo_photo ON collection_photo(photo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_favorite_user ON user_favorite(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_favorite_photo ON user_favorite(photo_id);
|
||||
`,
|
||||
DownSQL: `
|
||||
-- 删除索引
|
||||
DROP INDEX IF EXISTS idx_user_favorite_photo;
|
||||
DROP INDEX IF EXISTS idx_user_favorite_user;
|
||||
DROP INDEX IF EXISTS idx_collection_photo_photo;
|
||||
DROP INDEX IF EXISTS idx_collection_photo_collection;
|
||||
DROP INDEX IF EXISTS idx_collection_public;
|
||||
DROP INDEX IF EXISTS idx_collection_user_id;
|
||||
|
||||
-- 删除表
|
||||
DROP TABLE IF EXISTS user_favorite;
|
||||
DROP TABLE IF EXISTS collection_photo;
|
||||
DROP TABLE IF EXISTS collection;
|
||||
`,
|
||||
},
|
||||
{
|
||||
Version: "20250111_000003",
|
||||
Description: "Add user profile and preferences",
|
||||
Timestamp: time.Date(2025, 1, 11, 0, 0, 3, 0, time.UTC),
|
||||
UpSQL: `
|
||||
-- 为用户表添加更多字段
|
||||
ALTER TABLE user ADD COLUMN bio TEXT DEFAULT '';
|
||||
ALTER TABLE user ADD COLUMN website VARCHAR(255) DEFAULT '';
|
||||
ALTER TABLE user ADD COLUMN location VARCHAR(100) DEFAULT '';
|
||||
ALTER TABLE user ADD COLUMN birth_date DATE;
|
||||
ALTER TABLE user ADD COLUMN phone VARCHAR(20) DEFAULT '';
|
||||
ALTER TABLE user ADD COLUMN is_verified BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE user ADD COLUMN last_login_at DATETIME;
|
||||
|
||||
-- 用户设置表
|
||||
CREATE TABLE IF NOT EXISTS user_setting (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
theme VARCHAR(20) DEFAULT 'light', -- light, dark, auto
|
||||
language VARCHAR(10) DEFAULT 'zh-CN',
|
||||
timezone VARCHAR(50) DEFAULT 'Asia/Shanghai',
|
||||
email_notifications BOOLEAN DEFAULT TRUE,
|
||||
public_profile BOOLEAN DEFAULT TRUE,
|
||||
show_exif BOOLEAN DEFAULT TRUE,
|
||||
watermark_enabled BOOLEAN DEFAULT FALSE,
|
||||
watermark_text VARCHAR(100) DEFAULT '',
|
||||
watermark_position VARCHAR(20) DEFAULT 'bottom-right',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 为新字段创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_verified ON user(is_verified);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_last_login ON user(last_login_at);
|
||||
`,
|
||||
DownSQL: `
|
||||
-- 删除索引
|
||||
DROP INDEX IF EXISTS idx_user_last_login;
|
||||
DROP INDEX IF EXISTS idx_user_verified;
|
||||
|
||||
-- 删除用户设置表
|
||||
DROP TABLE IF EXISTS user_setting;
|
||||
|
||||
-- SQLite 不支持 DROP COLUMN,需要重建用户表
|
||||
CREATE TABLE user_temp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
avatar VARCHAR(255) DEFAULT '',
|
||||
status INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 复制原始数据
|
||||
INSERT INTO user_temp (id, username, password, email, avatar, status, created_at, updated_at)
|
||||
SELECT id, username, password, email, avatar, status, created_at, updated_at FROM user;
|
||||
|
||||
-- 删除原表
|
||||
DROP TABLE user;
|
||||
|
||||
-- 重命名临时表
|
||||
ALTER TABLE user_temp RENAME TO user;
|
||||
|
||||
-- 重新创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_user_username ON user(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_email ON user(email);
|
||||
`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetMigrationByVersion 根据版本号获取迁移
|
||||
func GetMigrationByVersion(version string) *Migration {
|
||||
migrations := GetAllMigrations()
|
||||
for _, migration := range migrations {
|
||||
if migration.Version == version {
|
||||
return &migration
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatestMigrationVersion 获取最新的迁移版本
|
||||
func GetLatestMigrationVersion() string {
|
||||
migrations := GetAllMigrations()
|
||||
if len(migrations) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
latest := migrations[0]
|
||||
for _, migration := range migrations {
|
||||
if migration.Version > latest.Version {
|
||||
latest = migration
|
||||
}
|
||||
}
|
||||
|
||||
return latest.Version
|
||||
}
|
||||
Reference in New Issue
Block a user