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:
287
backend/pkg/migration/README.md
Normal file
287
backend/pkg/migration/README.md
Normal file
@ -0,0 +1,287 @@
|
||||
# Migration Package Documentation
|
||||
|
||||
## 概述
|
||||
|
||||
这是摄影作品集项目的数据库迁移包,提供了完整的数据库版本管理和迁移功能。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"photography-backend/pkg/migration"
|
||||
"photography-backend/pkg/utils/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建数据库连接
|
||||
db, err := database.NewDB(dbConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建迁移器
|
||||
migrator := migration.NewMigrator(db)
|
||||
|
||||
// 添加迁移
|
||||
migrations := migration.GetAllMigrations()
|
||||
for _, m := range migrations {
|
||||
migrator.AddMigration(m)
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
if err := migrator.Up(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### Migrator
|
||||
|
||||
#### 创建迁移器
|
||||
|
||||
```go
|
||||
migrator := migration.NewMigrator(db)
|
||||
```
|
||||
|
||||
#### 主要方法
|
||||
|
||||
| 方法 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| `AddMigration(migration)` | 添加迁移 | `migrator.AddMigration(m)` |
|
||||
| `Up()` | 执行所有待处理迁移 | `migrator.Up()` |
|
||||
| `Down(steps)` | 回滚指定步数迁移 | `migrator.Down(2)` |
|
||||
| `Status()` | 显示迁移状态 | `migrator.Status()` |
|
||||
| `Reset()` | 重置数据库 | `migrator.Reset()` |
|
||||
| `Migrate(steps)` | 执行指定数量迁移 | `migrator.Migrate(3)` |
|
||||
|
||||
### Migration 结构
|
||||
|
||||
```go
|
||||
type Migration struct {
|
||||
Version string // 版本号 (YYYYMMDD_HHMMSS)
|
||||
Description string // 描述
|
||||
UpSQL string // 向前迁移 SQL
|
||||
DownSQL string // 回滚 SQL
|
||||
Timestamp time.Time // 时间戳
|
||||
}
|
||||
```
|
||||
|
||||
## 开发新迁移
|
||||
|
||||
### 1. 添加到 migrations.go
|
||||
|
||||
```go
|
||||
{
|
||||
Version: "20250111_120000",
|
||||
Description: "Add user avatar field",
|
||||
Timestamp: time.Date(2025, 1, 11, 12, 0, 0, 0, time.UTC),
|
||||
UpSQL: `
|
||||
ALTER TABLE user ADD COLUMN avatar VARCHAR(255) DEFAULT '';
|
||||
CREATE INDEX IF NOT EXISTS idx_user_avatar ON user(avatar);
|
||||
`,
|
||||
DownSQL: `
|
||||
DROP INDEX IF EXISTS idx_user_avatar;
|
||||
-- SQLite specific: recreate table without avatar field
|
||||
CREATE TABLE user_temp AS
|
||||
SELECT id, username, password, email, status, created_at, updated_at
|
||||
FROM user;
|
||||
DROP TABLE user;
|
||||
ALTER TABLE user_temp RENAME TO user;
|
||||
`,
|
||||
},
|
||||
```
|
||||
|
||||
### 2. 版本号规范
|
||||
|
||||
- 格式: `YYYYMMDD_HHMMSS`
|
||||
- 示例: `20250111_120000` (2025年1月11日 12:00:00)
|
||||
- 确保唯一性和时间顺序
|
||||
|
||||
### 3. SQL 编写规范
|
||||
|
||||
#### UP Migration (UpSQL)
|
||||
|
||||
```sql
|
||||
-- 使用 IF NOT EXISTS 确保幂等性
|
||||
CREATE TABLE IF NOT EXISTS new_table (...);
|
||||
ALTER TABLE user ADD COLUMN IF NOT EXISTS new_field VARCHAR(255);
|
||||
CREATE INDEX IF NOT EXISTS idx_name ON table(field);
|
||||
|
||||
-- 数据迁移使用 OR IGNORE
|
||||
INSERT OR IGNORE INTO table (field) VALUES ('value');
|
||||
UPDATE table SET field = 'value' WHERE condition;
|
||||
```
|
||||
|
||||
#### DOWN Migration (DownSQL)
|
||||
|
||||
```sql
|
||||
-- SQLite 不支持 DROP COLUMN,需要重建表
|
||||
CREATE TABLE table_temp (
|
||||
-- 列出所有保留的字段
|
||||
id INTEGER PRIMARY KEY,
|
||||
existing_field VARCHAR(255)
|
||||
);
|
||||
|
||||
INSERT INTO table_temp SELECT id, existing_field FROM table;
|
||||
DROP TABLE table;
|
||||
ALTER TABLE table_temp RENAME TO table;
|
||||
|
||||
-- 重建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_name ON table(field);
|
||||
```
|
||||
|
||||
## SQLite 特殊考虑
|
||||
|
||||
### 不支持的操作
|
||||
|
||||
SQLite 不支持以下操作:
|
||||
- `ALTER TABLE DROP COLUMN`
|
||||
- `ALTER TABLE MODIFY COLUMN`
|
||||
- `ALTER TABLE ADD CONSTRAINT`
|
||||
|
||||
### 解决方案
|
||||
|
||||
使用表重建模式:
|
||||
|
||||
```sql
|
||||
-- 1. 创建新表结构
|
||||
CREATE TABLE table_new (
|
||||
id INTEGER PRIMARY KEY,
|
||||
field1 VARCHAR(255),
|
||||
-- 新字段或修改的字段
|
||||
field2 INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- 2. 复制数据
|
||||
INSERT INTO table_new (id, field1, field2)
|
||||
SELECT id, field1, COALESCE(old_field, 0) FROM table;
|
||||
|
||||
-- 3. 删除旧表
|
||||
DROP TABLE table;
|
||||
|
||||
-- 4. 重命名新表
|
||||
ALTER TABLE table_new RENAME TO table;
|
||||
|
||||
-- 5. 重建索引和触发器
|
||||
CREATE INDEX IF NOT EXISTS idx_field1 ON table(field1);
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 单元测试
|
||||
|
||||
```go
|
||||
func TestMigration(t *testing.T) {
|
||||
// 创建内存数据库
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建迁移器
|
||||
migrator := migration.NewMigrator(db)
|
||||
|
||||
// 添加测试迁移
|
||||
migrator.AddMigration(testMigration)
|
||||
|
||||
// 测试向前迁移
|
||||
err = migrator.Up()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证表结构
|
||||
assert.True(t, db.Migrator().HasTable("test_table"))
|
||||
|
||||
// 测试回滚
|
||||
err = migrator.Down(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证回滚结果
|
||||
assert.False(t, db.Migrator().HasTable("test_table"))
|
||||
}
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
```bash
|
||||
# 在测试数据库上运行迁移
|
||||
go run cmd/migrate/main.go -f etc/test.yaml -c up
|
||||
|
||||
# 验证数据完整性
|
||||
sqlite3 test.db "SELECT COUNT(*) FROM user;"
|
||||
|
||||
# 测试回滚
|
||||
go run cmd/migrate/main.go -f etc/test.yaml -c down -s 1
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 迁移设计
|
||||
|
||||
- **小步快跑**: 每次迁移只做一件事
|
||||
- **可回滚**: 确保每个迁移都可以安全回滚
|
||||
- **向后兼容**: 新字段使用默认值,避免破坏现有功能
|
||||
- **测试优先**: 在开发环境充分测试后再部署
|
||||
|
||||
### 2. 数据安全
|
||||
|
||||
```go
|
||||
// 在重要操作前备份
|
||||
migrator.CreateBackup()
|
||||
|
||||
// 使用事务
|
||||
tx := db.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 执行迁移
|
||||
err := migrator.Up()
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
```
|
||||
|
||||
### 3. 性能优化
|
||||
|
||||
```sql
|
||||
-- 大表迁移时删除索引
|
||||
DROP INDEX idx_large_table_field;
|
||||
|
||||
-- 执行数据迁移
|
||||
UPDATE large_table SET new_field = 'value';
|
||||
|
||||
-- 重建索引
|
||||
CREATE INDEX idx_large_table_field ON large_table(field);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何处理冲突的迁移版本?
|
||||
|
||||
A: 使用时间戳版本号,确保每个版本唯一。如果出现冲突,重新生成版本号。
|
||||
|
||||
### Q: SQLite 如何删除字段?
|
||||
|
||||
A: SQLite 不支持 DROP COLUMN,需要重建表。参考上面的表重建模式。
|
||||
|
||||
### Q: 如何在生产环境安全迁移?
|
||||
|
||||
A: 1) 备份数据库 2) 在测试环境验证 3) 使用预览模式检查 4) 执行迁移 5) 验证结果
|
||||
|
||||
### Q: 迁移失败如何恢复?
|
||||
|
||||
A: 1) 使用 Down 迁移回滚 2) 从备份恢复 3) 手动修复数据
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [完整文档](../docs/DATABASE_MIGRATION.md)
|
||||
- [命令行工具](../cmd/migrate/main.go)
|
||||
- [生产脚本](../scripts/production-migrate.sh)
|
||||
- [Makefile 集成](../Makefile)
|
||||
361
backend/pkg/migration/migration.go
Normal file
361
backend/pkg/migration/migration.go
Normal file
@ -0,0 +1,361 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Migration 表示一个数据库迁移
|
||||
type Migration struct {
|
||||
Version string
|
||||
Description string
|
||||
UpSQL string
|
||||
DownSQL string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// MigrationRecord 数据库中的迁移记录
|
||||
type MigrationRecord struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Version string `gorm:"uniqueIndex;size:255;not null"`
|
||||
Description string `gorm:"size:500"`
|
||||
Applied bool `gorm:"default:false"`
|
||||
AppliedAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Migrator 迁移管理器
|
||||
type Migrator struct {
|
||||
db *gorm.DB
|
||||
migrations []Migration
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewMigrator 创建新的迁移管理器
|
||||
func NewMigrator(db *gorm.DB) *Migrator {
|
||||
return &Migrator{
|
||||
db: db,
|
||||
migrations: []Migration{},
|
||||
tableName: "schema_migrations",
|
||||
}
|
||||
}
|
||||
|
||||
// AddMigration 添加迁移
|
||||
func (m *Migrator) AddMigration(migration Migration) {
|
||||
m.migrations = append(m.migrations, migration)
|
||||
}
|
||||
|
||||
// initMigrationTable 初始化迁移表
|
||||
func (m *Migrator) initMigrationTable() error {
|
||||
// 确保迁移表存在
|
||||
if err := m.db.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||
return fmt.Errorf("failed to create migration table: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppliedMigrations 获取已应用的迁移
|
||||
func (m *Migrator) GetAppliedMigrations() ([]string, error) {
|
||||
var records []MigrationRecord
|
||||
if err := m.db.Where("applied = ?", true).Order("version ASC").Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versions := make([]string, len(records))
|
||||
for i, record := range records {
|
||||
versions[i] = record.Version
|
||||
}
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// GetPendingMigrations 获取待应用的迁移
|
||||
func (m *Migrator) GetPendingMigrations() ([]Migration, error) {
|
||||
appliedVersions, err := m.GetAppliedMigrations()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appliedMap := make(map[string]bool)
|
||||
for _, version := range appliedVersions {
|
||||
appliedMap[version] = true
|
||||
}
|
||||
|
||||
var pendingMigrations []Migration
|
||||
for _, migration := range m.migrations {
|
||||
if !appliedMap[migration.Version] {
|
||||
pendingMigrations = append(pendingMigrations, migration)
|
||||
}
|
||||
}
|
||||
|
||||
// 按版本号排序
|
||||
sort.Slice(pendingMigrations, func(i, j int) bool {
|
||||
return pendingMigrations[i].Version < pendingMigrations[j].Version
|
||||
})
|
||||
|
||||
return pendingMigrations, nil
|
||||
}
|
||||
|
||||
// Up 执行迁移(向上)
|
||||
func (m *Migrator) Up() error {
|
||||
if err := m.initMigrationTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pendingMigrations, err := m.GetPendingMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pendingMigrations) == 0 {
|
||||
log.Println("No pending migrations")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, migration := range pendingMigrations {
|
||||
log.Printf("Applying migration %s: %s", migration.Version, migration.Description)
|
||||
|
||||
// 开始事务
|
||||
tx := m.db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// 执行迁移SQL
|
||||
if err := m.executeSQL(tx, migration.UpSQL); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to apply migration %s: %v", migration.Version, err)
|
||||
}
|
||||
|
||||
// 记录迁移状态 (使用UPSERT)
|
||||
now := time.Now()
|
||||
|
||||
// 检查记录是否已存在
|
||||
var existingRecord MigrationRecord
|
||||
err := tx.Where("version = ?", migration.Version).First(&existingRecord).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// 创建新记录
|
||||
record := MigrationRecord{
|
||||
Version: migration.Version,
|
||||
Description: migration.Description,
|
||||
Applied: true,
|
||||
AppliedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to create migration record %s: %v", migration.Version, err)
|
||||
}
|
||||
} else if err == nil {
|
||||
// 更新现有记录
|
||||
updates := map[string]interface{}{
|
||||
"applied": true,
|
||||
"applied_at": now,
|
||||
"updated_at": now,
|
||||
}
|
||||
if err := tx.Model(&existingRecord).Updates(updates).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to update migration record %s: %v", migration.Version, err)
|
||||
}
|
||||
} else {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to check migration record %s: %v", migration.Version, err)
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return fmt.Errorf("failed to commit migration %s: %v", migration.Version, err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully applied migration %s", migration.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Down 回滚迁移(向下)
|
||||
func (m *Migrator) Down(steps int) error {
|
||||
if err := m.initMigrationTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appliedVersions, err := m.GetAppliedMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(appliedVersions) == 0 {
|
||||
log.Println("No applied migrations to rollback")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取要回滚的迁移(从最新开始)
|
||||
rollbackCount := steps
|
||||
if rollbackCount > len(appliedVersions) {
|
||||
rollbackCount = len(appliedVersions)
|
||||
}
|
||||
|
||||
for i := len(appliedVersions) - 1; i >= len(appliedVersions)-rollbackCount; i-- {
|
||||
version := appliedVersions[i]
|
||||
migration := m.findMigrationByVersion(version)
|
||||
if migration == nil {
|
||||
return fmt.Errorf("migration %s not found in migration definitions", version)
|
||||
}
|
||||
|
||||
log.Printf("Rolling back migration %s: %s", migration.Version, migration.Description)
|
||||
|
||||
// 开始事务
|
||||
tx := m.db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// 执行回滚SQL
|
||||
if err := m.executeSQL(tx, migration.DownSQL); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to rollback migration %s: %v", migration.Version, err)
|
||||
}
|
||||
|
||||
// 更新迁移状态
|
||||
if err := tx.Model(&MigrationRecord{}).Where("version = ?", version).Update("applied", false).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to update migration record %s: %v", migration.Version, err)
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return fmt.Errorf("failed to commit rollback %s: %v", migration.Version, err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully rolled back migration %s", migration.Version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status 显示迁移状态
|
||||
func (m *Migrator) Status() error {
|
||||
if err := m.initMigrationTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appliedVersions, err := m.GetAppliedMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appliedMap := make(map[string]bool)
|
||||
for _, version := range appliedVersions {
|
||||
appliedMap[version] = true
|
||||
}
|
||||
|
||||
// 排序所有迁移
|
||||
allMigrations := m.migrations
|
||||
sort.Slice(allMigrations, func(i, j int) bool {
|
||||
return allMigrations[i].Version < allMigrations[j].Version
|
||||
})
|
||||
|
||||
fmt.Println("Migration Status:")
|
||||
fmt.Println("Version | Status | Description")
|
||||
fmt.Println("---------------|---------|----------------------------------")
|
||||
|
||||
for _, migration := range allMigrations {
|
||||
status := "Pending"
|
||||
if appliedMap[migration.Version] {
|
||||
status = "Applied"
|
||||
}
|
||||
fmt.Printf("%-14s | %-7s | %s\n", migration.Version, status, migration.Description)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeSQL 执行SQL语句
|
||||
func (m *Migrator) executeSQL(tx *gorm.DB, sqlStr string) error {
|
||||
// 分割SQL语句(按分号分割)
|
||||
statements := strings.Split(sqlStr, ";")
|
||||
|
||||
for _, statement := range statements {
|
||||
statement = strings.TrimSpace(statement)
|
||||
if statement == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := tx.Exec(statement).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findMigrationByVersion 根据版本号查找迁移
|
||||
func (m *Migrator) findMigrationByVersion(version string) *Migration {
|
||||
for _, migration := range m.migrations {
|
||||
if migration.Version == version {
|
||||
return &migration
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset 重置数据库(谨慎使用)
|
||||
func (m *Migrator) Reset() error {
|
||||
log.Println("WARNING: This will drop all tables and reset the database!")
|
||||
|
||||
// 获取所有应用的迁移
|
||||
appliedVersions, err := m.GetAppliedMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 回滚所有迁移
|
||||
if len(appliedVersions) > 0 {
|
||||
if err := m.Down(len(appliedVersions)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 删除迁移表
|
||||
if err := m.db.Migrator().DropTable(&MigrationRecord{}); err != nil {
|
||||
return fmt.Errorf("failed to drop migration table: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Database reset completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate 执行指定数量的待处理迁移
|
||||
func (m *Migrator) Migrate(steps int) error {
|
||||
pendingMigrations, err := m.GetPendingMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pendingMigrations) == 0 {
|
||||
log.Println("No pending migrations")
|
||||
return nil
|
||||
}
|
||||
|
||||
migrateCount := steps
|
||||
if steps <= 0 || steps > len(pendingMigrations) {
|
||||
migrateCount = len(pendingMigrations)
|
||||
}
|
||||
|
||||
// 临时修改migrations列表,只包含要执行的迁移
|
||||
originalMigrations := m.migrations
|
||||
m.migrations = pendingMigrations[:migrateCount]
|
||||
|
||||
err = m.Up()
|
||||
|
||||
// 恢复原始migrations列表
|
||||
m.migrations = originalMigrations
|
||||
|
||||
return err
|
||||
}
|
||||
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