diff --git a/go.mod b/go.mod index 2dc2dae..fbedccf 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/baidubce/app-builder/go/appbuilder v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect diff --git a/go.sum b/go.sum index 5114cef..f65f269 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/baidubce/app-builder/go/appbuilder v1.1.1 h1:mPfUGmQU/Vi4KRJca6m34rWH/YWuQWOiPLmjtVjPhuA= +github.com/baidubce/app-builder/go/appbuilder v1.1.1/go.mod h1:mHOdSd9TJ52aiUbRE2rW1omu4A0U7H32xN39ED+etmE= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= diff --git a/internal/handlers/exam_grading.go b/internal/handlers/exam_grading.go index ed76c6f..ce53880 100644 --- a/internal/handlers/exam_grading.go +++ b/internal/handlers/exam_grading.go @@ -79,7 +79,11 @@ func gradeExam(recordID uint, examID uint, userID uint) { // 评分 totalScore := 0.0 - aiService := services.NewAIGradingService() + aiService, err := services.NewAIGradingService() + if err != nil { + log.Printf("AI服务初始化失败: %v,将跳过AI评分", err) + // 不返回错误,继续评分流程,只是跳过AI评分 + } for _, question := range questions { userAnswerRaw, answered := answersMap[question.ID] @@ -195,27 +199,35 @@ func gradeExam(recordID uint, examID uint, userID uint) { // AnswerData 直接存储答案文本 correctAnswer := question.AnswerData userAnswerStr := fmt.Sprintf("%v", userAnswerRaw) - aiResult, err := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr) - if err != nil { - log.Printf("AI评分失败: %v", err) + + // 检查AI服务是否可用 + if aiService == nil { + log.Printf("AI服务不可用,无法评分问题 %d", question.ID) isCorrect = false score = 0 } else { - isCorrect = aiResult.IsCorrect - // 按AI评分比例计算 - var questionScore float64 - if question.Type == "short-answer" { - questionScore = scoreMap["short-answer"] - } else if question.Type == "ordinary-essay" { - questionScore = scoreMap["ordinary-essay"] - } else if question.Type == "management-essay" { - questionScore = scoreMap["management-essay"] - } - score = questionScore * (aiResult.Score / 100.0) - aiGrading = &models.AIGrading{ - Score: aiResult.Score, - Feedback: aiResult.Feedback, - Suggestion: aiResult.Suggestion, + aiResult, aiErr := aiService.GradeShortAnswer(question.Question, correctAnswer, userAnswerStr) + if aiErr != nil { + log.Printf("AI评分失败: %v", aiErr) + isCorrect = false + score = 0 + } else { + isCorrect = aiResult.IsCorrect + // 按AI评分比例计算 + var questionScore float64 + if question.Type == "short-answer" { + questionScore = scoreMap["short-answer"] + } else if question.Type == "ordinary-essay" { + questionScore = scoreMap["ordinary-essay"] + } else if question.Type == "management-essay" { + questionScore = scoreMap["management-essay"] + } + score = questionScore * (aiResult.Score / 100.0) + aiGrading = &models.AIGrading{ + Score: aiResult.Score, + Feedback: aiResult.Feedback, + Suggestion: aiResult.Suggestion, + } } } } diff --git a/internal/handlers/practice_handler.go b/internal/handlers/practice_handler.go index 42264c2..c855f0f 100644 --- a/internal/handlers/practice_handler.go +++ b/internal/handlers/practice_handler.go @@ -259,10 +259,17 @@ func SubmitPracticeAnswer(c *gin.Context) { } // 调用AI评分服务(带重试机制) - aiService := services.NewAIGradingService() + aiService, err := services.NewAIGradingService() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": fmt.Sprintf("AI评分服务初始化失败: %v", err), + }) + return + } + var aiResult *services.AIGradingResult - var err error - maxRetries := 5 + maxRetries := 3 // 减少重试次数,从5次改为3次 // 区分简答题和论述题的评分方式 if strings.HasSuffix(question.Type, "-essay") { @@ -276,8 +283,8 @@ func SubmitPracticeAnswer(c *gin.Context) { } log.Printf("论述题AI评分失败 (第 %d 次尝试): %v", attempt, err) if attempt < maxRetries { - // 等待一小段时间后重试(指数退避) - time.Sleep(time.Second * time.Duration(attempt)) + // 减少等待时间,使用固定延迟500ms而不是指数退避 + time.Sleep(500 * time.Millisecond) } } } else { @@ -301,8 +308,8 @@ func SubmitPracticeAnswer(c *gin.Context) { } log.Printf("简答题AI评分失败 (第 %d 次尝试): %v", attempt, err) if attempt < maxRetries { - // 等待一小段时间后重试(指数退避) - time.Sleep(time.Second * time.Duration(attempt)) + // 减少等待时间,使用固定延迟500ms而不是指数退避 + time.Sleep(500 * time.Millisecond) } } } @@ -980,8 +987,16 @@ func ExplainQuestion(c *gin.Context) { c.Header("X-Accel-Buffering", "no") // 调用AI服务生成流式解析 - aiService := services.NewAIGradingService() - err := aiService.ExplainQuestionStream(c.Writer, question.Question, standardAnswerStr, question.Type) + aiService, err := services.NewAIGradingService() + if err != nil { + log.Printf("AI服务初始化失败: %v", err) + // SSE格式的错误消息 + fmt.Fprintf(c.Writer, "data: [ERROR] AI服务暂时不可用,请稍后再试\n\n") + c.Writer.(http.Flusher).Flush() + return + } + + err = aiService.ExplainQuestionStream(c.Writer, question.Question, standardAnswerStr, question.Type) if err != nil { log.Printf("AI流式解析失败: %v", err) // SSE格式的错误消息 diff --git a/internal/services/ai_grading.go b/internal/services/ai_grading.go index 3d04beb..0ca5619 100644 --- a/internal/services/ai_grading.go +++ b/internal/services/ai_grading.go @@ -1,37 +1,27 @@ package services import ( - "ankao/pkg/config" - "context" "encoding/json" "fmt" - "io" "net/http" "strings" - - "github.com/sashabaranov/go-openai" ) -// AIGradingService AI评分服务 +// AIGradingService AI评分服务接口(使用百度云AppBuilder) type AIGradingService struct { - client *openai.Client - config *config.AIConfig + baiduService *BaiduAIGradingService } // NewAIGradingService 创建AI评分服务实例 -func NewAIGradingService() *AIGradingService { - cfg := config.GetAIConfig() - - // 创建OpenAI客户端配置 - clientConfig := openai.DefaultConfig(cfg.APIKey) - clientConfig.BaseURL = cfg.BaseURL + "/v1" // 标准OpenAI API格式需要/v1后缀 - - client := openai.NewClientWithConfig(clientConfig) +func NewAIGradingService() (*AIGradingService, error) { + baiduService, err := NewBaiduAIGradingService() + if err != nil { + return nil, fmt.Errorf("创建百度云AI服务失败: %w", err) + } return &AIGradingService{ - client: client, - config: cfg, - } + baiduService: baiduService, + }, nil } // AIGradingResult AI评分结果 @@ -40,104 +30,18 @@ type AIGradingResult struct { IsCorrect bool `json:"is_correct"` // 是否正确 (Score >= 60 视为正确) Feedback string `json:"feedback"` // 评语 Suggestion string `json:"suggestion"` // 改进建议 - ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题) + ReferenceAnswer string `json:"reference_answer"` // 参考答案(论述题) ScoringRationale string `json:"scoring_rationale"` // 评分依据 } -// GradeEssay 对论述题进行AI评分(不需要标准答案) +// GradeEssay 对论述题进行AI评分(不需要标准答案) // question: 题目内容 // userAnswer: 用户答案 func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingResult, error) { - // 构建评分提示词(论述题评分模式) - prompt := fmt.Sprintf(`你是一位专业的保密领域阅卷老师,请对以下论述题进行评分。 - -题目:%s - -学生答案:%s - -评分依据: -请依据以下保密法律法规和管理制度进行分析和评分: -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. 生成一个专业的参考答案(reference_answer,150-300字,必须引用相关法规条文) -4. 给出评分依据(scoring_rationale,说明依据了哪些法规和条文,80-150字) -5. 给出简短的评语(feedback,说明得分情况,不超过80字) -6. 给出具体的改进建议(suggestion,如果分数在90分以上可以简短,否则必须指出具体改进方向,不超过80字) - -请按照以下JSON格式返回结果: -{ - "score": 75, - "is_correct": true, - "reference_answer": "根据《中华人民共和国保守国家秘密法》第XX条...", - "scoring_rationale": "依据《保密法》第XX条、《保密法实施条例》第XX条...", - "feedback": "论述较为完整,论点明确,但论据不够充分,缺少具体法规引用", - "suggestion": "建议补充《保密法》相关条文,加强论点之间的逻辑联系" -} - -注意: -1. 只返回JSON格式的结果,不要有其他内容 -2. 参考答案必须专业、准确,体现保密法规要求 -3. 评分依据必须具体引用法规条文`, question, 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, - Content: prompt, - }, - }, - Temperature: 0.3, // 低温度,保证评分相对稳定 - }, - ) - - if err != nil { - return nil, fmt.Errorf("AI评分失败: %w", err) + if s.baiduService == nil { + return nil, fmt.Errorf("百度云AI服务未初始化") } - - 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 + return s.baiduService.GradeEssay(question, userAnswer) } // GradeShortAnswer 对简答题进行AI评分 @@ -145,89 +49,38 @@ func (s *AIGradingService) GradeEssay(question, userAnswer string) (*AIGradingRe // 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的描述" + if s.baiduService == nil { + return nil, fmt.Errorf("百度云AI服务未初始化") + } + return s.baiduService.GradeShortAnswer(question, standardAnswer, userAnswer) } -注意: -1. 只返回JSON格式的结果,不要有其他内容 -2. 必须严格对照标准答案评分,不要过于宽松 -3. 评分依据必须说明符合或违反了哪些法规要求`, question, standardAnswer, userAnswer) +// AIExplanationResult AI解析结果 +type AIExplanationResult struct { + Explanation string `json:"explanation"` // 题目解析 +} - // 调用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, - Content: prompt, - }, - }, - Temperature: 0, // 温度为0,获得最确定、最一致的评分结果 - }, - ) - - if err != nil { - return nil, fmt.Errorf("AI评分失败: %w", err) +// ExplainQuestionStream 生成题目解析(流式输出) +// writer: HTTP响应写入器 +// question: 题目内容 +// standardAnswer: 标准答案 +// questionType: 题目类型 +func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error { + if s.baiduService == nil { + return fmt.Errorf("百度云AI服务未初始化") } + return s.baiduService.ExplainQuestionStream(writer, question, standardAnswer, questionType) +} - if len(resp.Choices) == 0 { - return nil, fmt.Errorf("AI未返回评分结果") +// ExplainQuestion 生成题目解析 +// question: 题目内容 +// standardAnswer: 标准答案 +// questionType: 题目类型 +func (s *AIGradingService) ExplainQuestion(question, standardAnswer, questionType string) (*AIExplanationResult, error) { + if s.baiduService == nil { + 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 + return s.baiduService.ExplainQuestion(question, standardAnswer, questionType) } // parseAIResponse 解析AI返回的JSON响应 @@ -243,223 +96,6 @@ func parseAIResponse(content string, result interface{}) error { return nil } -// AIExplanationResult AI解析结果 -type AIExplanationResult struct { - Explanation string `json:"explanation"` // 题目解析 -} - -// ExplainQuestionStream 生成题目解析(流式输出) -// writer: HTTP响应写入器 -// question: 题目内容 -// standardAnswer: 标准答案 -// questionType: 题目类型 -func (s *AIGradingService) ExplainQuestionStream(writer http.ResponseWriter, question, standardAnswer, questionType string) error { - // 构建解析提示词(直接输出,不使用JSON格式) - prompt := fmt.Sprintf(`你是一位严谨、专业的保密领域专家老师,请对以下题目进行详细解析。 - -题目类型:%s - -题目内容:%s - -标准答案:%s - -**重要依据**:请基于以下保密法律法规和管理制度进行专业分析: -1. 《中华人民共和国保守国家秘密法》 -2. 《中华人民共和国保守国家秘密法实施条例》 -3. 《保密工作管理制度2025.9.9》 -4. 《软件开发管理制度》 -5. 《涉密信息系统集成资质保密标准》 -6. 《涉密信息系统集成资质管理办法》 - -请提供一个详细的解析,要求: -1. **必须基于保密法规**:解析时必须引用相关法规条文,说明依据哪些具体法律法规 -2. **必须实事求是**:只基于题目内容、标准答案和实际法规进行解析 -3. **不要胡编乱造**:如果某些信息不确定或题目没有提供,请如实说明,不要编造法规条文 - -解析内容要求: -- **知识点**:说明题目考查的核心知识点,指出涉及哪些保密法规 -- **法规依据**:明确引用相关法律法规的具体条文(如:《保密法》第X条、《保密法实施条例》第X条等) -- **解题思路**:提供清晰的解题步骤和方法,结合保密工作实际 - -%s - -示例输出格式: -## 知识点 -本题考查的是[知识点名称],涉及《XX法规》第XX条... - -## 法规依据 -- 《中华人民共和国保守国家秘密法》第XX条规定:... -- 《保密工作管理制度2025.9.9》第X章第X节:... - -## 解题思路 -1. 首先根据《XX法规》第XX条,我们可以判断... -2. 然后结合保密工作实际,分析... - -%s - -## 总结 -%s - -**重要提醒**:请务必在解析中引用具体的法规条文,不要空泛地提及法规名称。如果不确定具体条文编号,可以说明法规的精神和要求。 - -请使用markdown格式输出解析内容。`, - questionType, - question, - standardAnswer, - // 根据题目类型添加特定要求 - func() string { - if questionType == "single-selection" || questionType == "multiple-selection" { - return `- **选项分析**:对于选择题,必须逐项分析每个选项的对错及原因,并说明依据哪些法规 -- **记忆口诀**:如果适用,提供便于记忆的口诀或技巧` - } - return "- **答案解析**:详细说明为什么这个答案是正确的,并引用相关法规依据" - }(), - // 根据题目类型添加示例格式 - func() string { - if questionType == "single-selection" || questionType == "multiple-selection" { - return `## 选项分析 -- **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 "总结本题的关键要点、涉及的主要法规要求和在保密工作中的实际应用" - }(), - ) - - // 调用AI API(流式) - stream, err := s.client.CreateChatCompletionStream( - context.Background(), - openai.ChatCompletionRequest{ - Model: s.config.Model, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleSystem, - Content: "你是一位严谨、专业的保密领域专家老师,精通保密法律法规和管理制度。你擅长深入浅出地讲解题目,并能准确引用相关法规条文。你必须实事求是,只基于题目、标准答案和实际法规提供解析,不编造任何不确定的信息。你使用Markdown格式输出,让学生更容易理解。", - }, - { - Role: openai.ChatMessageRoleUser, - Content: prompt, - }, - }, - Temperature: 0.3, // 低温度,保证输出相对稳定和专业 - Stream: true, - }, - ) - - if err != nil { - return fmt.Errorf("创建流式请求失败: %w", err) - } - defer stream.Close() - - flusher, ok := writer.(http.Flusher) - if !ok { - return fmt.Errorf("响应写入器不支持Flush") - } - - // 读取流式响应并发送给客户端 - for { - response, err := stream.Recv() - if err == io.EOF { - // 流结束 - fmt.Fprintf(writer, "data: [DONE]\n\n") - flusher.Flush() - break - } - if err != nil { - return fmt.Errorf("接收流式响应失败: %w", err) - } - - // 获取增量内容 - if len(response.Choices) > 0 { - delta := response.Choices[0].Delta.Content - if delta != "" { - // 使用SSE格式发送 - fmt.Fprintf(writer, "data: %s\n\n", delta) - flusher.Flush() - } - } - } - - return nil -} - -// ExplainQuestion 生成题目解析 -// question: 题目内容 -// standardAnswer: 标准答案 -// questionType: 题目类型 -func (s *AIGradingService) ExplainQuestion(question, standardAnswer, questionType string) (*AIExplanationResult, error) { - // 构建解析提示词 - prompt := fmt.Sprintf(`你是一位经验丰富的老师,请对以下题目进行详细解析。 - -题目类型:%s - -题目内容:%s - -标准答案:%s - -请提供一个详细的解析,包括: -1. 题目考查的知识点 -2. 解题思路和方法 -3. 为什么选择这个答案 -4. 相关的重要概念或注意事项 - -请按照以下JSON格式返回结果: -{ - "explanation": "这道题考查的是...(200字以内的详细解析)" -} - -注意:只返回JSON格式的结果,不要有其他内容。`, questionType, question, standardAnswer) - - // 调用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, - Content: prompt, - }, - }, - Temperature: 0.7, // 中等温度以获得有创造性的解释 - }, - ) - - 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 AIExplanationResult - if err := parseAIResponse(content, &result); err != nil { - return nil, fmt.Errorf("解析AI响应失败: %w", err) - } - - return &result, nil -} - // removeMarkdownCodeBlock 移除markdown代码块标记 func removeMarkdownCodeBlock(s string) string { // 去除可能的```json和```标记 diff --git a/main.go b/main.go index f084625..6373f52 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ import ( "ankao/internal/handlers" "ankao/internal/middleware" "log" + "net/http" + "time" "github.com/gin-gonic/gin" ) @@ -54,7 +56,7 @@ func main() { auth.GET("/practice/statistics", handlers.GetStatistics) // 获取统计数据 // 练习进度相关API - auth.GET("/practice/progress", handlers.GetPracticeProgress) // 获取练习进度 + auth.GET("/practice/progress", handlers.GetPracticeProgress) // 获取练习进度 auth.DELETE("/practice/progress", handlers.ClearPracticeProgress) // 清除练习进度 // 错题本相关API @@ -98,9 +100,21 @@ func main() { // 当没有匹配到任何 API 路由时,尝试提供静态文件 r.NoRoute(gin.WrapH(handlers.StaticFileHandler("./web"))) - // 启动服务器 + // 创建自定义HTTP服务器,设置超时时间 port := ":8080" - if err := r.Run(port); err != nil { + server := &http.Server{ + Addr: port, + Handler: r, + ReadTimeout: 5 * time.Minute, // 读取超时:5分钟 + WriteTimeout: 5 * time.Minute, // 写入超时:5分钟 + IdleTimeout: 10 * time.Minute, // 空闲连接超时:10分钟 + MaxHeaderBytes: 1 << 20, // 最大请求头:1MB + } + + log.Printf("服务器启动在端口 %s,超时配置:读/写 5分钟", port) + + // 启动服务器 + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { panic("服务器启动失败: " + err.Error()) } } diff --git a/pkg/config/config.go b/pkg/config/config.go index b192a7b..0bbc363 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -18,9 +18,10 @@ type DatabaseConfig struct { // AIConfig AI服务配置结构 type AIConfig struct { - BaseURL string - APIKey string - Model string + BaseURL string + APIKey string + Model string + BaiduAppID string // 百度云AppBuilder应用ID } // GetDatabaseConfig 获取数据库配置 @@ -79,12 +80,14 @@ func (c *DatabaseConfig) GetDSN() string { // 优先使用环境变量,如果没有设置则使用默认值 func GetAIConfig() *AIConfig { baseURL := getEnv("AI_BASE_URL", "http://172.20.0.117") - apiKey := getEnv("AI_API_KEY", "sk-OKBmOpJx855juSOPU14cWG6Iz87tZQuv3Xg9PiaJYXdHoKcN") + apiKey := getEnv("AI_API_KEY", "bce-v3/ALTAK-TgZ1YSBmbwNXo3BIuzNZ2/768b777896453e820a2c46f38614c8e9bf43f845") model := getEnv("AI_MODEL", "deepseek-v3") + baiduAppID := getEnv("BAIDU_APP_ID", "7b336aaf-f448-46d6-9e5f-bb9e38a1167c") return &AIConfig{ - BaseURL: baseURL, - APIKey: apiKey, - Model: model, + BaseURL: baseURL, + APIKey: apiKey, + Model: model, + BaiduAppID: baiduAppID, } } diff --git a/scripts/fix_practice_progress_index.sql b/scripts/fix_practice_progress_index.sql deleted file mode 100644 index 4de8b08..0000000 --- a/scripts/fix_practice_progress_index.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 修复 practice_progress 表的唯一索引 --- 问题:之前的唯一索引只在 user_id 上,导致同一用户只能有一条进度记录 --- 修复:改为 (user_id, type) 联合唯一索引,允许同一用户有多种题型的进度 - --- 1. 删除旧的唯一索引(如果存在) -DROP INDEX IF EXISTS idx_user_question; - --- 2. 创建新的联合唯一索引 -CREATE UNIQUE INDEX IF NOT EXISTS idx_user_type ON practice_progress(user_id, type); - --- 3. 验证索引 -SELECT - indexname, - indexdef -FROM - pg_indexes -WHERE - tablename = 'practice_progress'; diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index dd78e1e..5474a69 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -3,7 +3,7 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' // 创建 axios 实例 const instance: AxiosInstance = axios.create({ baseURL: '/api', // 通过 Vite 代理转发到 Go 后端 - timeout: 10000, + timeout: 300000, // 5分钟超时(300秒),适应AI评分长时间处理 headers: { 'Content-Type': 'application/json', },