AnCao/internal/handlers/wrong_question_handler.go
yanlongqi 2bcf6bdacc 实现完整的用户统计功能和认证系统
**统计功能**:
- 新增UserAnswerRecord模型记录用户答题历史
- 实现GetStatistics接口,统计题库总数、已答题数、正确率
- 在提交答案时自动记录答题历史
- 前端连接真实统计接口,显示实时数据

**认证系统优化**:
- 新增Auth中间件,实现基于Token的身份验证
- 登录和注册时自动生成并保存Token到数据库
- 所有需要登录的接口都通过Auth中间件保护
- 统一处理未授权请求,返回401状态码

**错题练习功能**:
- 新增GetRandomWrongQuestion接口,随机获取错题
- 支持错题练习模式(/question?mode=wrong)
- 优化错题本页面UI,移除已掌握功能
- 新增"开始错题练习"按钮,直接进入练习模式

**数据库迁移**:
- 新增user_answer_records表,记录用户答题历史
- User表新增token字段,存储用户登录凭证

**技术改进**:
- 统一错误处理,区分401未授权和404未找到
- 优化答题流程,记录历史和错题分离处理
- 移除异步记录错题,改为同步处理保证数据一致性

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:26:27 +08:00

266 lines
6.3 KiB
Go

package handlers
import (
"ankao/internal/database"
"ankao/internal/models"
"encoding/json"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// GetWrongQuestions 获取错题列表
func GetWrongQuestions(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
db := database.GetDB()
var wrongQuestions []models.WrongQuestion
// 查询参数
isMastered := c.Query("is_mastered") // "true" 或 "false"
questionType := c.Query("type") // 题型筛选
query := db.Where("user_id = ?", userID).Preload("PracticeQuestion")
// 筛选是否已掌握
if isMastered == "true" {
query = query.Where("is_mastered = ?", true)
} else if isMastered == "false" {
query = query.Where("is_mastered = ?", false)
}
// 按最后错误时间倒序
if err := query.Order("last_wrong_time DESC").Find(&wrongQuestions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "查询失败",
})
return
}
// 转换为DTO
dtos := make([]models.WrongQuestionDTO, 0, len(wrongQuestions))
for _, wq := range wrongQuestions {
// 题型筛选
if questionType != "" && mapBackendToFrontendType(wq.PracticeQuestion.Type) != questionType {
continue
}
// 解析答案
var wrongAnswer, correctAnswer interface{}
json.Unmarshal([]byte(wq.WrongAnswer), &wrongAnswer)
json.Unmarshal([]byte(wq.CorrectAnswer), &correctAnswer)
dto := models.WrongQuestionDTO{
ID: wq.ID,
QuestionID: wq.QuestionID,
Question: convertToDTO(wq.PracticeQuestion),
WrongAnswer: wrongAnswer,
CorrectAnswer: correctAnswer,
WrongCount: wq.WrongCount,
LastWrongTime: wq.LastWrongTime,
IsMastered: wq.IsMastered,
}
dtos = append(dtos, dto)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dtos,
"total": len(dtos),
})
}
// GetWrongQuestionStats 获取错题统计
func GetWrongQuestionStats(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
db := database.GetDB()
var wrongQuestions []models.WrongQuestion
if err := db.Where("user_id = ?", userID).Preload("PracticeQuestion").Find(&wrongQuestions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "查询失败",
})
return
}
stats := models.WrongQuestionStats{
TotalWrong: len(wrongQuestions),
Mastered: 0,
NotMastered: 0,
TypeStats: make(map[string]int),
CategoryStats: make(map[string]int),
}
for _, wq := range wrongQuestions {
if wq.IsMastered {
stats.Mastered++
} else {
stats.NotMastered++
}
// 统计题型
frontendType := mapBackendToFrontendType(wq.PracticeQuestion.Type)
stats.TypeStats[frontendType]++
// 统计分类
stats.CategoryStats[wq.PracticeQuestion.TypeName]++
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
// MarkWrongQuestionMastered 标记错题为已掌握
func MarkWrongQuestionMastered(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
wrongQuestionID := c.Param("id")
db := database.GetDB()
var wrongQuestion models.WrongQuestion
if err := db.Where("id = ? AND user_id = ?", wrongQuestionID, userID).First(&wrongQuestion).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "错题不存在",
})
return
}
wrongQuestion.IsMastered = true
if err := db.Save(&wrongQuestion).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "更新失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "已标记为掌握",
})
}
// ClearWrongQuestions 清空错题本
func ClearWrongQuestions(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
db := database.GetDB()
// 删除用户所有错题记录
if err := db.Where("user_id = ?", userID).Delete(&models.WrongQuestion{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "清空失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "错题本已清空",
})
}
// recordWrongQuestion 记录错题(内部函数,在答题错误时调用)
func recordWrongQuestion(userID, questionID uint, userAnswer, correctAnswer interface{}) error {
db := database.GetDB()
// 将答案序列化为JSON
wrongAnswerJSON, _ := json.Marshal(userAnswer)
correctAnswerJSON, _ := json.Marshal(correctAnswer)
// 查找是否已存在该错题
var existingWrong models.WrongQuestion
result := db.Where("user_id = ? AND question_id = ?", userID, questionID).First(&existingWrong)
if result.Error == nil {
// 已存在,更新错误次数和时间
existingWrong.WrongCount++
existingWrong.LastWrongTime = time.Now()
existingWrong.WrongAnswer = string(wrongAnswerJSON)
existingWrong.CorrectAnswer = string(correctAnswerJSON)
existingWrong.IsMastered = false // 重新标记为未掌握
return db.Save(&existingWrong).Error
}
// 不存在,创建新记录
newWrong := models.WrongQuestion{
UserID: userID,
QuestionID: questionID,
WrongAnswer: string(wrongAnswerJSON),
CorrectAnswer: string(correctAnswerJSON),
WrongCount: 1,
LastWrongTime: time.Now(),
IsMastered: false,
}
return db.Create(&newWrong).Error
}
// GetRandomWrongQuestion 获取随机错题进行练习
func GetRandomWrongQuestion(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
db := database.GetDB()
var wrongQuestion models.WrongQuestion
// 随机获取一个错题
if err := db.Where("user_id = ?", userID).Order("RANDOM()").Preload("PracticeQuestion").First(&wrongQuestion).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "暂无错题",
})
return
}
// 转换为DTO返回
dto := convertToDTO(wrongQuestion.PracticeQuestion)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dto,
})
}