AnCao/internal/services/daily_exam_service.go
yanlongqi b1551e6deb 添加每日一练排行榜功能
- 修复 daily_exam_service.go 中的类型转换错误
- 在首页添加每日一练排行榜组件
- 显示今日每日一练的考试成绩和用时排行
- 当今日尚未生成每日一练时显示友好提示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 00:31:33 +08:00

202 lines
5.3 KiB
Go
Raw 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 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
}