添加论述题权限控制系统和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>
This commit is contained in:
parent
2e526425a0
commit
3b7133d9de
@ -16,11 +16,65 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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 获取练习题目列表
|
// GetPracticeQuestions 获取练习题目列表
|
||||||
func GetPracticeQuestions(c *gin.Context) {
|
func GetPracticeQuestions(c *gin.Context) {
|
||||||
typeParam := c.Query("type")
|
typeParam := c.Query("type")
|
||||||
searchQuery := c.Query("search")
|
searchQuery := c.Query("search")
|
||||||
|
|
||||||
|
log.Printf("[GetPracticeQuestions] 收到请求 (type: '%s', search: '%s')", typeParam, searchQuery)
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var questions []models.PracticeQuestion
|
var questions []models.PracticeQuestion
|
||||||
var total int64
|
var total int64
|
||||||
@ -29,7 +83,19 @@ func GetPracticeQuestions(c *gin.Context) {
|
|||||||
|
|
||||||
// 根据题型过滤
|
// 根据题型过滤
|
||||||
if typeParam != "" {
|
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)
|
query = query.Where("type = ?", typeParam)
|
||||||
|
log.Printf("[GetPracticeQuestions] 添加题型过滤 (type: '%s')", typeParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据搜索关键词过滤(搜索题目内容或题目编号)
|
// 根据搜索关键词过滤(搜索题目内容或题目编号)
|
||||||
@ -40,10 +106,14 @@ func GetPracticeQuestions(c *gin.Context) {
|
|||||||
// 获取总数
|
// 获取总数
|
||||||
query.Count(&total)
|
query.Count(&total)
|
||||||
|
|
||||||
|
log.Printf("[GetPracticeQuestions] 查询结果统计 (total: %d)", total)
|
||||||
|
|
||||||
// 查询所有题目 - 按题型和题目编号升序排序
|
// 查询所有题目 - 按题型和题目编号升序排序
|
||||||
// 先将 question_id 转为文本,提取数字部分,再转为整数排序
|
// 先将 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 {
|
if err != nil {
|
||||||
|
log.Printf("[GetPracticeQuestions] 查询失败 (error: %v)", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "查询题目失败",
|
"message": "查询题目失败",
|
||||||
@ -51,6 +121,8 @@ func GetPracticeQuestions(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[GetPracticeQuestions] 查询成功,返回 %d 条数据", len(questions))
|
||||||
|
|
||||||
// 转换为DTO
|
// 转换为DTO
|
||||||
dtos := make([]models.PracticeQuestionDTO, len(questions))
|
dtos := make([]models.PracticeQuestionDTO, len(questions))
|
||||||
for i, q := range questions {
|
for i, q := range questions {
|
||||||
@ -101,16 +173,30 @@ func GetPracticeQuestionByID(c *gin.Context) {
|
|||||||
func GetRandomPracticeQuestion(c *gin.Context) {
|
func GetRandomPracticeQuestion(c *gin.Context) {
|
||||||
typeParam := c.Query("type")
|
typeParam := c.Query("type")
|
||||||
|
|
||||||
|
log.Printf("[GetRandomPracticeQuestion] 收到请求 (type: '%s')", typeParam)
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var question models.PracticeQuestion
|
var question models.PracticeQuestion
|
||||||
|
|
||||||
query := db.Model(&models.PracticeQuestion{})
|
query := db.Model(&models.PracticeQuestion{})
|
||||||
if typeParam != "" {
|
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)
|
query = query.Where("type = ?", typeParam)
|
||||||
|
log.Printf("[GetRandomPracticeQuestion] 添加题型过滤 (type: '%s')", typeParam)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用PostgreSQL的随机排序
|
// 使用PostgreSQL的随机排序
|
||||||
if err := query.Order("RANDOM()").First(&question).Error; err != nil {
|
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{
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "暂无题目",
|
"message": "暂无题目",
|
||||||
@ -118,6 +204,8 @@ func GetRandomPracticeQuestion(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[GetRandomPracticeQuestion] 找到题目 (id: %d, type: '%s')", question.ID, question.Type)
|
||||||
|
|
||||||
// 转换为DTO
|
// 转换为DTO
|
||||||
dto := convertToDTO(question)
|
dto := convertToDTO(question)
|
||||||
|
|
||||||
@ -159,40 +247,56 @@ func SubmitPracticeAnswer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析正确答案
|
// 解析正确答案(论述题不需要标准答案)
|
||||||
var correctAnswer interface{}
|
var correctAnswer interface{}
|
||||||
if err := json.Unmarshal([]byte(question.AnswerData), &correctAnswer); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
// 论述题跳过答案解析
|
||||||
"success": false,
|
if strings.HasSuffix(question.Type, "-essay") {
|
||||||
"message": "答案数据错误",
|
log.Printf("[论述题] 跳过答案解析 (题目ID: %d, 类型: %s)", question.ID, question.Type)
|
||||||
})
|
correctAnswer = "" // 论述题没有标准答案
|
||||||
return
|
} 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
|
var aiGrading *models.AIGrading = nil
|
||||||
|
|
||||||
// 对简答题使用AI评分(必须成功,失败重试最多5次)
|
// 对简答题和论述题使用AI评分(必须成功,失败重试最多5次)
|
||||||
if question.Type == "short-answer" {
|
if question.Type == "short-answer" || strings.HasSuffix(question.Type, "-essay") {
|
||||||
// 获取用户答案字符串
|
// 获取用户答案字符串
|
||||||
userAnswerStr, ok := submit.Answer.(string)
|
userAnswerStr, ok := submit.Answer.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "简答题答案格式错误",
|
"message": "答案格式错误",
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取标准答案字符串
|
|
||||||
standardAnswerStr, ok := correctAnswer.(string)
|
|
||||||
if !ok {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": "题目答案格式错误",
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -203,17 +307,46 @@ func SubmitPracticeAnswer(c *gin.Context) {
|
|||||||
var err error
|
var err error
|
||||||
maxRetries := 5
|
maxRetries := 5
|
||||||
|
|
||||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
// 区分简答题和论述题的评分方式
|
||||||
log.Printf("AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID)
|
if strings.HasSuffix(question.Type, "-essay") {
|
||||||
aiResult, err = aiService.GradeShortAnswer(question.Question, standardAnswerStr, userAnswerStr)
|
// 论述题:不需要标准答案,直接评分
|
||||||
if err == nil {
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||||
log.Printf("AI评分成功 (题目ID: %d, 得分: %.1f)", question.ID, aiResult.Score)
|
log.Printf("论述题AI评分尝试第 %d 次 (题目ID: %d)", attempt, question.ID)
|
||||||
break
|
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)
|
} else {
|
||||||
if attempt < maxRetries {
|
// 简答题:需要标准答案对比
|
||||||
// 等待一小段时间后重试(指数退避)
|
// 获取标准答案字符串
|
||||||
time.Sleep(time.Second * time.Duration(attempt))
|
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的评分结果
|
// 使用AI的评分结果
|
||||||
correct = aiResult.IsCorrect
|
correct = aiResult.IsCorrect
|
||||||
aiGrading = &models.AIGrading{
|
aiGrading = &models.AIGrading{
|
||||||
Score: aiResult.Score,
|
Score: aiResult.Score,
|
||||||
Feedback: aiResult.Feedback,
|
Feedback: aiResult.Feedback,
|
||||||
Suggestion: aiResult.Suggestion,
|
Suggestion: aiResult.Suggestion,
|
||||||
|
ReferenceAnswer: aiResult.ReferenceAnswer,
|
||||||
|
ScoringRationale: aiResult.ScoringRationale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,22 +383,34 @@ func SubmitPracticeAnswer(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果答错,记录到错题本
|
// 如果答错,记录到错题本(论述题也可能答错)
|
||||||
if !correct {
|
if !correct {
|
||||||
if uid, ok := userID.(uint); ok {
|
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)
|
log.Printf("记录错题失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建返回结果
|
||||||
result := models.PracticeAnswerResult{
|
result := models.PracticeAnswerResult{
|
||||||
Correct: correct,
|
Correct: correct,
|
||||||
UserAnswer: submit.Answer,
|
UserAnswer: submit.Answer,
|
||||||
CorrectAnswer: correctAnswer, // 始终返回正确答案
|
AIGrading: aiGrading, // AI评分结果(简答题和论述题)
|
||||||
AIGrading: aiGrading, // AI评分结果(仅简答题有值)
|
}
|
||||||
|
|
||||||
|
// 论述题不返回标准答案(因为没有固定答案)
|
||||||
|
if !strings.HasSuffix(question.Type, "-essay") {
|
||||||
|
result.CorrectAnswer = correctAnswer
|
||||||
|
} else {
|
||||||
|
result.CorrectAnswer = "" // 论述题返回空字符串
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@ -295,6 +442,14 @@ func GetPracticeQuestionTypes(c *gin.Context) {
|
|||||||
"type": "short-answer",
|
"type": "short-answer",
|
||||||
"type_name": "简答题",
|
"type_name": "简答题",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "ordinary-essay",
|
||||||
|
"type_name": "普通涉密人员论述题",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "management-essay",
|
||||||
|
"type_name": "保密管理人员论述题",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
@ -402,9 +557,28 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析答案数据
|
// 解析答案数据
|
||||||
var answer interface{}
|
if question.AnswerData != "" {
|
||||||
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err == nil {
|
// 对于简答题和论述题,答案可能是纯字符串或JSON格式
|
||||||
dto.Answer = answer
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断题自动生成选项(正确在前,错误在后)
|
// 判断题自动生成选项(正确在前,错误在后)
|
||||||
|
|||||||
@ -81,8 +81,12 @@ func Login(c *gin.Context) {
|
|||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Avatar: user.Avatar,
|
Avatar: user.Avatar,
|
||||||
Nickname: user.Nickname,
|
Nickname: user.Nickname,
|
||||||
|
UserType: user.UserType, // 返回用户类型
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户类型是否为空,如果为空,标识需要补充
|
||||||
|
needUserType := user.UserType == ""
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "登录成功",
|
"message": "登录成功",
|
||||||
@ -90,6 +94,7 @@ func Login(c *gin.Context) {
|
|||||||
Token: token,
|
Token: token,
|
||||||
User: userInfo,
|
User: userInfo,
|
||||||
},
|
},
|
||||||
|
"need_user_type": needUserType, // 添加标识,前端根据此标识显示补充弹窗
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +128,7 @@ func Register(c *gin.Context) {
|
|||||||
newUser := models.User{
|
newUser := models.User{
|
||||||
Username: req.Username,
|
Username: req.Username,
|
||||||
Nickname: req.Nickname,
|
Nickname: req.Nickname,
|
||||||
|
UserType: req.UserType, // 保存用户类型
|
||||||
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=" + req.Username, // 使用用户名生成默认头像
|
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=" + req.Username, // 使用用户名生成默认头像
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,6 +168,7 @@ func Register(c *gin.Context) {
|
|||||||
Username: newUser.Username,
|
Username: newUser.Username,
|
||||||
Avatar: newUser.Avatar,
|
Avatar: newUser.Avatar,
|
||||||
Nickname: newUser.Nickname,
|
Nickname: newUser.Nickname,
|
||||||
|
UserType: newUser.UserType, // 返回用户类型
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -45,7 +45,9 @@ type PracticeAnswerResult struct {
|
|||||||
|
|
||||||
// AIGrading AI评分结果
|
// AIGrading AI评分结果
|
||||||
type AIGrading struct {
|
type AIGrading struct {
|
||||||
Score float64 `json:"score"` // 得分 (0-100)
|
Score float64 `json:"score"` // 得分 (0-100)
|
||||||
Feedback string `json:"feedback"` // 评语
|
Feedback string `json:"feedback"` // 评语
|
||||||
Suggestion string `json:"suggestion"` // 改进建议
|
Suggestion string `json:"suggestion"` // 改进建议
|
||||||
|
ReferenceAnswer string `json:"reference_answer,omitempty"` // 参考答案(论述题)
|
||||||
|
ScoringRationale string `json:"scoring_rationale,omitempty"` // 评分依据
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ type User struct {
|
|||||||
Token string `gorm:"size:255;index" json:"-"` // 用户登录token
|
Token string `gorm:"size:255;index" json:"-"` // 用户登录token
|
||||||
Avatar string `gorm:"size:255" json:"avatar"`
|
Avatar string `gorm:"size:255" json:"avatar"`
|
||||||
Nickname string `gorm:"size:50" json:"nickname"`
|
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"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
@ -56,6 +57,7 @@ type UserInfoResponse struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
|
UserType string `json:"user_type"` // 用户类型
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRequest 注册请求
|
// RegisterRequest 注册请求
|
||||||
@ -63,4 +65,5 @@ type RegisterRequest struct {
|
|||||||
Username string `json:"username" binding:"required"`
|
Username string `json:"username" binding:"required"`
|
||||||
Password string `json:"password" binding:"required,min=6"`
|
Password string `json:"password" binding:"required,min=6"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
|
UserType string `json:"user_type" binding:"required,oneof=ordinary-person management-person"` // 用户类型,必填
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,49 +36,71 @@ func NewAIGradingService() *AIGradingService {
|
|||||||
|
|
||||||
// AIGradingResult AI评分结果
|
// AIGradingResult AI评分结果
|
||||||
type AIGradingResult struct {
|
type AIGradingResult struct {
|
||||||
Score float64 `json:"score"` // 得分 (0-100)
|
Score float64 `json:"score"` // 得分 (0-100)
|
||||||
IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确)
|
IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确)
|
||||||
Feedback string `json:"feedback"` // 评语
|
Feedback string `json:"feedback"` // 评语
|
||||||
Suggestion string `json:"suggestion"` // 改进建议
|
Suggestion string `json:"suggestion"` // 改进建议
|
||||||
|
ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题)
|
||||||
|
ScoringRationale string `json:"scoring_rationale"` // 评分依据
|
||||||
}
|
}
|
||||||
|
|
||||||
// GradeShortAnswer 对简答题进行AI评分
|
// GradeEssay 对论述题进行AI评分(不需要标准答案)
|
||||||
// question: 题目内容
|
// question: 题目内容
|
||||||
// standardAnswer: 标准答案
|
|
||||||
// userAnswer: 用户答案
|
// userAnswer: 用户答案
|
||||||
func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer string) (*AIGradingResult, error) {
|
func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) {
|
||||||
// 构建评分提示词(严格评分模式)
|
// 构建评分提示词(论述题评分模式)
|
||||||
prompt := fmt.Sprintf(`你是一位严格的阅卷老师,请严格按照标准答案对以下简答题进行评分。
|
prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请对以下论述题进行评分。
|
||||||
|
|
||||||
题目:%s
|
题目:%s
|
||||||
|
|
||||||
标准答案:%s
|
|
||||||
|
|
||||||
学生答案:%s
|
学生答案:%s
|
||||||
|
|
||||||
评分标准(请严格遵守):
|
评分依据:
|
||||||
1. 必须与标准答案进行逐项对比
|
请依据以下保密法律法规和管理制度进行分析和评分:
|
||||||
2. 答案要点完全覆盖标准答案且表述准确的,给85-100分
|
1. 《中华人民共和国保守国家秘密法》
|
||||||
3. 答案要点基本覆盖但有缺漏或表述不够准确的,给60-84分
|
2. 《中华人民共和国保守国家秘密法实施条例》
|
||||||
4. 答案要点缺失较多或有明显错误的,给40-59分
|
3. 《保密工作管理制度2025.9.9》
|
||||||
5. 答案完全错误或离题的,给0-39分
|
4. 《软件开发管理制度》
|
||||||
6. 判断标准:60分及以上为正确(is_correct: true),否则为错误(is_correct: false)
|
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的精确分数
|
1. 给出一个0-100的精确分数
|
||||||
2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false)
|
2. 判断答案是否正确(is_correct: 60分及以上为true,否则为false)
|
||||||
3. 给出简短的评语(说明得分和失分原因,不超过50字)
|
3. 生成一个专业的参考答案(reference_answer,150-300字,必须引用相关法规条文)
|
||||||
4. 给出具体的改进建议(如果答案满分可以为空,否则必须指出具体改进方向,不超过50字)
|
4. 给出评分依据(scoring_rationale,说明依据了哪些法规和条文,80-150字)
|
||||||
|
5. 给出简短的评语(feedback,说明得分情况,不超过80字)
|
||||||
|
6. 给出具体的改进建议(suggestion,如果分数在90分以上可以简短,否则必须指出具体改进方向,不超过80字)
|
||||||
|
|
||||||
请按照以下JSON格式返回结果:
|
请按照以下JSON格式返回结果:
|
||||||
{
|
{
|
||||||
"score": 85,
|
"score": 75,
|
||||||
"is_correct": true,
|
"is_correct": true,
|
||||||
"feedback": "答案覆盖了主要要点,但XXX部分描述不够准确",
|
"reference_answer": "根据《中华人民共和国保守国家秘密法》第XX条...",
|
||||||
"suggestion": "建议补充XXX内容,并完善XXX的描述"
|
"scoring_rationale": "依据《保密法》第XX条、《保密法实施条例》第XX条...",
|
||||||
|
"feedback": "论述较为完整,论点明确,但论据不够充分,缺少具体法规引用",
|
||||||
|
"suggestion": "建议补充《保密法》相关条文,加强论点之间的逻辑联系"
|
||||||
}
|
}
|
||||||
|
|
||||||
注意:只返回JSON格式的结果,不要有其他内容。必须严格对照标准答案评分,不要过于宽松。`, question, standardAnswer, userAnswer)
|
注意:
|
||||||
|
1. 只返回JSON格式的结果,不要有其他内容
|
||||||
|
2. 参考答案必须专业、准确,体现保密法规要求
|
||||||
|
3. 评分依据必须具体引用法规条文`, question, userAnswer)
|
||||||
|
|
||||||
// 调用AI API
|
// 调用AI API
|
||||||
resp, err := s.client.CreateChatCompletion(
|
resp, err := s.client.CreateChatCompletion(
|
||||||
@ -88,7 +110,97 @@ func (s *AIGradingService) GradeShortAnswer(question, standardAnswer, userAnswer
|
|||||||
Messages: []openai.ChatCompletionMessage{
|
Messages: []openai.ChatCompletionMessage{
|
||||||
{
|
{
|
||||||
Role: openai.ChatMessageRoleSystem,
|
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,
|
Role: openai.ChatMessageRoleUser,
|
||||||
@ -143,7 +255,7 @@ type AIExplanationResult struct {
|
|||||||
// questionType: 题目类型
|
// questionType: 题目类型
|
||||||
func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error {
|
func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error {
|
||||||
// 构建解析提示词(直接输出,不使用JSON格式)
|
// 构建解析提示词(直接输出,不使用JSON格式)
|
||||||
prompt := fmt.Sprintf(`你是一位严谨、专业的老师,请对以下题目进行详细解析。
|
prompt := fmt.Sprintf(`你是一位严谨、专业的保密领域专家老师,请对以下题目进行详细解析。
|
||||||
|
|
||||||
题目类型:%s
|
题目类型:%s
|
||||||
|
|
||||||
@ -151,30 +263,46 @@ func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, que
|
|||||||
|
|
||||||
标准答案:%s
|
标准答案:%s
|
||||||
|
|
||||||
|
**重要依据**:请基于以下保密法律法规和管理制度进行专业分析:
|
||||||
|
1. 《中华人民共和国保守国家秘密法》
|
||||||
|
2. 《中华人民共和国保守国家秘密法实施条例》
|
||||||
|
3. 《保密工作管理制度2025.9.9》
|
||||||
|
4. 《软件开发管理制度》
|
||||||
|
5. 《涉密信息系统集成资质保密标准》
|
||||||
|
6. 《涉密信息系统集成资质管理办法》
|
||||||
|
|
||||||
请提供一个详细的解析,要求:
|
请提供一个详细的解析,要求:
|
||||||
1. **必须实事求是**:只基于题目内容和标准答案进行解析,不要添加题目中没有的信息
|
1. **必须基于保密法规**:解析时必须引用相关法规条文,说明依据哪些具体法律法规
|
||||||
2. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造
|
2. **必须实事求是**:只基于题目内容、标准答案和实际法规进行解析
|
||||||
3. **使用Markdown格式**:使用标题、列表、加粗等markdown语法使内容更清晰易读
|
3. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造法规条文
|
||||||
|
4. **使用Markdown格式**:使用标题、列表、加粗等markdown语法使内容更清晰易读
|
||||||
|
|
||||||
解析内容要求:
|
解析内容要求:
|
||||||
- **知识点**:说明题目考查的核心知识点
|
- **知识点**:说明题目考查的核心知识点,指出涉及哪些保密法规
|
||||||
- **解题思路**:提供清晰的解题步骤和方法
|
- **法规依据**:明确引用相关法律法规的具体条文(如:《保密法》第X条、《保密法实施条例》第X条等)
|
||||||
|
- **解题思路**:提供清晰的解题步骤和方法,结合保密工作实际
|
||||||
|
|
||||||
%s
|
%s
|
||||||
|
|
||||||
示例输出格式:
|
示例输出格式:
|
||||||
## 知识点
|
## 知识点
|
||||||
题目考查的是...
|
本题考查的是[知识点名称],涉及《XX法规》第XX条...
|
||||||
|
|
||||||
|
## 法规依据
|
||||||
|
- 《中华人民共和国保守国家秘密法》第XX条规定:...
|
||||||
|
- 《保密工作管理制度2025.9.9》第X章第X节:...
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
1. 首先分析...
|
1. 首先根据《XX法规》第XX条,我们可以判断...
|
||||||
2. 然后判断...
|
2. 然后结合保密工作实际,分析...
|
||||||
|
|
||||||
%s
|
%s
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
%s
|
%s
|
||||||
|
|
||||||
|
**重要提醒**:请务必在解析中引用具体的法规条文,不要空泛地提及法规名称。如果不确定具体条文编号,可以说明法规的精神和要求。
|
||||||
|
|
||||||
请使用markdown格式输出解析内容。`,
|
请使用markdown格式输出解析内容。`,
|
||||||
questionType,
|
questionType,
|
||||||
question,
|
question,
|
||||||
@ -182,32 +310,32 @@ func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, que
|
|||||||
// 根据题目类型添加特定要求
|
// 根据题目类型添加特定要求
|
||||||
func() string {
|
func() string {
|
||||||
if questionType == "single-selection" || questionType == "multiple-selection" {
|
if questionType == "single-selection" || questionType == "multiple-selection" {
|
||||||
return `- **选项分析**:对于选择题,必须逐项分析每个选项的对错及原因
|
return `- **选项分析**:对于选择题,必须逐项分析每个选项的对错及原因,并说明依据哪些法规
|
||||||
- **记忆口诀**:如果适用,提供便于记忆的口诀或技巧`
|
- **记忆口诀**:如果适用,提供便于记忆的口诀或技巧`
|
||||||
}
|
}
|
||||||
return "- **答案解析**:详细说明为什么这个答案是正确的"
|
return "- **答案解析**:详细说明为什么这个答案是正确的,并引用相关法规依据"
|
||||||
}(),
|
}(),
|
||||||
// 根据题目类型添加示例格式
|
// 根据题目类型添加示例格式
|
||||||
func() string {
|
func() string {
|
||||||
if questionType == "single-selection" || questionType == "multiple-selection" {
|
if questionType == "single-selection" || questionType == "multiple-selection" {
|
||||||
return `## 选项分析
|
return `## 选项分析
|
||||||
- **A选项**:[分析该选项]
|
- **A选项**:[分析该选项],根据《XX法规》第XX条...
|
||||||
- **B选项**:[分析该选项]
|
- **B选项**:[分析该选项],根据《XX法规》第XX条...
|
||||||
- **C选项**:[分析该选项]
|
- **C选项**:[分析该选项],根据《XX法规》第XX条...
|
||||||
- **D选项**:[分析该选项]
|
- **D选项**:[分析该选项],根据《XX法规》第XX条...
|
||||||
|
|
||||||
## 正确答案
|
## 正确答案
|
||||||
正确答案是... 因为...`
|
正确答案是... 因为根据《XX法规》第XX条规定...`
|
||||||
}
|
}
|
||||||
return `## 答案解析
|
return `## 答案解析
|
||||||
正确答案是... 因为...`
|
正确答案是... 根据《XX法规》第XX条的规定...`
|
||||||
}(),
|
}(),
|
||||||
// 根据题目类型添加总结要求
|
// 根据题目类型添加总结要求
|
||||||
func() string {
|
func() string {
|
||||||
if questionType == "single-selection" || questionType == "multiple-selection" {
|
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{
|
Messages: []openai.ChatCompletionMessage{
|
||||||
{
|
{
|
||||||
Role: openai.ChatMessageRoleSystem,
|
Role: openai.ChatMessageRoleSystem,
|
||||||
Content: "你是一位严谨、专业的老师,擅长深入浅出地讲解题目。你必须实事求是,只基于题目和标准答案提供解析,不编造任何不确定的信息。你使用Markdown格式输出,让学生更容易理解。",
|
Content: "你是一位严谨、专业的保密领域专家老师,精通保密法律法规和管理制度。你擅长深入浅出地讲解题目,并能准确引用相关法规条文。你必须实事求是,只基于题目、标准答案和实际法规提供解析,不编造任何不确定的信息。你使用Markdown格式输出,让学生更容易理解。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Role: openai.ChatMessageRoleUser,
|
Role: openai.ChatMessageRoleUser,
|
||||||
Content: prompt,
|
Content: prompt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Temperature: 0, // 温度为0,获得最确定、最一致的输出
|
Temperature: 0.3, // 低温度,保证输出相对稳定和专业
|
||||||
Stream: true,
|
Stream: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
21
main.go
21
main.go
@ -33,19 +33,24 @@ func main() {
|
|||||||
api.POST("/login", handlers.Login) // 用户登录
|
api.POST("/login", handlers.Login) // 用户登录
|
||||||
api.POST("/register", handlers.Register) // 用户注册
|
api.POST("/register", handlers.Register) // 用户注册
|
||||||
|
|
||||||
// 练习题相关API
|
// 公开的练习题相关API
|
||||||
api.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表
|
api.GET("/practice/types", handlers.GetPracticeQuestionTypes) // 获取题型列表
|
||||||
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)
|
|
||||||
|
|
||||||
// 需要认证的路由
|
// 需要认证的路由
|
||||||
auth := api.Group("", middleware.Auth())
|
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.POST("/practice/submit", handlers.SubmitPracticeAnswer) // 提交练习答案
|
||||||
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据
|
||||||
|
|
||||||
// 错题本相关API
|
// 错题本相关API
|
||||||
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表
|
auth.GET("/wrong-questions", handlers.GetWrongQuestions) // 获取错题列表
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
|||||||
import { Alert, Typography, Card, Space, Progress, Button, Spin } from 'antd'
|
import { Alert, Typography, Card, Space, Progress, Button, Spin } from 'antd'
|
||||||
import { CheckOutlined, CloseOutlined, TrophyOutlined, CommentOutlined, BulbOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { CheckOutlined, CloseOutlined, TrophyOutlined, CommentOutlined, BulbOutlined, FileTextOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import { fetchWithAuth } from '../utils/request'
|
||||||
import type { AnswerResult as AnswerResultType } from '../types/question'
|
import type { AnswerResult as AnswerResultType } from '../types/question'
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography
|
const { Text, Paragraph } = Typography
|
||||||
@ -31,11 +32,8 @@ const AnswerResult: React.FC<AnswerResultProps> = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('发送请求到 /api/practice/explain')
|
console.log('发送请求到 /api/practice/explain')
|
||||||
const response = await fetch('/api/practice/explain', {
|
const response = await fetchWithAuth('/api/practice/explain', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ question_id: questionId }),
|
body: JSON.stringify({ question_id: questionId }),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -173,16 +171,19 @@ const AnswerResult: React.FC<AnswerResultProps> = ({
|
|||||||
{formatAnswer(answerResult.user_answer || selectedAnswer)}
|
{formatAnswer(answerResult.user_answer || selectedAnswer)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginBottom: 8 }}>
|
{/* 论述题不显示正确答案,因为没有标准答案 */}
|
||||||
<Text strong style={{ color: '#52c41a' }}>
|
{questionType !== 'ordinary-essay' && questionType !== 'management-essay' && (
|
||||||
正确答案:
|
<div style={{ marginBottom: 8 }}>
|
||||||
</Text>
|
<Text strong style={{ color: '#52c41a' }}>
|
||||||
<Text strong style={{ color: '#52c41a' }}>
|
正确答案:
|
||||||
{formatAnswer(
|
</Text>
|
||||||
answerResult.correct_answer || (answerResult.correct ? selectedAnswer : '暂无')
|
<Text strong style={{ color: '#52c41a' }}>
|
||||||
)}
|
{formatAnswer(
|
||||||
</Text>
|
answerResult.correct_answer || (answerResult.correct ? selectedAnswer : '暂无')
|
||||||
</div>
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{answerResult.explanation && (
|
{answerResult.explanation && (
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary">解析:</Text>
|
<Text type="secondary">解析:</Text>
|
||||||
@ -256,7 +257,7 @@ const AnswerResult: React.FC<AnswerResultProps> = ({
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI评分结果 - 仅简答题显示 */}
|
{/* AI评分结果 - 简答题和论述题显示 */}
|
||||||
{answerResult.ai_grading && (
|
{answerResult.ai_grading && (
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
@ -299,6 +300,36 @@ const AnswerResult: React.FC<AnswerResultProps> = ({
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 参考答案(论述题) */}
|
||||||
|
{answerResult.ai_grading.reference_answer && (
|
||||||
|
<div style={{ marginBottom: 16, padding: 12, backgroundColor: '#f0f9ff', borderRadius: 4 }}>
|
||||||
|
<Space align="start">
|
||||||
|
<FileTextOutlined style={{ fontSize: 16, color: '#1890ff', marginTop: 2 }} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text strong style={{ fontSize: 14, color: '#1890ff' }}>参考答案:</Text>
|
||||||
|
<Paragraph style={{ marginTop: 4, marginBottom: 0, color: '#262626', whiteSpace: 'pre-wrap' }}>
|
||||||
|
{answerResult.ai_grading.reference_answer}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 评分依据 */}
|
||||||
|
{answerResult.ai_grading.scoring_rationale && (
|
||||||
|
<div style={{ marginBottom: 16, padding: 12, backgroundColor: '#f6ffed', borderRadius: 4 }}>
|
||||||
|
<Space align="start">
|
||||||
|
<CheckOutlined style={{ fontSize: 16, color: '#52c41a', marginTop: 2 }} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Text strong style={{ fontSize: 14, color: '#52c41a' }}>评分依据:</Text>
|
||||||
|
<Paragraph style={{ marginTop: 4, marginBottom: 0, color: '#262626', whiteSpace: 'pre-wrap' }}>
|
||||||
|
{answerResult.ai_grading.scoring_rationale}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 评语 */}
|
{/* 评语 */}
|
||||||
{answerResult.ai_grading.feedback && (
|
{answerResult.ai_grading.feedback && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
|||||||
@ -88,14 +88,15 @@ const QuestionCard: React.FC<QuestionCardProps> = ({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (question.type === 'short-answer') {
|
// 简答题和论述题都显示文本框
|
||||||
|
if (question.type === 'short-answer' || question.type === 'ordinary-essay' || question.type === 'management-essay') {
|
||||||
return (
|
return (
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="请输入答案"
|
placeholder="请输入答案"
|
||||||
value={selectedAnswer as string}
|
value={selectedAnswer as string}
|
||||||
onChange={(e) => onAnswerChange(e.target.value)}
|
onChange={(e) => onAnswerChange(e.target.value)}
|
||||||
disabled={showResult}
|
disabled={showResult}
|
||||||
rows={4}
|
rows={6}
|
||||||
style={{ marginTop: 20 }}
|
style={{ marginTop: 20 }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal } from 'antd'
|
import { Card, Statistic, Row, Col, Typography, message, Space, Avatar, Button, Modal, Form, Radio, Alert } from 'antd'
|
||||||
import {
|
import {
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
UnorderedListOutlined as ListOutlined,
|
UnorderedListOutlined as ListOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import * as questionApi from '../api/question'
|
import * as questionApi from '../api/question'
|
||||||
|
import { fetchWithAuth } from '../utils/request'
|
||||||
import type { Statistics } from '../types/question'
|
import type { Statistics } from '../types/question'
|
||||||
import styles from './Home.module.less'
|
import styles from './Home.module.less'
|
||||||
|
|
||||||
@ -56,17 +57,28 @@ const questionTypes = [
|
|||||||
color: '#eb2f96',
|
color: '#eb2f96',
|
||||||
description: '深度理解练习',
|
description: '深度理解练习',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'essay', // 特殊标识,根据用户类型动态路由
|
||||||
|
title: '论述题',
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
color: '#f759ab',
|
||||||
|
description: '深度分析与表达',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
username: string
|
username: string
|
||||||
nickname: string
|
nickname: string
|
||||||
avatar: string
|
avatar: string
|
||||||
|
user_type?: string // 用户类型
|
||||||
}
|
}
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
|
||||||
|
const [userTypeModalVisible, setUserTypeModalVisible] = useState(false) // 用户类型补充模态框
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [userTypeForm] = Form.useForm()
|
||||||
const [statistics, setStatistics] = useState<Statistics>({
|
const [statistics, setStatistics] = useState<Statistics>({
|
||||||
total_questions: 0,
|
total_questions: 0,
|
||||||
answered_questions: 0,
|
answered_questions: 0,
|
||||||
@ -94,7 +106,13 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
if (token && savedUserInfo) {
|
if (token && savedUserInfo) {
|
||||||
try {
|
try {
|
||||||
setUserInfo(JSON.parse(savedUserInfo))
|
const user = JSON.parse(savedUserInfo)
|
||||||
|
setUserInfo(user)
|
||||||
|
|
||||||
|
// 检查用户是否有用户类型,如果没有则显示强制选择模态框
|
||||||
|
if (!user.user_type) {
|
||||||
|
setUserTypeModalVisible(true)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析用户信息失败', e)
|
console.error('解析用户信息失败', e)
|
||||||
}
|
}
|
||||||
@ -105,14 +123,56 @@ const Home: React.FC = () => {
|
|||||||
loadStatistics()
|
loadStatistics()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 处理用户类型更新
|
||||||
|
const handleUpdateUserType = async (values: { user_type: string }) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetchWithAuth('/api/user/type', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 更新本地存储的用户信息
|
||||||
|
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||||
|
user.user_type = data.data.user_type
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
setUserInfo(user)
|
||||||
|
|
||||||
|
message.success('身份类型设置成功')
|
||||||
|
setUserTypeModalVisible(false)
|
||||||
|
} else {
|
||||||
|
message.error(data.message || '更新失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error('网络错误,请稍后重试')
|
||||||
|
console.error('更新用户类型错误:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 点击题型卡片
|
// 点击题型卡片
|
||||||
const handleTypeClick = async (type: string) => {
|
const handleTypeClick = async (type: string) => {
|
||||||
try {
|
try {
|
||||||
|
// 如果是论述题,根据用户类型动态确定题型
|
||||||
|
let actualType = type
|
||||||
|
if (type === 'essay') {
|
||||||
|
if (!userInfo?.user_type) {
|
||||||
|
message.warning('请先设置您的身份类型')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 根据用户类型选择对应的论述题
|
||||||
|
actualType = userInfo.user_type === 'ordinary-person' ? 'ordinary-essay' : 'management-essay'
|
||||||
|
}
|
||||||
|
|
||||||
// 加载该题型的题目列表
|
// 加载该题型的题目列表
|
||||||
const res = await questionApi.getQuestions({ type })
|
const res = await questionApi.getQuestions({ type: actualType })
|
||||||
if (res.success && res.data && res.data.length > 0) {
|
if (res.success && res.data && res.data.length > 0) {
|
||||||
// 跳转到答题页面,并传递题型参数
|
// 跳转到答题页面,并传递题型参数
|
||||||
navigate(`/question?type=${type}`)
|
navigate(`/question?type=${actualType}`)
|
||||||
} else {
|
} else {
|
||||||
message.warning('该题型暂无题目')
|
message.warning('该题型暂无题目')
|
||||||
}
|
}
|
||||||
@ -316,6 +376,49 @@ const Home: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 用户类型补充模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="请选择您的身份类型"
|
||||||
|
open={userTypeModalVisible}
|
||||||
|
closable={false}
|
||||||
|
maskClosable={false}
|
||||||
|
keyboard={false}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
message="论述题需要使用"
|
||||||
|
description="为了更好地为您提供相应的论述题内容,请选择您的身份类型。"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<Form
|
||||||
|
form={userTypeForm}
|
||||||
|
name="userType"
|
||||||
|
onFinish={handleUpdateUserType}
|
||||||
|
autoComplete="off"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="user_type"
|
||||||
|
label="身份类型"
|
||||||
|
rules={[{ required: true, message: '请选择身份类型' }]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="ordinary-person">普通涉密人员</Radio>
|
||||||
|
<Radio value="management-person">保密管理人员</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Form, Input, Button, Card, Modal, message, Typography } from 'antd'
|
import { Form, Input, Button, Card, Modal, message, Typography, Radio, Alert } from 'antd'
|
||||||
import { UserOutlined, LockOutlined, IdcardOutlined } from '@ant-design/icons'
|
import { UserOutlined, LockOutlined, IdcardOutlined } from '@ant-design/icons'
|
||||||
|
import { fetchWithAuth } from '../utils/request'
|
||||||
import styles from './Login.module.less'
|
import styles from './Login.module.less'
|
||||||
|
|
||||||
const { Title, Text, Link } = Typography
|
const { Title, Text, Link } = Typography
|
||||||
@ -9,12 +10,14 @@ const { Title, Text, Link } = Typography
|
|||||||
interface LoginResponse {
|
interface LoginResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
message: string
|
message: string
|
||||||
|
need_user_type?: boolean // 是否需要补充用户类型
|
||||||
data?: {
|
data?: {
|
||||||
token: string
|
token: string
|
||||||
user: {
|
user: {
|
||||||
username: string
|
username: string
|
||||||
avatar: string
|
avatar: string
|
||||||
nickname: string
|
nickname: string
|
||||||
|
user_type?: string // 用户类型
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -23,8 +26,11 @@ const Login: React.FC = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [registerModalVisible, setRegisterModalVisible] = useState(false)
|
const [registerModalVisible, setRegisterModalVisible] = useState(false)
|
||||||
|
const [userTypeModalVisible, setUserTypeModalVisible] = useState(false) // 用户类型补充模态框
|
||||||
|
const [userType, setUserType] = useState<string>('') // 临时存储用户选择的类型
|
||||||
const [loginForm] = Form.useForm()
|
const [loginForm] = Form.useForm()
|
||||||
const [registerForm] = Form.useForm()
|
const [registerForm] = Form.useForm()
|
||||||
|
const [userTypeForm] = Form.useForm()
|
||||||
|
|
||||||
// 如果已登录,重定向到首页
|
// 如果已登录,重定向到首页
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,11 +44,8 @@ const Login: React.FC = () => {
|
|||||||
const handleLogin = async (values: { username: string; password: string }) => {
|
const handleLogin = async (values: { username: string; password: string }) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/login', {
|
const response = await fetchWithAuth('/api/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(values),
|
body: JSON.stringify(values),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,8 +55,13 @@ const Login: React.FC = () => {
|
|||||||
localStorage.setItem('token', data.data.token)
|
localStorage.setItem('token', data.data.token)
|
||||||
localStorage.setItem('user', JSON.stringify(data.data.user))
|
localStorage.setItem('user', JSON.stringify(data.data.user))
|
||||||
|
|
||||||
message.success('登录成功')
|
// 检查是否需要补充用户类型
|
||||||
navigate('/')
|
if (data.need_user_type) {
|
||||||
|
setUserTypeModalVisible(true)
|
||||||
|
} else {
|
||||||
|
message.success('登录成功')
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(data.message || '登录失败')
|
message.error(data.message || '登录失败')
|
||||||
}
|
}
|
||||||
@ -66,14 +74,11 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理注册
|
// 处理注册
|
||||||
const handleRegister = async (values: { username: string; password: string; nickname?: string }) => {
|
const handleRegister = async (values: { username: string; password: string; nickname?: string; user_type: string }) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/register', {
|
const response = await fetchWithAuth('/api/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(values),
|
body: JSON.stringify(values),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -97,6 +102,37 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理用户类型更新
|
||||||
|
const handleUpdateUserType = async (values: { user_type: string }) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetchWithAuth('/api/user/type', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// 更新本地存储的用户信息
|
||||||
|
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||||
|
user.user_type = data.data.user_type
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
|
||||||
|
message.success('身份类型设置成功')
|
||||||
|
setUserTypeModalVisible(false)
|
||||||
|
navigate('/')
|
||||||
|
} else {
|
||||||
|
message.error(data.message || '更新失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error('网络错误,请稍后重试')
|
||||||
|
console.error('更新用户类型错误:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
@ -205,6 +241,17 @@ const Login: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="user_type"
|
||||||
|
label="身份类型"
|
||||||
|
rules={[{ required: true, message: '请选择身份类型' }]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="ordinary-person">普通涉密人员</Radio>
|
||||||
|
<Radio value="management-person">保密管理人员</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" block loading={loading}>
|
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||||
注册
|
注册
|
||||||
@ -212,6 +259,48 @@ const Login: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 用户类型补充模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="请选择您的身份类型"
|
||||||
|
open={userTypeModalVisible}
|
||||||
|
closable={false}
|
||||||
|
maskClosable={false}
|
||||||
|
footer={null}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
message="论述题需要使用"
|
||||||
|
description="为了更好地为您提供相应的论述题内容,请选择您的身份类型。"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
|
<Form
|
||||||
|
form={userTypeForm}
|
||||||
|
name="userType"
|
||||||
|
onFinish={handleUpdateUserType}
|
||||||
|
autoComplete="off"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="user_type"
|
||||||
|
label="身份类型"
|
||||||
|
rules={[{ required: true, message: '请选择身份类型' }]}
|
||||||
|
>
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="ordinary-person">普通涉密人员</Radio>
|
||||||
|
<Radio value="management-person">保密管理人员</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" block loading={loading}>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,8 @@ const questionTypeConfig: Record<string, { label: string; icon: React.ReactNode;
|
|||||||
'true-false': { label: '判断题', icon: <CheckCircleOutlined />, color: '#fa8c16' },
|
'true-false': { label: '判断题', icon: <CheckCircleOutlined />, color: '#fa8c16' },
|
||||||
'fill-in-blank': { label: '填空题', icon: <FileTextOutlined />, color: '#722ed1' },
|
'fill-in-blank': { label: '填空题', icon: <FileTextOutlined />, color: '#722ed1' },
|
||||||
'short-answer': { label: '简答题', icon: <EditOutlined />, color: '#eb2f96' },
|
'short-answer': { label: '简答题', icon: <EditOutlined />, color: '#eb2f96' },
|
||||||
|
'ordinary-essay': { label: '普通涉密人员论述题', icon: <FileTextOutlined />, color: '#f759ab' },
|
||||||
|
'management-essay': { label: '保密管理人员论述题', icon: <FileTextOutlined />, color: '#d4380d' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionList: React.FC = () => {
|
const QuestionList: React.FC = () => {
|
||||||
|
|||||||
@ -36,6 +36,8 @@ const questionTypes = [
|
|||||||
{ key: 'true-false', label: '判断题' },
|
{ key: 'true-false', label: '判断题' },
|
||||||
{ key: 'fill-in-blank', label: '填空题' },
|
{ key: 'fill-in-blank', label: '填空题' },
|
||||||
{ key: 'short-answer', label: '简答题' },
|
{ key: 'short-answer', label: '简答题' },
|
||||||
|
{ key: 'ordinary-essay', label: '普通涉密人员论述题' },
|
||||||
|
{ key: 'management-essay', label: '保密管理人员论述题' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const QuestionManagement: React.FC = () => {
|
const QuestionManagement: React.FC = () => {
|
||||||
@ -137,7 +139,7 @@ const QuestionManagement: React.FC = () => {
|
|||||||
} else if (values.type === 'fill-in-blank') {
|
} else if (values.type === 'fill-in-blank') {
|
||||||
// 填空题答案是数组
|
// 填空题答案是数组
|
||||||
answer = values.answer
|
answer = values.answer
|
||||||
} else if (values.type === 'short-answer') {
|
} else if (values.type === 'short-answer' || values.type === 'ordinary-essay' || values.type === 'management-essay') {
|
||||||
answer = values.answer
|
answer = values.answer
|
||||||
} else {
|
} else {
|
||||||
answer = values.answer
|
answer = values.answer
|
||||||
@ -205,7 +207,7 @@ const QuestionManagement: React.FC = () => {
|
|||||||
title: '题型',
|
title: '题型',
|
||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
key: 'type',
|
key: 'type',
|
||||||
width: 120,
|
width: 180,
|
||||||
render: (type: string) => {
|
render: (type: string) => {
|
||||||
const typeConfig = questionTypes.find(t => t.key === type)
|
const typeConfig = questionTypes.find(t => t.key === type)
|
||||||
const colorMap: Record<string, string> = {
|
const colorMap: Record<string, string> = {
|
||||||
@ -214,6 +216,8 @@ const QuestionManagement: React.FC = () => {
|
|||||||
'true-false': 'orange',
|
'true-false': 'orange',
|
||||||
'fill-in-blank': 'purple',
|
'fill-in-blank': 'purple',
|
||||||
'short-answer': 'magenta',
|
'short-answer': 'magenta',
|
||||||
|
'ordinary-essay': 'pink',
|
||||||
|
'management-essay': 'red',
|
||||||
}
|
}
|
||||||
return <Tag color={colorMap[type]}>{typeConfig?.label || type}</Tag>
|
return <Tag color={colorMap[type]}>{typeConfig?.label || type}</Tag>
|
||||||
},
|
},
|
||||||
@ -434,6 +438,8 @@ const QuestionManagement: React.FC = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
case 'short-answer':
|
case 'short-answer':
|
||||||
|
case 'ordinary-essay':
|
||||||
|
case 'management-essay':
|
||||||
return (
|
return (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="参考答案"
|
label="参考答案"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// 题目类型 - 使用数据库中的实际类型
|
// 题目类型 - 使用数据库中的实际类型
|
||||||
export type QuestionType = 'multiple-choice' | 'multiple-selection' | 'fill-in-blank' | 'true-false' | 'short-answer'
|
export type QuestionType = 'multiple-choice' | 'multiple-selection' | 'fill-in-blank' | 'true-false' | 'short-answer' | 'ordinary-essay' | 'management-essay'
|
||||||
|
|
||||||
// 选项
|
// 选项
|
||||||
export interface Option {
|
export interface Option {
|
||||||
@ -29,6 +29,8 @@ export interface AIGrading {
|
|||||||
score: number // 得分 (0-100)
|
score: number // 得分 (0-100)
|
||||||
feedback: string // 评语
|
feedback: string // 评语
|
||||||
suggestion: string // 改进建议
|
suggestion: string // 改进建议
|
||||||
|
reference_answer?: string // 参考答案(论述题)
|
||||||
|
scoring_rationale?: string // 评分依据
|
||||||
}
|
}
|
||||||
|
|
||||||
// 答案结果
|
// 答案结果
|
||||||
@ -37,7 +39,7 @@ export interface AnswerResult {
|
|||||||
user_answer: string | string[] | boolean
|
user_answer: string | string[] | boolean
|
||||||
correct_answer: string | string[]
|
correct_answer: string | string[]
|
||||||
explanation?: string
|
explanation?: string
|
||||||
ai_grading?: AIGrading // AI评分结果(仅简答题)
|
ai_grading?: AIGrading // AI评分结果(简答题和论述题)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
|
|||||||
@ -76,4 +76,52 @@ export const request = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一的 fetch 请求工具(用于需要原生 fetch 的场景,如流式请求)
|
||||||
|
interface FetchOptions extends RequestInit {
|
||||||
|
// 扩展选项(如果需要)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一封装的 fetch 请求方法
|
||||||
|
* 自动添加 Authorization header 和其他通用配置
|
||||||
|
*/
|
||||||
|
export const fetchWithAuth = async (
|
||||||
|
url: string,
|
||||||
|
options: FetchOptions = {}
|
||||||
|
): Promise<Response> => {
|
||||||
|
// 获取 token
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
// 合并 headers
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有 token,添加到请求头
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的请求配置
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await fetch(url, fetchOptions)
|
||||||
|
|
||||||
|
// 统一处理 401 未授权错误
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error('Token已过期或未授权,请重新登录')
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
window.location.href = '/login'
|
||||||
|
throw new Error('未授权,请重新登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
export default instance
|
export default instance
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user