Files
ai_english/serve/internal/database/migrate.go

172 lines
5.8 KiB
Go
Raw Permalink Normal View History

2025-11-17 13:39:05 +08:00
package database
import (
"log"
"os"
"path/filepath"
"strings"
"github.com/Nanqipro/YunQue-Tech-Projects/ai_english_learning/serve/internal/models"
"gorm.io/gorm"
)
// AutoMigrate 自动迁移数据库表结构
func AutoMigrate(db *gorm.DB) error {
log.Println("开始数据库迁移...")
// 用户相关表
err := db.AutoMigrate(
&models.User{},
&models.UserSocialLink{},
&models.UserPreference{},
)
if err != nil {
return err
}
// 词汇相关表迁移由 SQL 脚本维护,避免与既有外键/类型冲突
// (跳过 Vocabulary* / UserVocabularyProgress / VocabularyTest 的 AutoMigrate
// 词汇书相关表(新增)
err = db.AutoMigrate(
&models.VocabularyBook{},
&models.VocabularyBookWord{},
)
if err != nil {
return err
}
// 学习相关表
err = db.AutoMigrate(
&models.Notification{},
&models.StudyPlan{},
&models.StudyPlanRecord{},
&models.ListeningMaterial{},
&models.ListeningRecord{},
&models.ReadingMaterial{},
&models.ReadingRecord{},
&models.WritingPrompt{},
&models.WritingSubmission{},
&models.SpeakingScenario{},
&models.SpeakingRecord{},
)
if err != nil {
return err
}
log.Println("数据库迁移完成")
return nil
}
// CreateIndexes 创建额外的索引
func CreateIndexes(db *gorm.DB) error {
log.Println("开始创建索引...")
// 创建索引的辅助函数
createIndexIfNotExists := func(indexName, tableName, columns string) {
// 检查索引是否存在
var count int64
db.Raw("SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?", tableName, indexName).Scan(&count)
if count == 0 {
// 索引不存在,创建索引
sql := "CREATE INDEX " + indexName + " ON " + tableName + "(" + columns + ")"
result := db.Exec(sql)
if result.Error != nil {
log.Printf("创建索引 %s 失败: %v", indexName, result.Error)
}
}
}
// 用户表索引
createIndexIfNotExists("idx_users_email_verified", "ai_users", "email_verified")
createIndexIfNotExists("idx_users_status", "ai_users", "status")
createIndexIfNotExists("idx_users_created_at", "ai_users", "created_at")
// 词汇表索引
createIndexIfNotExists("idx_vocabulary_level", "ai_vocabulary", "level")
createIndexIfNotExists("idx_vocabulary_frequency", "ai_vocabulary", "frequency")
createIndexIfNotExists("idx_vocabulary_is_active", "ai_vocabulary", "is_active")
// 用户词汇进度索引
createIndexIfNotExists("idx_user_vocabulary_progress_user_vocab", "ai_user_vocabulary_progress", "user_id, vocabulary_id")
createIndexIfNotExists("idx_user_vocabulary_progress_mastery", "ai_user_vocabulary_progress", "mastery_level")
createIndexIfNotExists("idx_user_vocabulary_progress_next_review", "ai_user_vocabulary_progress", "next_review_at")
// 学习记录索引
createIndexIfNotExists("idx_listening_records_user_material", "ai_listening_records", "user_id, material_id")
createIndexIfNotExists("idx_reading_records_user_material", "ai_reading_records", "user_id, material_id")
createIndexIfNotExists("idx_writing_submissions_user_prompt", "ai_writing_submissions", "user_id, prompt_id")
createIndexIfNotExists("idx_speaking_records_user_scenario", "ai_speaking_records", "user_id, scenario_id")
// 材料表索引
createIndexIfNotExists("idx_listening_materials_level", "ai_listening_materials", "level")
createIndexIfNotExists("idx_reading_materials_level", "ai_reading_materials", "level")
createIndexIfNotExists("idx_writing_prompts_level", "ai_writing_prompts", "level")
createIndexIfNotExists("idx_speaking_scenarios_level", "ai_speaking_scenarios", "level")
log.Println("索引创建完成")
return nil
}
// ApplyMergedSchemaIfNeeded 读取并执行合并后的SQL脚本用于创建视图、触发器及扩展表
func ApplyMergedSchemaIfNeeded(db *gorm.DB) error {
// 检查一个扩展表是否存在,作为是否需要执行脚本的依据
var count int64
db.Raw("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'ai_vocabulary_books'").Scan(&count)
if count > 0 {
// 已存在扩展结构,跳过
return nil
}
// 读取脚本文件(从 serve 目录运行,脚本位于 ../docs/
candidates := []string{
"../docs/database_schema_merged.sql",
"../docs/database_schema.sql",
}
var content []byte
var readErr error
for _, p := range candidates {
abs, _ := filepath.Abs(p)
content, readErr = os.ReadFile(abs)
if readErr == nil {
break
}
}
if readErr != nil {
log.Printf("读取数据库脚本失败: %v", readErr)
return nil
}
sql := string(content)
// 移除 DELIMITER 指令并将触发器结束符 // 转为 ;
lines := strings.Split(sql, "\n")
var cleaned []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "DELIMITER") {
continue
}
// 将以 // 结尾的行替换为 ;
if strings.HasSuffix(trimmed, "//") {
cleaned = append(cleaned, strings.TrimSuffix(line, "//")+";")
continue
}
cleaned = append(cleaned, line)
}
cleanedSQL := strings.Join(cleaned, "\n")
// 关闭外键检查以避免初始化时的顺序问题
if err := db.Exec("SET FOREIGN_KEY_CHECKS=0;").Error; err != nil {
log.Printf("关闭外键检查失败: %v", err)
}
// 执行脚本(依赖 multiStatements=true
if err := db.Exec(cleanedSQL).Error; err != nil {
log.Printf("执行合并SQL失败: %v", err)
}
// 恢复外键检查
if err := db.Exec("SET FOREIGN_KEY_CHECKS=1;").Error; err != nil {
log.Printf("开启外键检查失败: %v", err)
}
return nil
}