AnCao/internal/handlers/practice_handler.go
yanlongqi 3b7133d9de 添加论述题权限控制系统和AI评分功能
本次更新实现了基于用户类型的论述题访问权限控制,并为论述题添加了专门的AI评分功能。

后端更新:
- 添加论述题权限验证:根据用户类型(ordinary-person/management-person)控制不同论述题的访问权限
- 新增 GradeEssay 方法:为论述题提供专门的AI评分,不依赖标准答案,基于保密法规进行专业评分
- 优化AI评分提示词:增加法规依据要求,返回参考答案、评分依据等更详细的评分信息
- 添加用户类型管理:新增 UpdateUserType API,支持用户更新个人类型
- 路由调整:将练习题相关API移至需要认证的路由组

前端更新:
- 论述题答题界面优化:不显示标准答案,展示AI评分的参考答案和评分依据
- 用户类型选择:登录/注册时支持选择用户类型
- 权限控制适配:根据用户类型显示对应的论述题列表

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 16:47:37 +08:00

988 lines
27 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"
"ankao/internal/services"
"encoding/json"
"fmt"
"log"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// checkEssayPermission 检查用户是否有权限访问论述题
// 返回 true 表示有权限false 表示无权限
func checkEssayPermission(c *gin.Context, questionType string) bool {
// 如果不是论述题,直接允许访问
if !strings.HasSuffix(questionType, "-essay") {
log.Printf("[论述题权限检查] 非论述题类型,直接允许访问 (type: %s)", questionType)
return true
}
log.Printf("[论述题权限检查] 开始检查 (question_type: %s)", questionType)
// 从上下文获取用户信息Auth中间件已设置
username, exists := c.Get("username")
if !exists {
log.Printf("[论述题权限检查] 失败: 未登录用户 (question_type: %s)", questionType)
return false
}
log.Printf("[论述题权限检查] 已登录用户 (username: %v, question_type: %s)", username, questionType)
// 管理员可以访问所有论述题
if username == "yanlongqi" {
log.Printf("[论述题权限检查] 通过: 管理员用户,允许访问所有论述题 (username: %s)", username)
return true
}
// 获取用户信息
db := database.GetDB()
var user models.User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
log.Printf("[论述题权限检查] 失败: 查询用户失败 (username: %v, error: %v)", username, err)
return false
}
log.Printf("[论述题权限检查] 用户信息 (username: %s, user_type: '%s', question_type: '%s')",
user.Username, user.UserType, questionType)
// 检查用户类型是否匹配
if questionType == "ordinary-essay" && user.UserType == "ordinary-person" {
log.Printf("[论述题权限检查] 通过: 普通涉密人员访问普通论述题 (username: %s)", user.Username)
return true
}
if questionType == "management-essay" && user.UserType == "management-person" {
log.Printf("[论述题权限检查] 通过: 保密管理人员访问管理论述题 (username: %s)", user.Username)
return true
}
log.Printf("[论述题权限检查] 失败: 类型不匹配 (username: %s, user_type: '%s', question_type: '%s')",
user.Username, user.UserType, questionType)
return false
}
// GetPracticeQuestions 获取练习题目列表
func GetPracticeQuestions(c *gin.Context) {
typeParam := c.Query("type")
searchQuery := c.Query("search")
log.Printf("[GetPracticeQuestions] 收到请求 (type: '%s', search: '%s')", typeParam, searchQuery)
db := database.GetDB()
var questions []models.PracticeQuestion
var total int64
query := db.Model(&models.PracticeQuestion{})
// 根据题型过滤
if typeParam != "" {
// 如果请求论述题,检查权限
if strings.HasSuffix(typeParam, "-essay") && !checkEssayPermission(c, typeParam) {
// 权限不足,返回空列表
log.Printf("[GetPracticeQuestions] 权限检查失败,返回空列表 (type: '%s')", typeParam)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": []models.PracticeQuestionDTO{},
"total": 0,
})
return
}
query = query.Where("type = ?", typeParam)
log.Printf("[GetPracticeQuestions] 添加题型过滤 (type: '%s')", typeParam)
}
// 根据搜索关键词过滤(搜索题目内容或题目编号)
if searchQuery != "" {
query = query.Where("question LIKE ? OR question_id LIKE ?", "%"+searchQuery+"%", "%"+searchQuery+"%")
}
// 获取总数
query.Count(&total)
log.Printf("[GetPracticeQuestions] 查询结果统计 (total: %d)", total)
// 查询所有题目 - 按题型和题目编号升序排序
// 先将 question_id 转为文本,提取数字部分,再转为整数排序
// 显式查询所有字段,包括 answer_data
err := query.Select("*").Order("type ASC, CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) ASC").Find(&questions).Error
if err != nil {
log.Printf("[GetPracticeQuestions] 查询失败 (error: %v)", err)
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "查询题目失败",
})
return
}
log.Printf("[GetPracticeQuestions] 查询成功,返回 %d 条数据", len(questions))
// 转换为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")
log.Printf("[GetRandomPracticeQuestion] 收到请求 (type: '%s')", typeParam)
db := database.GetDB()
var question models.PracticeQuestion
query := db.Model(&models.PracticeQuestion{})
if typeParam != "" {
// 如果请求论述题,检查权限
if strings.HasSuffix(typeParam, "-essay") && !checkEssayPermission(c, typeParam) {
// 权限不足,返回暂无题目
log.Printf("[GetRandomPracticeQuestion] 权限检查失败,返回暂无题目 (type: '%s')", typeParam)
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "暂无题目",
})
return
}
query = query.Where("type = ?", typeParam)
log.Printf("[GetRandomPracticeQuestion] 添加题型过滤 (type: '%s')", typeParam)
}
// 使用PostgreSQL的随机排序
if err := query.Order("RANDOM()").First(&question).Error; err != nil {
log.Printf("[GetRandomPracticeQuestion] 未找到题目 (type: '%s', error: %v)", typeParam, err)
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "暂无题目",
})
return
}
log.Printf("[GetRandomPracticeQuestion] 找到题目 (id: %d, type: '%s')", question.ID, question.Type)
// 转换为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, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, 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 strings.HasSuffix(question.Type, "-essay") {
log.Printf("[论述题] 跳过答案解析 (题目ID: %d, 类型: %s)", question.ID, question.Type)
correctAnswer = "" // 论述题没有标准答案
} else {
// 其他题型需要解析标准答案
if question.AnswerData == "" {
log.Printf("[错误] 题目缺少答案数据 (题目ID: %d, 类型: %s)", question.ID, question.Type)
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "题目答案数据缺失",
})
return
}
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil {
log.Printf("[错误] 答案数据解析失败 (题目ID: %d, 类型: %s, AnswerData: '%s', 错误: %v)",
question.ID, question.Type, question.AnswerData, err)
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "答案数据错误",
})
return
}
}
// 验证答案(论述题不需要验证,因为没有标准答案)
correct := false
if strings.HasSuffix(question.Type, "-essay") {
// 论述题默认为false实际结果由AI评分决定
correct = false
} else {
// 其他题型使用标准答案验证
correct = checkPracticeAnswer(question.Type, submit.Answer, correctAnswer)
}
// AI评分结果简答题和论述题
var aiGrading *models.AIGrading = nil
// 对简答题和论述题使用AI评分必须成功失败重试最多5次
if question.Type == "short-answer" || strings.HasSuffix(question.Type, "-essay") {
// 获取用户答案字符串
userAnswerStr, ok := submit.Answer.(string)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "答案格式错误",
})
return
}
// 调用AI评分服务带重试机制
aiService := services.NewAIGradingService()
var aiResult *services.AIGradingResult
var err error
maxRetries := 5
// 区分简答题和论述题的评分方式
if strings.HasSuffix(question.Type, "-essay") {
// 论述题:不需要标准答案,直接评分
for attempt := 1; attempt <= maxRetries; attempt++ {
log.Printf("论述题AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID)
aiResult, err = aiService.GradeEssay(question.Question, userAnswerStr)
if err == nil {
log.Printf("论述题AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score)
break
}
log.Printf("论述题AI评分失败 (第 %d 次尝试): %v", attempt, err)
if attempt < maxRetries {
// 等待一小段时间后重试(指数退避)
time.Sleep(time.Second * time.Duration(attempt))
}
}
} else {
// 简答题:需要标准答案对比
// 获取标准答案字符串
standardAnswerStr, ok := correctAnswer.(string)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "题目答案格式错误",
})
return
}
for attempt := 1; attempt <= maxRetries; attempt++ {
log.Printf("简答题AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID)
aiResult, err = aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr)
if err == nil {
log.Printf("简答题AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score)
break
}
log.Printf("简答题AI评分失败 (第 %d 次尝试): %v", attempt, err)
if attempt < maxRetries {
// 等待一小段时间后重试(指数退避)
time.Sleep(time.Second * time.Duration(attempt))
}
}
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": fmt.Sprintf("AI评分服务暂时不可用已重试%d次请稍后再试", maxRetries),
})
return
}
// 使用AI的评分结果
correct = aiResult.IsCorrect
aiGrading = &models.AIGrading{
Score: aiResult.Score,
Feedback: aiResult.Feedback,
Suggestion: aiResult.Suggestion,
ReferenceAnswer: aiResult.ReferenceAnswer,
ScoringRationale: aiResult.ScoringRationale,
}
}
// 记录用户答题历史
if uid, ok := userID.(uint); ok {
record := models.UserAnswerRecord{
UserID: uid,
QuestionID: question.ID,
IsCorrect: correct,
AnsweredAt: time.Now(),
}
// 记录到数据库(忽略错误,不影响主流程)
if err := db.Create(&record).Error; err != nil {
log.Printf("记录答题历史失败: %v", err)
}
}
// 如果答错,记录到错题本(论述题也可能答错)
if !correct {
if uid, ok := userID.(uint); ok {
// 记录错题
// 论述题没有 correctAnswer传 nil
wrongAnswer := correctAnswer
if strings.HasSuffix(question.Type, "-essay") {
wrongAnswer = "" // 论述题没有标准答案
}
if err := recordWrongQuestion(uid, question.ID, submit.Answer, wrongAnswer); err != nil {
// 记录错题失败不影响主流程,只记录日志
log.Printf("记录错题失败: %v", err)
}
}
}
// 构建返回结果
result := models.PracticeAnswerResult{
Correct: correct,
UserAnswer: submit.Answer,
AIGrading: aiGrading, // AI评分结果简答题和论述题
}
// 论述题不返回标准答案(因为没有固定答案)
if !strings.HasSuffix(question.Type, "-essay") {
result.CorrectAnswer = correctAnswer
} else {
result.CorrectAnswer = "" // 论述题返回空字符串
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": result,
})
}
// GetPracticeQuestionTypes 获取题型列表
func GetPracticeQuestionTypes(c *gin.Context) {
types := []gin.H{
{
"type": "fill-in-blank",
"type_name": "填空题",
},
{
"type": "true-false",
"type_name": "判断题",
},
{
"type": "multiple-choice",
"type_name": "选择题",
},
{
"type": "multiple-selection",
"type_name": "多选题",
},
{
"type": "short-answer",
"type_name": "简答题",
},
{
"type": "ordinary-essay",
"type_name": "普通涉密人员论述题",
},
{
"type": "management-essay",
"type_name": "保密管理人员论述题",
},
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": types,
})
}
// checkPracticeAnswer 检查练习答案是否正确
func checkPracticeAnswer(questionType string, userAnswer, correctAnswer interface{}) bool {
switch questionType {
case "true-false":
// 判断题: boolean 比较
userBool, ok1 := userAnswer.(bool)
correctBool, ok2 := correctAnswer.(bool)
return ok1 && ok2 && userBool == correctBool
case "multiple-choice":
// 单选题: 字符串比较
userStr, ok1 := userAnswer.(string)
correctStr, ok2 := correctAnswer.(string)
return ok1 && ok2 && userStr == correctStr
case "multiple-selection":
// 多选题: 数组比较
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 "fill-in-blank":
// 填空题: 数组比较
userArr, ok1 := toStringArray(userAnswer)
correctArr, ok2 := toStringArray(correctAnswer)
if !ok1 || !ok2 || len(userArr) != len(correctArr) {
log.Printf("填空题验证失败 - 数组转换或长度不匹配: ok1=%v, ok2=%v, userLen=%d, correctLen=%d",
ok1, ok2, len(userArr), len(correctArr))
return false
}
// 逐个比较填空答案(去除前后空格)
for i := range correctArr {
userTrimmed := strings.TrimSpace(userArr[i])
correctTrimmed := strings.TrimSpace(correctArr[i])
if userTrimmed != correctTrimmed {
log.Printf("填空题验证失败 - 第%d个答案不匹配: user='%s', correct='%s'",
i+1, userTrimmed, correctTrimmed)
return false
}
}
return true
case "short-answer":
// 简答题: 字符串比较(简单实现,实际可能需要更复杂的判断)
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
}
}
// convertToDTO 将数据库模型转换为前端DTO
func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
dto := models.PracticeQuestionDTO{
ID: question.ID,
QuestionID: question.QuestionID,
Type: question.Type, // 直接使用数据库中的type不做映射
Content: question.Question,
Category: question.TypeName, // 使用typeName作为分类显示
Options: []models.Option{},
}
// 解析答案数据
if question.AnswerData != "" {
// 对于简答题和论述题答案可能是纯字符串或JSON格式
if question.Type == "short-answer" || strings.HasSuffix(question.Type, "-essay") {
// 尝试JSON解析
var answer interface{}
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err != nil {
// JSON解析失败直接使用原始字符串
dto.Answer = question.AnswerData
} else {
// JSON解析成功
dto.Answer = answer
}
} else {
// 其他题型必须是JSON格式
var answer interface{}
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err != nil {
log.Printf("[convertToDTO] 解析答案失败 (id: %d, type: %s, error: %v)",
question.ID, question.Type, err)
} else {
dto.Answer = answer
}
}
}
// 判断题自动生成选项(正确在前,错误在后)
if question.Type == "true-false" {
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数组并按key排序
keys := make([]string, 0, len(optionsMap))
for key := range optionsMap {
keys = append(keys, key)
}
// 对keys进行排序
sort.Strings(keys)
// 按排序后的key顺序添加选项
for _, key := range keys {
dto.Options = append(dto.Options, models.Option{
Key: key,
Value: optionsMap[key],
})
}
}
}
return dto
}
// GetStatistics 获取用户统计数据
func GetStatistics(c *gin.Context) {
// 获取用户ID
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}
uid, ok := userID.(uint)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "用户ID格式错误",
})
return
}
db := database.GetDB()
// 获取题库总数
var totalQuestions int64
db.Model(&models.PracticeQuestion{}).Count(&totalQuestions)
// 获取用户已答题数(去重)
var answeredQuestions int64
db.Model(&models.UserAnswerRecord{}).
Where("user_id = ?", uid).
Distinct("question_id").
Count(&answeredQuestions)
// 获取用户答对题数
var correctAnswers int64
db.Model(&models.UserAnswerRecord{}).
Where("user_id = ? AND is_correct = ?", uid, true).
Count(&correctAnswers)
// 获取用户错题数量(所有错题,包括已掌握和未掌握的)
var wrongQuestions int64
db.Model(&models.WrongQuestion{}).
Where("user_id = ?", uid).
Count(&wrongQuestions)
// 计算正确率
var accuracy float64
if answeredQuestions > 0 {
// 正确率 = 答对题数 / 总答题数
var totalAnswers int64
db.Model(&models.UserAnswerRecord{}).
Where("user_id = ?", uid).
Count(&totalAnswers)
accuracy = float64(correctAnswers) / float64(totalAnswers) * 100
}
stats := models.UserStatistics{
TotalQuestions: int(totalQuestions),
AnsweredQuestions: int(answeredQuestions),
CorrectAnswers: int(correctAnswers),
WrongQuestions: int(wrongQuestions),
Accuracy: accuracy,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
// CreatePracticeQuestion 创建新的练习题目
func CreatePracticeQuestion(c *gin.Context) {
var req struct {
Type string `json:"type" binding:"required"`
TypeName string `json:"type_name"`
Question string `json:"question" binding:"required"`
Answer interface{} `json:"answer" binding:"required"`
Options map[string]string `json:"options"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误: " + err.Error(),
})
return
}
db := database.GetDB()
// 自动生成题目编号:找到该题型的最大编号并+1
var maxQuestionID string
err := db.Model(&models.PracticeQuestion{}).
Where("type = ?", req.Type).
Select("question_id").
Order("CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) DESC").
Limit(1).
Pluck("question_id", &maxQuestionID).Error
// 生成新的题目编号
var newQuestionID string
if err != nil || maxQuestionID == "" {
// 没有找到该题型的题目从1开始
newQuestionID = "1"
} else {
// 从最大编号中提取数字并+1
var maxNum int
_, scanErr := strconv.Atoi(maxQuestionID)
if scanErr == nil {
maxNum, _ = strconv.Atoi(maxQuestionID)
}
newQuestionID = strconv.Itoa(maxNum + 1)
}
// 将答案序列化为JSON字符串
answerData, err := json.Marshal(req.Answer)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "答案格式错误",
})
return
}
// 将选项序列化为JSON字符串
var optionsData string
if req.Options != nil && len(req.Options) > 0 {
optionsBytes, err := json.Marshal(req.Options)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "选项格式错误",
})
return
}
optionsData = string(optionsBytes)
}
question := models.PracticeQuestion{
QuestionID: newQuestionID,
Type: req.Type,
TypeName: req.TypeName,
Question: req.Question,
AnswerData: string(answerData),
OptionsData: optionsData,
}
if err := db.Create(&question).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "创建题目失败",
})
return
}
// 返回创建的题目
dto := convertToDTO(question)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dto,
"message": "创建成功",
})
}
// UpdatePracticeQuestion 更新练习题目
func UpdatePracticeQuestion(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
}
var req struct {
Type string `json:"type"`
TypeName string `json:"type_name"`
Question string `json:"question"`
Answer interface{} `json:"answer"`
Options map[string]string `json:"options"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误: " + err.Error(),
})
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
}
// 更新字段(注意:不允许修改 QuestionID由系统自动生成
if req.Type != "" {
question.Type = req.Type
}
if req.TypeName != "" {
question.TypeName = req.TypeName
}
if req.Question != "" {
question.Question = req.Question
}
if req.Answer != nil {
answerData, err := json.Marshal(req.Answer)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "答案格式错误",
})
return
}
question.AnswerData = string(answerData)
}
if req.Options != nil {
optionsBytes, err := json.Marshal(req.Options)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "选项格式错误",
})
return
}
question.OptionsData = string(optionsBytes)
}
// 保存更新
if err := db.Save(&question).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "更新题目失败",
})
return
}
// 返回更新后的题目
dto := convertToDTO(question)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": dto,
"message": "更新成功",
})
}
// DeletePracticeQuestion 删除练习题目
func DeletePracticeQuestion(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
}
// 删除题目
if err := db.Delete(&question).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "删除题目失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "删除成功",
})
}
// ExplainQuestion 生成题目解析
func ExplainQuestion(c *gin.Context) {
var req struct {
QuestionID uint `json:"question_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "请求参数错误",
})
return
}
db := database.GetDB()
var question models.PracticeQuestion
// 查询题目
if err := db.First(&question, req.QuestionID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "题目不存在",
})
return
}
// 解析标准答案
var standardAnswer interface{}
if err := json.Unmarshal([]byte(question.AnswerData), &standardAnswer); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "解析题目答案失败",
})
return
}
// 将标准答案转为字符串
var standardAnswerStr string
switch v := standardAnswer.(type) {
case string:
standardAnswerStr = v
case []interface{}:
// 多选题或填空题,将数组转为逗号分隔的字符串
parts := make([]string, len(v))
for i, item := range v {
parts[i] = fmt.Sprint(item)
}
standardAnswerStr = strings.Join(parts, ", ")
case bool:
// 判断题
if v {
standardAnswerStr = "正确"
} else {
standardAnswerStr = "错误"
}
default:
standardAnswerStr = fmt.Sprint(v)
}
// 设置SSE响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
// 调用AI服务生成流式解析
aiService := services.NewAIGradingService()
err := aiService.ExplainQuestionStream(c.Writer, question.Question, standardAnswerStr, question.Type)
if err != nil {
log.Printf("AI流式解析失败: %v", err)
// SSE格式的错误消息
fmt.Fprintf(c.Writer, "data: {\"error\": \"生成解析失败\"}\n\n")
c.Writer.(http.Flusher).Flush()
}
}