AnCao/internal/handlers/wrong_question_handler.go

324 lines
8.7 KiB
Go
Raw Permalink 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 handlers
import (
"ankao/internal/database"
"ankao/internal/models"
"ankao/internal/services"
"encoding/json"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// ==================== 错题管理 API ====================
// GetWrongQuestions 获取错题列表(新版)
// GET /api/v2/wrong-questions?is_mastered=false&type=single-choice&tag=数学&sort=review_time
func GetWrongQuestions(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
db := database.GetDB()
query := db.Model(&models.WrongQuestion{}).Where("user_id = ?", userID)
// 筛选条件
if isMastered := c.Query("is_mastered"); isMastered != "" {
query = query.Where("is_mastered = ?", isMastered == "true")
}
// 排序
switch c.Query("sort") {
case "wrong_count":
// 按错误次数排序(错误最多的在前)
query = query.Order("total_wrong_count DESC")
case "mastery_level":
// 按掌握度排序(掌握度最低的在前)
query = query.Order("mastery_level ASC")
default:
// 默认按最后错误时间排序
query = query.Order("last_wrong_time DESC")
}
var wrongQuestions []models.WrongQuestion
// 先查询错题记录
if err := query.Find(&wrongQuestions).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询错题失败"})
return
}
// 手动加载关联数据
for i := range wrongQuestions {
// 加载题目信息(确保使用正确的关联)
var practiceQuestion models.PracticeQuestion
if err := db.Where("id = ?", wrongQuestions[i].QuestionID).First(&practiceQuestion).Error; err == nil {
wrongQuestions[i].PracticeQuestion = &practiceQuestion
}
// 加载最近3次历史
var history []models.WrongQuestionHistory
if err := db.Where("wrong_question_id = ?", wrongQuestions[i].ID).
Order("answered_at DESC").
Limit(3).
Find(&history).Error; err == nil {
wrongQuestions[i].History = history
}
}
// 转换为 DTO
dtos := make([]models.WrongQuestionDTO, len(wrongQuestions))
for i, wq := range wrongQuestions {
dtos[i] = convertWrongQuestionToDTO(&wq, true) // 包含最近历史
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dtos,
})
}
// GetWrongQuestionDetail 获取错题详情(包含完整历史)
// GET /api/v2/wrong-questions/:id
func GetWrongQuestionDetail(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的错题ID"})
return
}
db := database.GetDB()
var wrongQuestion models.WrongQuestion
if err := db.Where("id = ? AND user_id = ?", id, userID).
Preload("PracticeQuestion").
Preload("History", func(db *gorm.DB) *gorm.DB {
return db.Order("answered_at DESC")
}).
First(&wrongQuestion).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "错题不存在"})
return
}
// 转换为 DTO包含完整历史
dto := convertToDetailDTO(&wrongQuestion)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dto,
})
}
// GetWrongQuestionStats 获取错题统计(新版)
// GET /api/v2/wrong-questions/stats
func GetWrongQuestionStats(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
stats, err := services.GetWrongQuestionStats(userID.(int64))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取统计失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
// GetRecommendedWrongQuestions 获取推荐练习的错题(智能推荐)
// GET /api/v2/wrong-questions/recommended?limit=10&exclude=123
func GetRecommendedWrongQuestions(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
limit := 10
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
// 获取要排除的题目ID前端传递当前题目ID避免重复推荐
excludeQuestionID := int64(0)
if e := c.Query("exclude"); e != "" {
if parsed, err := strconv.ParseUint(e, 10, 64); err == nil {
excludeQuestionID = int64(parsed)
}
}
questions, err := services.GetRecommendedWrongQuestions(userID.(int64), limit, excludeQuestionID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取推荐错题失败"})
return
}
// 转换为 DTO
dtos := make([]models.WrongQuestionDTO, len(questions))
for i, wq := range questions {
dtos[i] = convertWrongQuestionToDTO(&wq, false)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dtos,
})
}
// DeleteWrongQuestion 删除错题(新版)
// DELETE /api/v2/wrong-questions/:id
func DeleteWrongQuestion(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的错题ID"})
return
}
db := database.GetDB()
// 删除历史记录
db.Where("wrong_question_id = ?", id).Delete(&models.WrongQuestionHistory{})
// 删除错题记录
result := db.Where("id = ? AND user_id = ?", id, userID).Delete(&models.WrongQuestion{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除错题失败"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "错题不存在"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "删除成功",
})
}
// ClearWrongQuestions 清空错题本(新版)
// DELETE /api/v2/wrong-questions
func ClearWrongQuestions(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
db := database.GetDB()
// 获取所有错题ID
var wrongQuestionIDs []uint
db.Model(&models.WrongQuestion{}).Where("user_id = ?", userID).Pluck("id", &wrongQuestionIDs)
// 删除历史记录
if len(wrongQuestionIDs) > 0 {
db.Where("wrong_question_id IN ?", wrongQuestionIDs).Delete(&models.WrongQuestionHistory{})
}
// 删除错题记录
if err := db.Where("user_id = ?", userID).Delete(&models.WrongQuestion{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空错题本失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "错题本已清空",
})
}
// ==================== 辅助函数 ====================
// convertWrongQuestionToDTO 转换为 DTO V2可选是否包含最近历史
func convertWrongQuestionToDTO(wq *models.WrongQuestion, includeHistory bool) models.WrongQuestionDTO {
dto := models.WrongQuestionDTO{
ID: wq.ID,
QuestionID: wq.QuestionID,
FirstWrongTime: wq.FirstWrongTime,
LastWrongTime: wq.LastWrongTime,
TotalWrongCount: wq.TotalWrongCount,
MasteryLevel: wq.MasteryLevel,
ConsecutiveCorrect: wq.ConsecutiveCorrect,
IsMastered: wq.IsMastered,
}
// 转换题目信息
if wq.PracticeQuestion != nil {
questionDTO := convertToDTO(*wq.PracticeQuestion)
dto.Question = &questionDTO
}
// 包含最近3次历史
if includeHistory && len(wq.History) > 0 {
count := 3
if len(wq.History) < count {
count = len(wq.History)
}
dto.RecentHistory = make([]models.WrongQuestionHistoryDTO, count)
for i := 0; i < count; i++ {
dto.RecentHistory[i] = convertWrongHistoryToDTO(&wq.History[i])
}
}
return dto
}
// convertToDetailDTO 转换为详情 DTO包含完整历史
func convertToDetailDTO(wq *models.WrongQuestion) models.WrongQuestionDTO {
dto := convertWrongQuestionToDTO(wq, false)
// 包含完整历史
if len(wq.History) > 0 {
dto.RecentHistory = make([]models.WrongQuestionHistoryDTO, len(wq.History))
for i, h := range wq.History {
dto.RecentHistory[i] = convertWrongHistoryToDTO(&h)
}
}
return dto
}
// convertWrongHistoryToDTO 转换历史记录为 DTO
func convertWrongHistoryToDTO(h *models.WrongQuestionHistory) models.WrongQuestionHistoryDTO {
return models.WrongQuestionHistoryDTO{
ID: h.ID,
UserAnswer: parseJSONAnswer(h.UserAnswer),
CorrectAnswer: parseJSONAnswer(h.CorrectAnswer),
AnsweredAt: h.AnsweredAt,
TimeSpent: h.TimeSpent,
IsCorrect: h.IsCorrect,
}
}
// parseJSONAnswer 解析 JSON 答案
func parseJSONAnswer(answerStr string) interface{} {
var answer interface{}
if err := json.Unmarshal([]byte(answerStr), &answer); err != nil {
return answerStr
}
return answer
}