AnCao/internal/handlers/practice_handler.go
yanlongqi 6446508954 实现完整的错题本功能模块
后端实现:
- 创建错题数据模型和数据库表结构
- 实现错题记录、查询、统计、标记和清空API
- 答题错误时自动记录到错题本
- 支持重复错误累计次数和更新时间

前端实现:
- 创建错题本页面,支持查看、筛选和管理错题
- 实现错题统计展示(总数、已掌握、待掌握)
- 支持标记已掌握、清空错题本和重做题目
- 在首页和个人中心添加错题本入口
- 完整的响应式设计适配移动端和PC端

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 13:44:51 +08:00

373 lines
8.8 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 handlers
import (
"ankao/internal/database"
"ankao/internal/models"
"encoding/json"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// GetPracticeQuestions 获取练习题目列表
func GetPracticeQuestions(c *gin.Context) {
typeParam := c.Query("type")
category := c.Query("category")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 100
}
db := database.GetDB()
var questions []models.PracticeQuestion
var total int64
query := db.Model(&models.PracticeQuestion{})
// 根据题型过滤 - 将前端类型映射到后端类型
if typeParam != "" {
backendType := mapFrontendToBackendType(typeParam)
query = query.Where("type = ?", backendType)
}
// 根据分类过滤
if category != "" {
query = query.Where("type_name = ?", category)
}
// 获取总数
query.Count(&total)
// 分页查询
offset := (page - 1) * pageSize
err := query.Offset(offset).Limit(pageSize).Find(&questions).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "查询题目失败",
})
return
}
// 转换为DTO
dtos := make([]models.PracticeQuestionDTO, len(questions))
for i, q := range questions {
dto := convertToDTO(q)
dtos[i] = dto
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dtos,
"total": total,
})
}
// GetPracticeQuestionByID 获取单个练习题目
func GetPracticeQuestionByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "无效的题目ID",
})
return
}
db := database.GetDB()
var question models.PracticeQuestion
if err := db.First(&question, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "题目不存在",
})
return
}
// 转换为DTO
dto := convertToDTO(question)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dto,
})
}
// GetRandomPracticeQuestion 获取随机练习题目
func GetRandomPracticeQuestion(c *gin.Context) {
typeParam := c.Query("type")
db := database.GetDB()
var question models.PracticeQuestion
query := db.Model(&models.PracticeQuestion{})
if typeParam != "" {
backendType := mapFrontendToBackendType(typeParam)
query = query.Where("type = ?", backendType)
}
// 使用PostgreSQL的随机排序
if err := query.Order("RANDOM()").First(&question).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "暂无题目",
})
return
}
// 转换为DTO
dto := convertToDTO(question)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dto,
})
}
// SubmitPracticeAnswer 提交练习答案
func SubmitPracticeAnswer(c *gin.Context) {
var submit models.PracticeAnswerSubmit
if err := c.ShouldBindJSON(&submit); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误",
})
return
}
// 获取用户ID如果已登录
userID, _ := c.Get("user_id")
db := database.GetDB()
var question models.PracticeQuestion
if err := db.First(&question, submit.QuestionID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "题目不存在",
})
return
}
// 解析正确答案
var correctAnswer interface{}
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "答案数据错误",
})
return
}
// 验证答案
correct := checkPracticeAnswer(question.Type, submit.Answer, correctAnswer)
// 如果答错且用户已登录,记录到错题本
if !correct && userID != nil {
if uid, ok := userID.(uint); ok {
// 异步记录错题,不影响主流程
go recordWrongQuestion(uid, question.ID, submit.Answer, correctAnswer)
}
}
result := models.PracticeAnswerResult{
Correct: correct,
UserAnswer: submit.Answer,
CorrectAnswer: correctAnswer, // 始终返回正确答案
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": result,
})
}
// GetPracticeQuestionTypes 获取题型列表
func GetPracticeQuestionTypes(c *gin.Context) {
types := []gin.H{
{
"type": models.FillInBlank,
"type_name": "填空题",
},
{
"type": models.TrueFalseType,
"type_name": "判断题",
},
{
"type": models.MultipleChoiceQ,
"type_name": "选择题",
},
{
"type": models.MultipleSelection,
"type_name": "多选题",
},
{
"type": models.ShortAnswer,
"type_name": "简答题",
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": types,
})
}
// checkPracticeAnswer 检查练习答案是否正确
func checkPracticeAnswer(questionType models.PracticeQuestionType, userAnswer, correctAnswer interface{}) bool {
switch questionType {
case models.TrueFalseType:
// 判断题: boolean 比较
userBool, ok1 := userAnswer.(bool)
correctBool, ok2 := correctAnswer.(bool)
return ok1 && ok2 && userBool == correctBool
case models.MultipleChoiceQ:
// 单选题: 字符串比较
userStr, ok1 := userAnswer.(string)
correctStr, ok2 := correctAnswer.(string)
return ok1 && ok2 && userStr == correctStr
case models.MultipleSelection:
// 多选题: 数组比较
userArr, ok1 := toStringArray(userAnswer)
correctArr, ok2 := toStringArray(correctAnswer)
if !ok1 || !ok2 || len(userArr) != len(correctArr) {
return false
}
// 转换为map进行比较
userMap := make(map[string]bool)
for _, v := range userArr {
userMap[v] = true
}
for _, v := range correctArr {
if !userMap[v] {
return false
}
}
return true
case models.FillInBlank:
// 填空题: 数组比较
userArr, ok1 := toStringArray(userAnswer)
correctArr, ok2 := toStringArray(correctAnswer)
if !ok1 || !ok2 || len(userArr) != len(correctArr) {
return false
}
// 逐个比较填空答案
for i := range correctArr {
if userArr[i] != correctArr[i] {
return false
}
}
return true
case models.ShortAnswer:
// 简答题: 字符串比较(简单实现,实际可能需要更复杂的判断)
userStr, ok1 := userAnswer.(string)
correctStr, ok2 := correctAnswer.(string)
return ok1 && ok2 && userStr == correctStr
}
return false
}
// toStringArray 将interface{}转换为字符串数组
func toStringArray(v interface{}) ([]string, bool) {
switch arr := v.(type) {
case []string:
return arr, true
case []interface{}:
result := make([]string, len(arr))
for i, item := range arr {
if str, ok := item.(string); ok {
result[i] = str
} else {
return nil, false
}
}
return result, true
default:
return nil, false
}
}
// mapFrontendToBackendType 将前端类型映射到后端类型
func mapFrontendToBackendType(frontendType string) models.PracticeQuestionType {
typeMap := map[string]models.PracticeQuestionType{
"single": models.MultipleChoiceQ, // 单选
"multiple": models.MultipleSelection, // 多选
"judge": models.TrueFalseType, // 判断
"fill": models.FillInBlank, // 填空
"short": models.ShortAnswer, // 简答
}
if backendType, ok := typeMap[frontendType]; ok {
return backendType
}
return models.MultipleChoiceQ // 默认返回单选
}
// mapBackendToFrontendType 将后端类型映射到前端类型
func mapBackendToFrontendType(backendType models.PracticeQuestionType) string {
typeMap := map[models.PracticeQuestionType]string{
models.MultipleChoiceQ: "single", // 单选
models.MultipleSelection: "multiple", // 多选
models.TrueFalseType: "judge", // 判断
models.FillInBlank: "fill", // 填空
models.ShortAnswer: "short", // 简答
}
if frontendType, ok := typeMap[backendType]; ok {
return frontendType
}
return "single" // 默认返回单选
}
// convertToDTO 将数据库模型转换为前端DTO
func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
dto := models.PracticeQuestionDTO{
ID: question.ID,
QuestionID: question.QuestionID, // 添加题目编号
Type: mapBackendToFrontendType(question.Type),
Content: question.Question,
Category: question.TypeName,
Options: []models.Option{},
}
// 判断题自动生成选项
if question.Type == models.TrueFalseType {
dto.Options = []models.Option{
{Key: "true", Value: "正确"},
{Key: "false", Value: "错误"},
}
return dto
}
// 解析选项数据(如果有)
if question.OptionsData != "" {
var optionsMap map[string]string
if err := json.Unmarshal([]byte(question.OptionsData), &optionsMap); err == nil {
// 将map转换为Option数组
for key, value := range optionsMap {
dto.Options = append(dto.Options, models.Option{
Key: key,
Value: value,
})
}
}
}
return dto
}