package main import ( "ankao/internal/database" "ankao/internal/models" "fmt" "log" "time" "gorm.io/gorm" ) // OldExam 旧的试卷模型(用于迁移) type OldExam struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` UserID uint `gorm:"not null;index"` Title string `gorm:"type:varchar(200);default:''"` TotalScore int `gorm:"not null;default:100"` Duration int `gorm:"not null;default:60"` PassScore int `gorm:"not null;default:60"` QuestionIDs []byte `gorm:"type:json"` Status string `gorm:"type:varchar(20);not null;default:'active'"` IsShared bool `gorm:"default:false"` SharedByID *uint `gorm:"index"` } func (OldExam) TableName() string { return "exams" } func main() { // 初始化数据库 if err := database.InitDB(); err != nil { log.Fatal("数据库初始化失败:", err) } db := database.GetDB() log.Println("开始迁移试卷分享数据...") // 1. 查找所有被分享的试卷副本 var sharedExams []OldExam if err := db.Where("is_shared = ? AND shared_by_id IS NOT NULL", true). Find(&sharedExams).Error; err != nil { log.Fatal("查询分享试卷失败:", err) } log.Printf("找到 %d 份分享试卷副本", len(sharedExams)) if len(sharedExams) == 0 { log.Println("没有需要迁移的数据,退出。") return } // 2. 按 shared_by_id + question_ids 分组 type ShareGroup struct { SharedByID uint QuestionIDs string Exams []OldExam } groupMap := make(map[string]*ShareGroup) for _, exam := range sharedExams { if exam.SharedByID == nil { continue } key := fmt.Sprintf("%d_%s", *exam.SharedByID, string(exam.QuestionIDs)) if group, exists := groupMap[key]; exists { group.Exams = append(group.Exams, exam) } else { groupMap[key] = &ShareGroup{ SharedByID: *exam.SharedByID, QuestionIDs: string(exam.QuestionIDs), Exams: []OldExam{exam}, } } } log.Printf("分组后共 %d 组", len(groupMap)) successCount := 0 failCount := 0 // 3. 处理每个分组 for _, group := range groupMap { // 查找原始试卷 var originalExam OldExam if err := db.Where("user_id = ? AND question_ids = ? AND is_shared = ?", group.SharedByID, group.QuestionIDs, false). First(&originalExam).Error; err != nil { log.Printf("未找到原始试卷: shared_by_id=%d, 跳过该组", group.SharedByID) failCount += len(group.Exams) continue } // 开始事务 tx := db.Begin() migrationSuccess := true // 4. 为每个分享副本创建关联记录 for _, sharedExam := range group.Exams { // 创建分享记录 share := models.ExamShare{ ExamID: originalExam.ID, SharedByID: group.SharedByID, SharedToID: sharedExam.UserID, SharedAt: sharedExam.CreatedAt, } if err := tx.Create(&share).Error; err != nil { log.Printf("创建分享记录失败: %v", err) tx.Rollback() failCount += len(group.Exams) migrationSuccess = false break } // 5. 更新考试记录,将 exam_id 指向原始试卷 if err := tx.Model(&models.ExamRecord{}). Where("exam_id = ?", sharedExam.ID). Update("exam_id", originalExam.ID).Error; err != nil { log.Printf("更新考试记录失败: %v", err) tx.Rollback() failCount += len(group.Exams) migrationSuccess = false break } // 6. 软删除分享副本 if err := tx.Delete(&sharedExam).Error; err != nil { log.Printf("删除分享副本失败: %v", err) tx.Rollback() failCount += len(group.Exams) migrationSuccess = false break } if migrationSuccess { successCount++ } } // 提交事务 if migrationSuccess { if err := tx.Commit().Error; err != nil { log.Printf("提交事务失败: %v", err) failCount += len(group.Exams) } } } log.Printf("迁移完成: 成功 %d, 失败 %d", successCount, failCount) // 7. 验证迁移结果 log.Println("\n开始验证迁移结果...") // 统计 exam_shares 表记录数 var shareCount int64 db.Model(&models.ExamShare{}).Count(&shareCount) log.Printf("exam_shares 表记录数: %d", shareCount) // 统计剩余的分享副本数(应该为0) var remainingSharedExams int64 db.Model(&OldExam{}).Where("is_shared = ?", true).Count(&remainingSharedExams) log.Printf("剩余分享副本数: %d (应该为0)", remainingSharedExams) // 检查是否有孤立的考试记录 var orphanRecords int64 db.Raw(` SELECT COUNT(*) FROM exam_records er WHERE NOT EXISTS ( SELECT 1 FROM exams e WHERE e.id = er.exam_id AND e.deleted_at IS NULL ) `).Scan(&orphanRecords) log.Printf("孤立的考试记录数: %d (应该为0)", orphanRecords) if remainingSharedExams == 0 && orphanRecords == 0 { log.Println("\n✓ 迁移验证通过!") } else { log.Println("\n✗ 迁移验证失败,请检查数据!") } log.Println("\n注意: 如果验证通过,可以考虑在未来某个时间点执行以下SQL移除旧字段:") log.Println(" ALTER TABLE exams DROP COLUMN is_shared;") log.Println(" ALTER TABLE exams DROP COLUMN shared_by_id;") }