AnCao/internal/services/wrong_question_service.go
yanlongqi 2fbeb23947 优化错题本功能和UI设计
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>
2025-11-08 04:20:42 +08:00

337 lines
10 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"
"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
}