Files
ai_english/serve/internal/database/migrate.go
2025-11-17 13:39:05 +08:00

172 lines
5.8 KiB
Go
Raw Permalink 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 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
}