diff --git a/internal/handlers/practice_handler.go b/internal/handlers/practice_handler.go index 979c720..d686ce5 100644 --- a/internal/handlers/practice_handler.go +++ b/internal/handlers/practice_handler.go @@ -16,11 +16,65 @@ import ( "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 @@ -29,7 +83,19 @@ func GetPracticeQuestions(c *gin.Context) { // 根据题型过滤 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) } // 根据搜索关键词过滤(搜索题目内容或题目编号) @@ -40,10 +106,14 @@ func GetPracticeQuestions(c *gin.Context) { // 获取总数 query.Count(&total) + log.Printf("[GetPracticeQuestions] 查询结果统计 (total: %d)", total) + // 查询所有题目 - 按题型和题目编号升序排序 // 先将 question_id 转为文本,提取数字部分,再转为整数排序 - err := query.Order("type ASC, CAST(COALESCE(NULLIF(REGEXP_REPLACE(question_id::text, '[^0-9]', '', 'g'), ''), '0') AS INTEGER) ASC").Find(&questions).Error + // 显式查询所有字段,包括 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": "查询题目失败", @@ -51,6 +121,8 @@ func GetPracticeQuestions(c *gin.Context) { return } + log.Printf("[GetPracticeQuestions] 查询成功,返回 %d 条数据", len(questions)) + // 转换为DTO dtos := make([]models.PracticeQuestionDTO, len(questions)) for i, q := range questions { @@ -101,16 +173,30 @@ func GetPracticeQuestionByID(c *gin.Context) { 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": "暂无题目", @@ -118,6 +204,8 @@ func GetRandomPracticeQuestion(c *gin.Context) { return } + log.Printf("[GetRandomPracticeQuestion] 找到题目 (id: %d, type: '%s')", question.ID, question.Type) + // 转换为DTO dto := convertToDTO(question) @@ -159,40 +247,56 @@ func SubmitPracticeAnswer(c *gin.Context) { return } - // 解析正确答案 + // 解析正确答案(论述题不需要标准答案) var correctAnswer interface{} - if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "答案数据错误", - }) - return + + // 论述题跳过答案解析 + 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 := checkPracticeAnswer(question.Type, submit.Answer, correctAnswer) + // 验证答案(论述题不需要验证,因为没有标准答案) + correct := false + if strings.HasSuffix(question.Type, "-essay") { + // 论述题默认为false,实际结果由AI评分决定 + correct = false + } else { + // 其他题型使用标准答案验证 + correct = checkPracticeAnswer(question.Type, submit.Answer, correctAnswer) + } - // AI评分结果(仅简答题) + // AI评分结果(简答题和论述题) var aiGrading *models.AIGrading = nil - // 对简答题使用AI评分(必须成功,失败重试最多5次) - if question.Type == "short-answer" { + // 对简答题和论述题使用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 - } - - // 获取标准答案字符串 - standardAnswerStr, ok := correctAnswer.(string) - if !ok { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": "题目答案格式错误", + "message": "答案格式错误", }) return } @@ -203,17 +307,46 @@ func SubmitPracticeAnswer(c *gin.Context) { var err error maxRetries := 5 - 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 + // 区分简答题和论述题的评分方式 + 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)) + } } - 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)) + } } } @@ -228,9 +361,11 @@ func SubmitPracticeAnswer(c *gin.Context) { // 使用AI的评分结果 correct = aiResult.IsCorrect aiGrading = &models.AIGrading{ - Score: aiResult.Score, - Feedback: aiResult.Feedback, - Suggestion: aiResult.Suggestion, + Score: aiResult.Score, + Feedback: aiResult.Feedback, + Suggestion: aiResult.Suggestion, + ReferenceAnswer: aiResult.ReferenceAnswer, + ScoringRationale: aiResult.ScoringRationale, } } @@ -248,22 +383,34 @@ func SubmitPracticeAnswer(c *gin.Context) { } } - // 如果答错,记录到错题本 + // 如果答错,记录到错题本(论述题也可能答错) if !correct { if uid, ok := userID.(uint); ok { // 记录错题 - if err := recordWrongQuestion(uid, question.ID, submit.Answer, correctAnswer); err != nil { + // 论述题没有 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, - CorrectAnswer: correctAnswer, // 始终返回正确答案 - AIGrading: aiGrading, // AI评分结果(仅简答题有值) + 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{ @@ -295,6 +442,14 @@ func GetPracticeQuestionTypes(c *gin.Context) { "type": "short-answer", "type_name": "简答题", }, + { + "type": "ordinary-essay", + "type_name": "普通涉密人员论述题", + }, + { + "type": "management-essay", + "type_name": "保密管理人员论述题", + }, } c.JSON(http.StatusOK, gin.H{ @@ -402,9 +557,28 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO { } // 解析答案数据 - var answer interface{} - if err := json.Unmarshal([]byte(question.AnswerData), &answer); err == nil { - dto.Answer = answer + 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 + } + } } // 判断题自动生成选项(正确在前,错误在后) diff --git a/internal/handlers/user.go b/internal/handlers/user.go index 032444c..cfd0466 100644 --- a/internal/handlers/user.go +++ b/internal/handlers/user.go @@ -81,8 +81,12 @@ func Login(c *gin.Context) { Username: user.Username, Avatar: user.Avatar, Nickname: user.Nickname, + UserType: user.UserType, // 返回用户类型 } + // 检查用户类型是否为空,如果为空,标识需要补充 + needUserType := user.UserType == "" + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "登录成功", @@ -90,6 +94,7 @@ func Login(c *gin.Context) { Token: token, User: userInfo, }, + "need_user_type": needUserType, // 添加标识,前端根据此标识显示补充弹窗 }) } @@ -123,6 +128,7 @@ func Register(c *gin.Context) { newUser := models.User{ Username: req.Username, Nickname: req.Nickname, + UserType: req.UserType, // 保存用户类型 Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=" + req.Username, // 使用用户名生成默认头像 } @@ -162,6 +168,7 @@ func Register(c *gin.Context) { Username: newUser.Username, Avatar: newUser.Avatar, Nickname: newUser.Nickname, + UserType: newUser.UserType, // 返回用户类型 } c.JSON(http.StatusOK, gin.H{ @@ -173,3 +180,67 @@ func Register(c *gin.Context) { }, }) } + +// UpdateUserTypeRequest 更新用户类型请求 +type UpdateUserTypeRequest struct { + UserType string `json:"user_type" binding:"required,oneof=ordinary-person management-person"` +} + +// UpdateUserType 更新用户类型 +func UpdateUserType(c *gin.Context) { + var req UpdateUserTypeRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "请求参数错误,用户类型必须是 ordinary-person 或 management-person", + "error": err.Error(), + }) + return + } + + // 从上下文获取用户信息(由认证中间件设置) + username, exists := c.Get("username") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "未授权访问", + }) + return + } + + db := database.GetDB() + var user models.User + if err := db.Where("username = ?", username).First(&user).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{ + "success": false, + "message": "用户不存在", + }) + return + } + + // 更新用户类型 + user.UserType = req.UserType + if err := db.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "更新用户类型失败", + "error": err.Error(), + }) + return + } + + // 返回更新后的用户信息 + userInfo := models.UserInfoResponse{ + Username: user.Username, + Avatar: user.Avatar, + Nickname: user.Nickname, + UserType: user.UserType, + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "用户类型更新成功", + "data": userInfo, + }) +} diff --git a/internal/models/practice_question.go b/internal/models/practice_question.go index b885b56..18d6522 100644 --- a/internal/models/practice_question.go +++ b/internal/models/practice_question.go @@ -45,7 +45,9 @@ type PracticeAnswerResult struct { // AIGrading AI评分结果 type AIGrading struct { - Score float64 `json:"score"` // 得分 (0-100) - Feedback string `json:"feedback"` // 评语 - Suggestion string `json:"suggestion"` // 改进建议 + Score float64 `json:"score"` // 得分 (0-100) + Feedback string `json:"feedback"` // 评语 + Suggestion string `json:"suggestion"` // 改进建议 + ReferenceAnswer string `json:"reference_answer,omitempty"` // 参考答案(论述题) + ScoringRationale string `json:"scoring_rationale,omitempty"` // 评分依据 } diff --git a/internal/models/user.go b/internal/models/user.go index da5e842..e21e8c6 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -15,6 +15,7 @@ type User struct { Token string `gorm:"size:255;index" json:"-"` // 用户登录token Avatar string `gorm:"size:255" json:"avatar"` Nickname string `gorm:"size:50" json:"nickname"` + UserType string `gorm:"size:50" json:"user_type"` // 用户类型: ordinary-person 或 management-person CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` @@ -56,6 +57,7 @@ type UserInfoResponse struct { Username string `json:"username"` Avatar string `json:"avatar"` Nickname string `json:"nickname"` + UserType string `json:"user_type"` // 用户类型 } // RegisterRequest 注册请求 @@ -63,4 +65,5 @@ type RegisterRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required,min=6"` Nickname string `json:"nickname"` + UserType string `json:"user_type" binding:"required,oneof=ordinary-person management-person"` // 用户类型,必填 } diff --git a/internal/services/ai_grading.go b/internal/services/ai_grading.go index e011362..0311639 100644 --- a/internal/services/ai_grading.go +++ b/internal/services/ai_grading.go @@ -36,49 +36,71 @@ func NewAIGradingService() *AIGradingService { // AIGradingResult AI评分结果 type AIGradingResult struct { - Score float64 `json:"score"` // 得分 (0-100) - IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确) - Feedback string `json:"feedback"` // 评语 - Suggestion string `json:"suggestion"` // 改进建议 + Score float64 `json:"score"` // 得分 (0-100) + IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确) + Feedback string `json:"feedback"` // 评语 + Suggestion string `json:"suggestion"` // 改进建议 + ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题) + ScoringRationale string `json:"scoring_rationale"` // 评分依据 } -// GradeShortAnswer 对简答题进行AI评分 +// GradeEssay 对论述题进行AI评分(不需要标准答案) // question: 题目内容 -// standardAnswer: 标准答案 // userAnswer: 用户答案 -func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) { - // 构建评分提示词(严格评分模式) - prompt := fmt.Sprintf(`你是一位严格的阅卷老师,请严格按照标准答案对以下简答题进行评分。 +func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) { + // 构建评分提示词(论述题评分模式) + prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请对以下论述题进行评分。 题目:%s -标准答案:%s - 学生答案:%s -评分标准(请严格遵守): -1. 必须与标准答案进行逐项对比 -2. 答案要点完全覆盖标准答案且表述准确的,给85-100分 -3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分 -4. 答案要点缺失较多或有明显错误的,给40-59分 -5. 答案完全错误或离题的,给0-39分 -6. 判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false) +评分依据: +请依据以下保密法律法规和管理制度进行分析和评分: +1. 《中华人民共和国保守国家秘密法》 +2. 《中华人民共和国保守国家秘密法实施条例》 +3. 《保密工作管理制度2025.9.9》 +4. 《软件开发管理制度》 +5. 《涉密信息系统集成资质保密标准》 +6. 《涉密信息系统集成资质管理办法》 + +评分标准(论述题没有固定标准答案,请根据答题质量和法规符合度评分): +1. 论点是否明确,是否符合保密法规要求(30分) +2. 内容是否充实,论据是否引用相关法规条文(30分) +3. 逻辑是否严密,分析是否符合保密工作实际(25分) +4. 语言表达是否准确、专业(15分) + +评分等级: +- 85-100分:论述优秀,论点明确、论据充分、符合法规要求、分析专业 +- 70-84分:论述良好,基本要素齐全,符合保密工作要求 +- 60-69分:论述基本合格,要点基本涵盖但不够深入 +- 40-59分:论述不够完整,缺乏法规支撑或逻辑性较差 +- 0-39分:论述严重缺失或完全离题 + +判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false) 评分要求: 1. 给出一个0-100的精确分数 2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false) -3. 给出简短的评语(说明得分和失分原因,不超过50字) -4. 给出具体的改进建议(如果答案满分可以为空,否则必须指出具体改进方向,不超过50字) +3. 生成一个专业的参考答案(reference_answer,150-300字,必须引用相关法规条文) +4. 给出评分依据(scoring_rationale,说明依据了哪些法规和条文,80-150字) +5. 给出简短的评语(feedback,说明得分情况,不超过80字) +6. 给出具体的改进建议(suggestion,如果分数在90分以上可以简短,否则必须指出具体改进方向,不超过80字) 请按照以下JSON格式返回结果: { - "score": 85, + "score": 75, "is_correct": true, - "feedback": "答案覆盖了主要要点,但XXX部分描述不够准确", - "suggestion": "建议补充XXX内容,并完善XXX的描述" + "reference_answer": "根据《中华人民共和国保守国家秘密法》第XX条...", + "scoring_rationale": "依据《保密法》第XX条、《保密法实施条例》第XX条...", + "feedback": "论述较为完整,论点明确,但论据不够充分,缺少具体法规引用", + "suggestion": "建议补充《保密法》相关条文,加强论点之间的逻辑联系" } -注意:只返回JSON格式的结果,不要有其他内容。必须严格对照标准答案评分,不要过于宽松。`, question, standardAnswer, userAnswer) +注意: +1. 只返回JSON格式的结果,不要有其他内容 +2. 参考答案必须专业、准确,体现保密法规要求 +3. 评分依据必须具体引用法规条文`, question, userAnswer) // 调用AI API resp, err := s.client.CreateChatCompletion( @@ -88,7 +110,97 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleSystem, - Content: "你是一位严格的阅卷老师,必须严格按照标准答案进行评分,不能过于宽松。你的评分标准是客观的、一致的、可预测的。", + Content: "你是一位专业的保密领域阅卷老师,精通保密法律法规。你的评分客观公正,既要有专业要求,也要合理宽容。你必须基于保密法规进行评分,并在参考答案和评分依据中引用具体的法规条文。", + }, + { + Role: openai.ChatMessageRoleUser, + Content: prompt, + }, + }, + Temperature: 0.3, // 低温度,保证评分相对稳定 + }, + ) + + if err != nil { + return nil, fmt.Errorf("AI评分失败: %w", err) + } + + if len(resp.Choices) == 0 { + return nil, fmt.Errorf("AI未返回评分结果") + } + + // 解析AI返回的JSON结果 + content := resp.Choices[0].Message.Content + + var result AIGradingResult + if err := parseAIResponse(content, &result); err != nil { + return nil, fmt.Errorf("解析AI响应失败: %w", err) + } + + return &result, nil +} + +// GradeShortAnswer 对简答题进行AI评分 +// question: 题目内容 +// standardAnswer: 标准答案 +// userAnswer: 用户答案 +func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) { + // 构建评分提示词(严格评分模式) + prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请严格按照标准答案对以下简答题进行评分。 + +题目:%s + +标准答案:%s + +学生答案:%s + +评分依据: +请依据以下保密法律法规和管理制度进行分析和评分: +1. 《中华人民共和国保守国家秘密法》 +2. 《中华人民共和国保守国家秘密法实施条例》 +3. 《保密工作管理制度2025.9.9》 +4. 《软件开发管理制度》 +5. 《涉密信息系统集成资质保密标准》 +6. 《涉密信息系统集成资质管理办法》 + +评分标准(请严格遵守): +1. 必须与标准答案进行逐项对比 +2. 答案要点完全覆盖标准答案且表述准确、符合法规要求的,给85-100分 +3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分 +4. 答案要点缺失较多或有明显错误的,给40-59分 +5. 答案完全错误或离题的,给0-39分 +6. 判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false) + +评分要求: +1. 给出一个0-100的精确分数 +2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false) +3. 给出评分依据(scoring_rationale,说明依据了哪些法规和标准答案的哪些要点,80-150字) +4. 给出简短的评语(feedback,说明得分和失分原因,不超过80字) +5. 给出具体的改进建议(suggestion,如果答案满分可以为空,否则必须指出具体改进方向,不超过80字) + +请按照以下JSON格式返回结果: +{ + "score": 85, + "is_correct": true, + "scoring_rationale": "依据《保密法》第XX条和标准答案要点分析...", + "feedback": "答案覆盖了主要要点,但XXX部分描述不够准确", + "suggestion": "建议补充XXX内容,并完善XXX的描述" +} + +注意: +1. 只返回JSON格式的结果,不要有其他内容 +2. 必须严格对照标准答案评分,不要过于宽松 +3. 评分依据必须说明符合或违反了哪些法规要求`, question, standardAnswer, userAnswer) + + // 调用AI API + resp, err := s.client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: s.config.Model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: "你是一位专业的保密领域阅卷老师,精通保密法律法规。必须严格按照标准答案进行评分,不能过于宽松。你的评分标准是客观的、一致的、可预测的,并且必须基于保密法规进行专业分析。", }, { Role: openai.ChatMessageRoleUser, @@ -143,7 +255,7 @@ type AIExplanationResult struct { // questionType: 题目类型 func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error { // 构建解析提示词(直接输出,不使用JSON格式) - prompt := fmt.Sprintf(`你是一位严谨、专业的老师,请对以下题目进行详细解析。 + prompt := fmt.Sprintf(`你是一位严谨、专业的保密领域专家老师,请对以下题目进行详细解析。 题目类型:%s @@ -151,30 +263,46 @@ func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, que 标准答案:%s +**重要依据**:请基于以下保密法律法规和管理制度进行专业分析: +1. 《中华人民共和国保守国家秘密法》 +2. 《中华人民共和国保守国家秘密法实施条例》 +3. 《保密工作管理制度2025.9.9》 +4. 《软件开发管理制度》 +5. 《涉密信息系统集成资质保密标准》 +6. 《涉密信息系统集成资质管理办法》 + 请提供一个详细的解析,要求: -1. **必须实事求是**:只基于题目内容和标准答案进行解析,不要添加题目中没有的信息 -2. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造 -3. **使用Markdown格式**:使用标题、列表、加粗等markdown语法使内容更清晰易读 +1. **必须基于保密法规**:解析时必须引用相关法规条文,说明依据哪些具体法律法规 +2. **必须实事求是**:只基于题目内容、标准答案和实际法规进行解析 +3. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造法规条文 +4. **使用Markdown格式**:使用标题、列表、加粗等markdown语法使内容更清晰易读 解析内容要求: -- **知识点**:说明题目考查的核心知识点 -- **解题思路**:提供清晰的解题步骤和方法 +- **知识点**:说明题目考查的核心知识点,指出涉及哪些保密法规 +- **法规依据**:明确引用相关法律法规的具体条文(如:《保密法》第X条、《保密法实施条例》第X条等) +- **解题思路**:提供清晰的解题步骤和方法,结合保密工作实际 %s 示例输出格式: ## 知识点 -题目考查的是... +本题考查的是[知识点名称],涉及《XX法规》第XX条... + +## 法规依据 +- 《中华人民共和国保守国家秘密法》第XX条规定:... +- 《保密工作管理制度2025.9.9》第X章第X节:... ## 解题思路 -1. 首先分析... -2. 然后判断... +1. 首先根据《XX法规》第XX条,我们可以判断... +2. 然后结合保密工作实际,分析... %s ## 总结 %s +**重要提醒**:请务必在解析中引用具体的法规条文,不要空泛地提及法规名称。如果不确定具体条文编号,可以说明法规的精神和要求。 + 请使用markdown格式输出解析内容。`, questionType, question, @@ -182,32 +310,32 @@ func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, que // 根据题目类型添加特定要求 func() string { if questionType == "single-selection" || questionType == "multiple-selection" { - return `- **选项分析**:对于选择题,必须逐项分析每个选项的对错及原因 + return `- **选项分析**:对于选择题,必须逐项分析每个选项的对错及原因,并说明依据哪些法规 - **记忆口诀**:如果适用,提供便于记忆的口诀或技巧` } - return "- **答案解析**:详细说明为什么这个答案是正确的" + return "- **答案解析**:详细说明为什么这个答案是正确的,并引用相关法规依据" }(), // 根据题目类型添加示例格式 func() string { if questionType == "single-selection" || questionType == "multiple-selection" { return `## 选项分析 -- **A选项**:[分析该选项] -- **B选项**:[分析该选项] -- **C选项**:[分析该选项] -- **D选项**:[分析该选项] +- **A选项**:[分析该选项],根据《XX法规》第XX条... +- **B选项**:[分析该选项],根据《XX法规》第XX条... +- **C选项**:[分析该选项],根据《XX法规》第XX条... +- **D选项**:[分析该选项],根据《XX法规》第XX条... ## 正确答案 -正确答案是... 因为...` +正确答案是... 因为根据《XX法规》第XX条规定...` } return `## 答案解析 -正确答案是... 因为...` +正确答案是... 根据《XX法规》第XX条的规定...` }(), // 根据题目类型添加总结要求 func() string { if questionType == "single-selection" || questionType == "multiple-selection" { - return "对于选择题,可以提供记忆口诀或关键要点总结" + return "对于选择题,可以提供记忆口诀或关键要点总结,并总结涉及的主要法规要求" } - return "总结本题的关键要点和注意事项" + return "总结本题的关键要点、涉及的主要法规要求和在保密工作中的实际应用" }(), ) @@ -219,14 +347,14 @@ func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, que Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleSystem, - Content: "你是一位严谨、专业的老师,擅长深入浅出地讲解题目。你必须实事求是,只基于题目和标准答案提供解析,不编造任何不确定的信息。你使用Markdown格式输出,让学生更容易理解。", + Content: "你是一位严谨、专业的保密领域专家老师,精通保密法律法规和管理制度。你擅长深入浅出地讲解题目,并能准确引用相关法规条文。你必须实事求是,只基于题目、标准答案和实际法规提供解析,不编造任何不确定的信息。你使用Markdown格式输出,让学生更容易理解。", }, { Role: openai.ChatMessageRoleUser, Content: prompt, }, }, - Temperature: 0, // 温度为0,获得最确定、最一致的输出 + Temperature: 0.3, // 低温度,保证输出相对稳定和专业 Stream: true, }, ) diff --git a/main.go b/main.go index f76d77c..d70fe97 100644 --- a/main.go +++ b/main.go @@ -33,19 +33,24 @@ func main() { api.POST("/login", handlers.Login) // 用户登录 api.POST("/register", handlers.Register) // 用户注册 - // 练习题相关API - api.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表 - api.GET("/practice/questions/random", handlers.GetRandomPracticeQuestion) // 获取随机练习题目 - api.GET("/practice/questions/:id", handlers.GetPracticeQuestionByID) // 获取指定练习题目 - api.GET("/practice/types", handlers.GetPracticeQuestionTypes) // 获取题型列表 - api.POST("/practice/explain", handlers.ExplainQuestion) // 生成题目解析(AI) + // 公开的练习题相关API + api.GET("/practice/types", handlers.GetPracticeQuestionTypes) // 获取题型列表 // 需要认证的路由 auth := api.Group("", middleware.Auth()) { + // 用户相关API + auth.PUT("/user/type", handlers.UpdateUserType) // 更新用户类型 + + // 练习题相关API(需要登录) + auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表 + auth.GET("/practice/questions/random", handlers.GetRandomPracticeQuestion) // 获取随机练习题目 + auth.GET("/practice/questions/:id", handlers.GetPracticeQuestionByID) // 获取指定练习题目 + auth.POST("/practice/explain", handlers.ExplainQuestion) // 生成题目解析(AI) + // 练习题提交(需要登录才能记录错题) - auth.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案 - auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据 + auth.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案 + auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据 // 错题本相关API auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表 diff --git a/web/src/components/AnswerResult.tsx b/web/src/components/AnswerResult.tsx index 1c16fec..3d78c47 100644 --- a/web/src/components/AnswerResult.tsx +++ b/web/src/components/AnswerResult.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react' import { Alert, Typography, Card, Space, Progress, Button, Spin } from 'antd' import { CheckOutlined, CloseOutlined, TrophyOutlined, CommentOutlined, BulbOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' +import { fetchWithAuth } from '../utils/request' import type { AnswerResult as AnswerResultType } from '../types/question' const { Text, Paragraph } = Typography @@ -31,11 +32,8 @@ const AnswerResult: React.FC = ({ try { console.log('发送请求到 /api/practice/explain') - const response = await fetch('/api/practice/explain', { + const response = await fetchWithAuth('/api/practice/explain', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, body: JSON.stringify({ question_id: questionId }), }) @@ -173,16 +171,19 @@ const AnswerResult: React.FC = ({ {formatAnswer(answerResult.user_answer || selectedAnswer)} -
- - 正确答案: - - - {formatAnswer( - answerResult.correct_answer || (answerResult.correct ? selectedAnswer : '暂无') - )} - -
+ {/* 论述题不显示正确答案,因为没有标准答案 */} + {questionType !== 'ordinary-essay' && questionType !== 'management-essay' && ( +
+ + 正确答案: + + + {formatAnswer( + answerResult.correct_answer || (answerResult.correct ? selectedAnswer : '暂无') + )} + +
+ )} {answerResult.explanation && (
解析: @@ -256,7 +257,7 @@ const AnswerResult: React.FC = ({ )} - {/* AI评分结果 - 仅简答题显示 */} + {/* AI评分结果 - 简答题和论述题显示 */} {answerResult.ai_grading && ( = ({
+ {/* 参考答案(论述题) */} + {answerResult.ai_grading.reference_answer && ( +
+ + +
+ 参考答案: + + {answerResult.ai_grading.reference_answer} + +
+
+
+ )} + + {/* 评分依据 */} + {answerResult.ai_grading.scoring_rationale && ( +
+ + +
+ 评分依据: + + {answerResult.ai_grading.scoring_rationale} + +
+
+
+ )} + {/* 评语 */} {answerResult.ai_grading.feedback && (
diff --git a/web/src/components/QuestionCard.tsx b/web/src/components/QuestionCard.tsx index 34a8372..0d61fb8 100644 --- a/web/src/components/QuestionCard.tsx +++ b/web/src/components/QuestionCard.tsx @@ -88,14 +88,15 @@ const QuestionCard: React.FC = ({ return null } - if (question.type === 'short-answer') { + // 简答题和论述题都显示文本框 + if (question.type === 'short-answer' || question.type === 'ordinary-essay' || question.type === 'management-essay') { return (