AnCao/internal/handlers/practice_handler.go
yanlongqi a7ede7692f 实现完整的题目练习功能模块
- 后端功能:
  * 新增练习题数据模型和数据库表结构
  * 实现题目列表、随机题目、提交答案等API接口
  * 支持5种题型:单选、多选、判断、填空、简答
  * 判断题自动生成"对/错"选项
  * 前后端类型映射(single/multiple/judge/fill/short)

- 前端功能:
  * 新增首页,展示5种题型选择卡片和统计信息
  * 完善答题页面,支持所有题型的渲染和答题
  * 填空题特殊渲染:将****替换为横线输入框
  * 实现题目列表、筛选、随机练习等功能
  * 优化底部导航,添加首页、答题、我的三个标签

- 工具脚本:
  * 新增题目数据导入脚本
  * 支持从JSON文件批量导入题库

- 文档更新:
  * 更新CLAUDE.md和README.md,记录新增功能

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 02:39:18 +08:00

365 lines
8.5 KiB
Go

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
}
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)
result := models.PracticeAnswerResult{
Correct: correct,
UserAnswer: submit.Answer,
}
// 如果答案错误,返回正确答案
if !correct {
result.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,
Type: mapBackendToFrontendType(question.Type),
Content: question.Question,
Category: question.TypeName,
Options: []models.Option{},
}
// 判断题自动生成选项
if question.Type == models.TrueFalseType {
dto.Options = []models.Option{
{Key: "A", Value: "对"},
{Key: "B", 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
}