- 修复 daily_exam_service.go 中的类型转换错误 - 在首页添加每日一练排行榜组件 - 显示今日每日一练的考试成绩和用时排行 - 当今日尚未生成每日一练时显示友好提示 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
202 lines
5.3 KiB
Go
202 lines
5.3 KiB
Go
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
|
||
}
|