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