- 后端功能: * 新增练习题数据模型和数据库表结构 * 实现题目列表、随机题目、提交答案等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>
365 lines
8.5 KiB
Go
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
|
|
}
|