1. 错题本系统重构: - 新增错题服务层 (wrong_question_service.go) - 实现智能推荐算法(基于掌握度和错误次数) - 添加掌握度追踪机制(连续答对6次标记为已掌握) - 支持错题筛选和排序功能 - 新增错题统计趋势分析 2. UI优化: - 美化错题本界面,采用毛玻璃卡片设计 - 添加四宫格统计卡片(错题总数、已掌握、未掌握、掌握率) - 优化筛选和操作按钮布局 - 使用条状进度条显示掌握度 - 改进响应式设计,优化移动端体验 3. 功能完善: - 修复判断题答案显示问题 - 当掌握率100%时禁用"开始练习"按钮 - 删除测试文件和 nul 文件 - 更新文档 (CLAUDE.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
337 lines
10 KiB
Go
337 lines
10 KiB
Go
package services
|
||
|
||
import (
|
||
"ankao/internal/database"
|
||
"ankao/internal/models"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// ==================== 错题服务 ====================
|
||
|
||
// RecordWrongAnswer 记录错误答案
|
||
func RecordWrongAnswer(userID, questionID uint, 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,
|
||
Tags: []string{},
|
||
}
|
||
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 uint, 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 uint) (*models.WrongQuestionStats, error) {
|
||
db := database.GetDB()
|
||
|
||
stats := &models.WrongQuestionStats{
|
||
TypeStats: make(map[string]int),
|
||
CategoryStats: make(map[string]int),
|
||
MasteryLevelDist: make(map[string]int),
|
||
TagStats: 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
|
||
}
|
||
|
||
// 标签统计
|
||
var wrongQuestions []models.WrongQuestion
|
||
db.Where("user_id = ?", userID).Find(&wrongQuestions)
|
||
for _, wq := range wrongQuestions {
|
||
for _, tag := range wq.Tags {
|
||
stats.TagStats[tag]++
|
||
}
|
||
}
|
||
|
||
// 错题趋势(最近7天)
|
||
stats.TrendData = calculateTrendData(db, userID, 7)
|
||
|
||
return stats, nil
|
||
}
|
||
|
||
// calculateTrendData 计算错题趋势数据
|
||
func calculateTrendData(db *gorm.DB, userID uint, 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 uint, limit int, excludeQuestionID uint) ([]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) []uint {
|
||
if len(questions) == 0 {
|
||
return []uint{0} // 避免 SQL 错误
|
||
}
|
||
ids := make([]uint, len(questions))
|
||
for i, q := range questions {
|
||
ids[i] = q.ID
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// AnalyzeWeakPoints 分析薄弱知识点
|
||
func AnalyzeWeakPoints(userID uint) (map[string]float64, error) {
|
||
db := database.GetDB()
|
||
|
||
// 按分类统计错误率
|
||
var categoryStats []struct {
|
||
Category string
|
||
WrongCount int
|
||
TotalCount int
|
||
WrongRate float64
|
||
}
|
||
|
||
// 查询每个分类的错题数和总题数
|
||
db.Raw(`
|
||
SELECT
|
||
pq.category,
|
||
COUNT(DISTINCT wq.question_id) as wrong_count,
|
||
(SELECT COUNT(*) FROM practice_questions WHERE category = pq.category) as total_count,
|
||
CAST(COUNT(DISTINCT wq.question_id) AS FLOAT) / NULLIF((SELECT COUNT(*) FROM practice_questions WHERE category = pq.category), 0) as wrong_rate
|
||
FROM wrong_questions wq
|
||
LEFT JOIN practice_questions pq ON wq.question_id = pq.id
|
||
WHERE wq.user_id = ?
|
||
GROUP BY pq.category
|
||
ORDER BY wrong_rate DESC
|
||
`, userID).Scan(&categoryStats)
|
||
|
||
result := make(map[string]float64)
|
||
for _, cs := range categoryStats {
|
||
if cs.Category != "" {
|
||
result[cs.Category] = cs.WrongRate
|
||
}
|
||
}
|
||
|
||
return result, nil
|
||
}
|