290 lines
8.7 KiB
Go
290 lines
8.7 KiB
Go
package services
|
||
|
||
import (
|
||
"ankao/internal/database"
|
||
"ankao/internal/models"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ==================== 错题服务 ====================
|
||
|
||
// RecordWrongAnswer 记录错误答案
|
||
func RecordWrongAnswer(userID, questionID int64, userAnswer, correctAnswer interface{}, timeSpent int) error {
|
||
db := database.GetDB()
|
||
|
||
log.Printf("[错题记录] 开始记录错题 (userID: %d, questionID: %d)", userID, questionID)
|
||
|
||
// 序列化答案
|
||
userAnswerJSON, _ := json.Marshal(userAnswer)
|
||
correctAnswerJSON, _ := json.Marshal(correctAnswer)
|
||
|
||
// 查找或创建错题记录
|
||
var wrongQuestion models.WrongQuestion
|
||
err := db.Where("user_id = ? AND question_id = ?", userID, questionID).First(&wrongQuestion).Error
|
||
|
||
if err != nil {
|
||
// 不存在,创建新记录
|
||
log.Printf("[错题记录] 创建新错题记录 (userID: %d, questionID: %d)", userID, questionID)
|
||
wrongQuestion = models.WrongQuestion{
|
||
UserID: userID,
|
||
QuestionID: questionID,
|
||
}
|
||
wrongQuestion.RecordWrongAnswer()
|
||
|
||
if err := db.Create(&wrongQuestion).Error; err != nil {
|
||
log.Printf("[错题记录] 创建错题记录失败: %v", err)
|
||
return fmt.Errorf("创建错题记录失败: %v", err)
|
||
}
|
||
log.Printf("[错题记录] 成功创建错题记录 (ID: %d)", wrongQuestion.ID)
|
||
} else {
|
||
// 已存在,更新记录
|
||
log.Printf("[错题记录] 更新已存在的错题记录 (ID: %d)", wrongQuestion.ID)
|
||
wrongQuestion.RecordWrongAnswer()
|
||
if err := db.Save(&wrongQuestion).Error; err != nil {
|
||
log.Printf("[错题记录] 更新错题记录失败: %v", err)
|
||
return fmt.Errorf("更新错题记录失败: %v", err)
|
||
}
|
||
log.Printf("[错题记录] 成功更新错题记录 (ID: %d, 错误次数: %d)", wrongQuestion.ID, wrongQuestion.TotalWrongCount)
|
||
}
|
||
|
||
// 创建历史记录
|
||
history := models.WrongQuestionHistory{
|
||
WrongQuestionID: wrongQuestion.ID,
|
||
UserAnswer: string(userAnswerJSON),
|
||
CorrectAnswer: string(correctAnswerJSON),
|
||
AnsweredAt: time.Now(),
|
||
TimeSpent: timeSpent,
|
||
IsCorrect: false,
|
||
}
|
||
|
||
if err := db.Create(&history).Error; err != nil {
|
||
log.Printf("[错题记录] 创建错题历史失败: %v", err)
|
||
} else {
|
||
log.Printf("[错题记录] 成功创建历史记录 (ID: %d, WrongQuestionID: %d)", history.ID, history.WrongQuestionID)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// RecordCorrectAnswer 记录正确答案(用于错题练习)
|
||
func RecordCorrectAnswer(userID, questionID int64, userAnswer, correctAnswer interface{}, timeSpent int) error {
|
||
db := database.GetDB()
|
||
|
||
// 查找错题记录
|
||
var wrongQuestion models.WrongQuestion
|
||
err := db.Where("user_id = ? AND question_id = ?", userID, questionID).First(&wrongQuestion).Error
|
||
if err != nil {
|
||
// 不存在错题记录,无需处理
|
||
return nil
|
||
}
|
||
|
||
// 序列化答案
|
||
userAnswerJSON, _ := json.Marshal(userAnswer)
|
||
correctAnswerJSON, _ := json.Marshal(correctAnswer)
|
||
|
||
// 更新连续答对次数
|
||
wrongQuestion.RecordCorrectAnswer()
|
||
if err := db.Save(&wrongQuestion).Error; err != nil {
|
||
return fmt.Errorf("更新错题记录失败: %v", err)
|
||
}
|
||
|
||
// 创建历史记录
|
||
history := models.WrongQuestionHistory{
|
||
WrongQuestionID: wrongQuestion.ID,
|
||
UserAnswer: string(userAnswerJSON),
|
||
CorrectAnswer: string(correctAnswerJSON),
|
||
AnsweredAt: time.Now(),
|
||
TimeSpent: timeSpent,
|
||
IsCorrect: true,
|
||
}
|
||
|
||
if err := db.Create(&history).Error; err != nil {
|
||
log.Printf("创建错题历史失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetWrongQuestionStats 获取错题统计
|
||
func GetWrongQuestionStats(userID int64) (*models.WrongQuestionStats, error) {
|
||
db := database.GetDB()
|
||
|
||
stats := &models.WrongQuestionStats{
|
||
TypeStats: make(map[string]int),
|
||
CategoryStats: make(map[string]int),
|
||
MasteryLevelDist: make(map[string]int),
|
||
}
|
||
|
||
// 基础统计
|
||
var totalWrong, mastered int64
|
||
db.Model(&models.WrongQuestion{}).Where("user_id = ?", userID).Count(&totalWrong)
|
||
db.Model(&models.WrongQuestion{}).Where("user_id = ? AND is_mastered = ?", userID, true).Count(&mastered)
|
||
|
||
stats.TotalWrong = int(totalWrong)
|
||
stats.Mastered = int(mastered)
|
||
stats.NotMastered = int(totalWrong) - int(mastered)
|
||
stats.NeedReview = 0 // 不再使用复习时间,设置为0
|
||
|
||
// 按题型统计
|
||
var typeStats []struct {
|
||
Type string
|
||
Count int
|
||
}
|
||
db.Model(&models.WrongQuestion{}).
|
||
Select("practice_questions.type, COUNT(*) as count").
|
||
Joins("LEFT JOIN practice_questions ON practice_questions.id = wrong_questions.question_id").
|
||
Where("wrong_questions.user_id = ?", userID).
|
||
Group("practice_questions.type").
|
||
Scan(&typeStats)
|
||
|
||
for _, ts := range typeStats {
|
||
stats.TypeStats[ts.Type] = ts.Count
|
||
}
|
||
|
||
// 按分类统计
|
||
var categoryStats []struct {
|
||
Category string
|
||
Count int
|
||
}
|
||
db.Model(&models.WrongQuestion{}).
|
||
Select("practice_questions.category, COUNT(*) as count").
|
||
Joins("LEFT JOIN practice_questions ON practice_questions.id = wrong_questions.question_id").
|
||
Where("wrong_questions.user_id = ?", userID).
|
||
Group("practice_questions.category").
|
||
Scan(&categoryStats)
|
||
|
||
for _, cs := range categoryStats {
|
||
stats.CategoryStats[cs.Category] = cs.Count
|
||
}
|
||
|
||
// 掌握度分布
|
||
var masteryDist []struct {
|
||
Level string
|
||
Count int
|
||
}
|
||
db.Model(&models.WrongQuestion{}).
|
||
Select(`
|
||
CASE
|
||
WHEN mastery_level >= 80 THEN '优秀'
|
||
WHEN mastery_level >= 60 THEN '良好'
|
||
WHEN mastery_level >= 40 THEN '一般'
|
||
WHEN mastery_level >= 20 THEN '较差'
|
||
ELSE '很差'
|
||
END as level,
|
||
COUNT(*) as count
|
||
`).
|
||
Where("user_id = ?", userID).
|
||
Group("level").
|
||
Scan(&masteryDist)
|
||
|
||
for _, md := range masteryDist {
|
||
stats.MasteryLevelDist[md.Level] = md.Count
|
||
}
|
||
|
||
// 错题趋势(最近7天)
|
||
stats.TrendData = calculateTrendData(db, userID, 7)
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
// calculateTrendData 计算错题趋势数据
|
||
func calculateTrendData(db *gorm.DB, userID int64, days int) []models.TrendPoint {
|
||
trendData := make([]models.TrendPoint, days)
|
||
now := time.Now()
|
||
|
||
for i := days - 1; i >= 0; i-- {
|
||
date := now.AddDate(0, 0, -i)
|
||
dateStr := date.Format("01-02")
|
||
|
||
var count int64
|
||
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||
|
||
db.Model(&models.WrongQuestion{}).
|
||
Where("user_id = ? AND last_wrong_time >= ? AND last_wrong_time < ?", userID, startOfDay, endOfDay).
|
||
Count(&count)
|
||
|
||
trendData[days-1-i] = models.TrendPoint{
|
||
Date: dateStr,
|
||
Count: int(count),
|
||
}
|
||
}
|
||
|
||
return trendData
|
||
}
|
||
|
||
// GetRecommendedWrongQuestions 获取推荐练习的错题(智能推荐)
|
||
// 推荐策略(按优先级):
|
||
// 1. 最优先推荐掌握度为0的题目(从未答对过)
|
||
// 2. 其次推荐掌握度低的题目(mastery_level 从低到高)
|
||
// 3. 最后推荐最近答错的题目
|
||
func GetRecommendedWrongQuestions(userID int64, limit int, excludeQuestionID int64) ([]models.WrongQuestion, error) {
|
||
db := database.GetDB()
|
||
|
||
var questions []models.WrongQuestion
|
||
|
||
// 策略1: 最优先推荐掌握度为0的题目(从未答对过)
|
||
var zeroMastery []models.WrongQuestion
|
||
query1 := db.Where("user_id = ? AND is_mastered = ? AND mastery_level = 0", userID, false)
|
||
if excludeQuestionID > 0 {
|
||
query1 = query1.Where("question_id != ?", excludeQuestionID)
|
||
}
|
||
query1.Order("total_wrong_count DESC, last_wrong_time DESC").
|
||
Limit(limit).
|
||
Preload("PracticeQuestion").
|
||
Find(&zeroMastery)
|
||
questions = append(questions, zeroMastery...)
|
||
|
||
// 如果已经够了,直接返回
|
||
if len(questions) >= limit {
|
||
return questions[:limit], nil
|
||
}
|
||
|
||
// 策略2: 推荐掌握度低的题目(mastery_level 从低到高)
|
||
var lowMastery []models.WrongQuestion
|
||
query2 := db.Where("user_id = ? AND is_mastered = ? AND mastery_level > 0 AND id NOT IN ?", userID, false, getIDs(questions))
|
||
if excludeQuestionID > 0 {
|
||
query2 = query2.Where("question_id != ?", excludeQuestionID)
|
||
}
|
||
query2.Order("mastery_level ASC, total_wrong_count DESC").
|
||
Limit(limit - len(questions)).
|
||
Preload("PracticeQuestion").
|
||
Find(&lowMastery)
|
||
questions = append(questions, lowMastery...)
|
||
|
||
if len(questions) >= limit {
|
||
return questions[:limit], nil
|
||
}
|
||
|
||
// 策略3: 最近答错的题目(填充剩余,以防万一)
|
||
var recent []models.WrongQuestion
|
||
query3 := db.Where("user_id = ? AND is_mastered = ? AND id NOT IN ?", userID, false, getIDs(questions))
|
||
if excludeQuestionID > 0 {
|
||
query3 = query3.Where("question_id != ?", excludeQuestionID)
|
||
}
|
||
query3.Order("last_wrong_time DESC").
|
||
Limit(limit - len(questions)).
|
||
Preload("PracticeQuestion").
|
||
Find(&recent)
|
||
questions = append(questions, recent...)
|
||
|
||
return questions, nil
|
||
}
|
||
|
||
// getIDs 获取错题记录的ID列表
|
||
func getIDs(questions []models.WrongQuestion) []int64 {
|
||
if len(questions) == 0 {
|
||
return []int64{0} // 避免 SQL 错误
|
||
}
|
||
ids := make([]int64, len(questions))
|
||
for i, q := range questions {
|
||
ids[i] = q.ID
|
||
}
|
||
return ids
|
||
}
|