package services import ( "ankao/internal/database" "ankao/internal/models" "encoding/json" "fmt" "log" "math/rand" "time" "gorm.io/gorm" ) type DailyExamService struct { db *gorm.DB } func NewDailyExamService() *DailyExamService { return &DailyExamService{ db: database.GetDB(), } } // GenerateDailyExam 生成每日一练试卷 func (s *DailyExamService) GenerateDailyExam() error { // 1. 获取分布式锁(使用日期作为锁ID) today := time.Now().Format("20060102") lockID := hashString(today) // 使用日期哈希作为锁ID var locked bool if err := s.db.Raw("SELECT pg_try_advisory_lock(?)", lockID).Scan(&locked).Error; err != nil { return fmt.Errorf("获取锁失败: %w", err) } if !locked { log.Println("其他实例正在生成每日一练,跳过") return nil } defer s.db.Exec("SELECT pg_advisory_unlock(?)", lockID) // 2. 检查今天是否已生成 todayStart := time.Now().Truncate(24 * time.Hour) todayEnd := todayStart.Add(24 * time.Hour) var count int64 s.db.Model(&models.Exam{}). Where("is_system = ? AND created_at >= ? AND created_at < ?", true, todayStart, todayEnd). Count(&count) if count > 0 { log.Println("今日每日一练已生成,跳过") return nil } // 3. 生成试卷标题 now := time.Now() title := fmt.Sprintf("%d年%02d月%02d日的每日一练", now.Year(), now.Month(), now.Day()) // 4. 随机选择题目(使用与创建试卷相同的逻辑) questionIDs, totalScore, err := s.selectQuestions() if err != nil { return fmt.Errorf("选择题目失败: %w", err) } questionIDsJSON, _ := json.Marshal(questionIDs) // 5. 创建试卷(使用第一个用户作为创建者,但标记为系统试卷) // 获取第一个用户ID var firstUser models.User if err := s.db.Order("id ASC").First(&firstUser).Error; err != nil { return fmt.Errorf("查询用户失败: %w", err) } exam := models.Exam{ UserID: uint(firstUser.ID), // 使用第一个用户作为创建者 Title: title, TotalScore: int(totalScore), Duration: 60, PassScore: 80, QuestionIDs: questionIDsJSON, Status: "active", IsSystem: true, // 标记为系统试卷 } if err := s.db.Create(&exam).Error; err != nil { return fmt.Errorf("创建试卷失败: %w", err) } log.Printf("成功创建每日一练试卷: ID=%d, Title=%s", exam.ID, exam.Title) // 6. 分享给所有用户 if err := s.shareToAllUsers(exam.ID, uint(firstUser.ID)); err != nil { log.Printf("分享试卷失败: %v", err) // 不返回错误,因为试卷已创建成功 } return nil } // selectQuestions 选择题目(复用现有逻辑) func (s *DailyExamService) selectQuestions() ([]int64, float64, error) { questionTypes := []struct { Type string Count int Score float64 }{ {Type: "fill-in-blank", Count: 20, Score: 2.0}, // 40分 {Type: "true-false", Count: 10, Score: 1.0}, // 10分 {Type: "multiple-choice", Count: 10, Score: 1.0}, // 10分 {Type: "multiple-selection", Count: 10, Score: 2.0}, // 20分 {Type: "short-answer", Count: 1, Score: 10.0}, // 10分 {Type: "ordinary-essay", Count: 1, Score: 10.0}, // 10分(普通涉密人员论述题) {Type: "management-essay", Count: 1, Score: 10.0}, // 10分(保密管理人员论述题) } var allQuestionIDs []int64 var totalScore float64 for _, qt := range questionTypes { var questions []models.PracticeQuestion if err := s.db.Where("type = ?", qt.Type).Find(&questions).Error; err != nil { return nil, 0, err } // 检查题目数量是否足够 if len(questions) < qt.Count { return nil, 0, fmt.Errorf("题型 %s 题目数量不足,需要 %d 道,实际 %d 道", qt.Type, qt.Count, len(questions)) } // 随机抽取 (Fisher-Yates 洗牌算法) rand.Seed(time.Now().UnixNano()) for i := len(questions) - 1; i > 0; i-- { j := rand.Intn(i + 1) questions[i], questions[j] = questions[j], questions[i] } selectedQuestions := questions[:qt.Count] for _, q := range selectedQuestions { allQuestionIDs = append(allQuestionIDs, q.ID) } totalScore += float64(qt.Count) * qt.Score } // 随机打乱题目ID顺序 rand.Seed(time.Now().UnixNano()) for i := len(allQuestionIDs) - 1; i > 0; i-- { j := rand.Intn(i + 1) allQuestionIDs[i], allQuestionIDs[j] = allQuestionIDs[j], allQuestionIDs[i] } return allQuestionIDs, totalScore, nil } // shareToAllUsers 分享给所有用户 func (s *DailyExamService) shareToAllUsers(examID uint, sharedByID uint) error { // 查询所有用户(排除创建者) var users []models.User if err := s.db.Where("id != ?", sharedByID).Find(&users).Error; err != nil { return err } // 批量创建分享记录 now := time.Now() shares := make([]models.ExamShare, 0, len(users)) for _, user := range users { shares = append(shares, models.ExamShare{ ExamID: examID, SharedByID: int64(sharedByID), SharedToID: int64(user.ID), SharedAt: now, }) } if len(shares) > 0 { // 批量插入 if err := s.db.Create(&shares).Error; err != nil { return err } log.Printf("成功分享给 %d 个用户", len(shares)) } return nil } // hashString 计算字符串哈希值(用于生成锁ID) func hashString(s string) int64 { var hash int64 for _, c := range s { hash = hash*31 + int64(c) } // 确保返回正数 if hash < 0 { hash = -hash } return hash }