// controllers/ai.go package controllers import ( "context" // 在此处添加 context 导入 "fmt" "github.com/gin-gonic/gin" "github.com/sashabaranov/go-openai" "hr_receiver/util" "io" "io/ioutil" "log" "mime/multipart" "net/http" "os" ) // 配置文件 (与 main.go 保持一致) const ( BaseURL = "https://api.lkeap.cloud.tencent.com/v1" APIKey = "sk-Y4zjnwulSuSlf60mrzwCxq2ipktHSs4jZHgWeQOArWuWJEOd" // 请替换为实际的 API Key Model = "deepseek-v3" ) // readDocxContent 读取 .docx 文件并将其转换为结构化文本 // 修改为先保存临时文件再读取 func readDocxContent(fileHeader *multipart.FileHeader) (string, error) { // 1. 创建临时文件 tempFile, err := os.CreateTemp("", "upload_*.docx") if err != nil { return "", fmt.Errorf("failed to create temporary file: %w", err) } defer os.Remove(tempFile.Name()) // 确保函数结束时删除临时文件 defer tempFile.Close() // 2. 打开上传的文件流 src, err := fileHeader.Open() if err != nil { return "", fmt.Errorf("failed to open uploaded file: %w", err) } defer src.Close() // 3. 将上传的文件内容复制到临时文件 _, err = io.Copy(tempFile, src) if err != nil { return "", fmt.Errorf("failed to copy file to temporary location: %w", err) } // 4. 获取临时文件的完整路径 tempFilePath := tempFile.Name() str, err := util.DocxToStructuredPrompt(tempFilePath) if err != nil { return "", fmt.Errorf("failed to parse docx with go-docx: %w", err) } // 注意:表格、图片等复杂元素的处理可能需要更复杂的逻辑,这里仅处理简单文本 return str, nil } // readCSVContent 读取 .csv 文件内容 // 修改为先保存临时文件再读取 func readCSVContent(fileHeader *multipart.FileHeader) (string, error) { // 1. 创建临时文件 tempFile, err := os.CreateTemp("", "upload_*.csv") if err != nil { return "", fmt.Errorf("failed to create temporary file: %w", err) } defer os.Remove(tempFile.Name()) // 确保函数结束时删除临时文件 defer tempFile.Close() // 2. 打开上传的文件流 src, err := fileHeader.Open() if err != nil { return "", fmt.Errorf("failed to open uploaded file: %w", err) } defer src.Close() // 3. 将上传的文件内容复制到临时文件 _, err = io.Copy(tempFile, src) if err != nil { return "", fmt.Errorf("failed to copy file to temporary location: %w", err) } // 4. 读取临时文件内容 content, err := ioutil.ReadFile(tempFile.Name()) if err != nil { return "", fmt.Errorf("failed to read CSV content from temporary file: %w", err) } return string(content), nil } // buildAnalysisPrompt 构建发送给 AI 的提示词 func buildAnalysisPrompt(teachingPlanContent, heartRateContent string) string { return fmt.Sprintf(`请根据以下体育课堂的教案和心率监测数据,生成一份详细的课堂分析报告: ## 教案内容: %s ## 心率监测数据: %s 这是一份幼儿园体育课的教案和课程心率监测数据,请帮对照分析课程教学效果,运动量和运动负荷情况是否科学,并提出课程设计的优化方案。 优化方案参考如下格式,教学过程需要详细一些: # 幼儿体育教案(华侨大学版本) | 项目 | 内容 | | ------------ | -------------------------------- | | **课程名** | | | **年段** | 小 中 大 | | **教师姓名** | | | **时间** | 年 月 日 | | **地点** | | | **人数** | 男: 女: | | **时长** | 分钟 | | **天气预报** | 晴 雨 阴;温度 ℃ | | **器材准备** | | ## 教学目标 | 类型 | 目标 | | -------- | ------------ | | **体能目标** | | | **技能目标** | | | **情感目标** | | ## 教学过程 | 阶段 | 阶段 | 项目名称 | 引导语及教学方法 | 队形/站位/留意点 | 目标心率区间 | 时间(分) | | ---------- | -------- | ----------------------------- | ------------------------ | --------------------- | ------------ | ---------- | | **准备部分** | 热身 | | | | | 3 | | | 注意力游戏 | | | | | 3 | | **正课部分** | 基本素质练习及常规意识培养环节 | | | | | 5 | | | 复习环节 | | | | | 5 | | | 新授环节 | | | | | 8 | | **结束部分** | 社会性及情感目标游戏 | | | | | 4 | | | 整理放松 | | | | | 2 | 请以专业体育教师的视角,提供详细的数据分析和教学建议。`, teachingPlanContent, heartRateContent) } // callAIForAnalysis 调用大模型进行分析 func callAIForAnalysis(prompt string) (string, error) { config := openai.DefaultConfig(APIKey) config.BaseURL = BaseURL client := openai.NewClientWithConfig(config) resp, err := client.CreateChatCompletion( context.Background(), openai.ChatCompletionRequest{ Model: Model, Messages: []openai.ChatCompletionMessage{ { Role: openai.ChatMessageRoleUser, Content: prompt, }, }, Temperature: 0.6, // 可调整 TopP: 0.6, // 可调整 MaxTokens: 4000, // 根据需要调整 }, ) if err != nil { return "", fmt.Errorf("API call failed: %w", err) } if len(resp.Choices) == 0 { return "", fmt.Errorf("no choices returned from API") } return resp.Choices[0].Message.Content, nil } // AnalyzeByAI Gin 控制器方法 func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { // 1. 解析多部分表单请求 form, err := c.MultipartForm() if err != nil { log.Printf("Error parsing multipart form: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Failed to parse form: %v", err)}) return } // 2. 获取文件列表 docxFiles := form.File["teaching_plan"] // 假设前端字段名为 'teaching_plan' csvFiles := form.File["heart_rate_data"] // 假设前端字段名为 'heart_rate_data' if len(docxFiles) == 0 || len(csvFiles) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Missing required files: teaching_plan (.docx) or heart_rate_data (.csv)"}) return } // 3. 读取文件内容 // 注意:这里我们只取第一个上传的文件 teachingPlanFileHeader := docxFiles[0] heartRateFileHeader := csvFiles[0] teachingPlanContent, err := readDocxContent(teachingPlanFileHeader) if err != nil { log.Printf("Error reading teaching plan file (%s): %v", teachingPlanFileHeader.Filename, err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to process teaching plan file: %v", err)}) return } heartRateContent, err := readCSVContent(heartRateFileHeader) if err != nil { log.Printf("Error reading heart rate file (%s): %v", heartRateFileHeader.Filename, err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to process heart rate file: %v", err)}) return } // 4. 构建 Prompt prompt := buildAnalysisPrompt(teachingPlanContent, heartRateContent) // 5. 调用 AI 分析 analysisResult, err := callAIForAnalysis(prompt) if err != nil { log.Printf("Error calling AI for analysis: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("AI analysis failed: %v", err)}) return } //outputFile := ".md" //ioutil.WriteFile(outputFile, []byte(analysisResult), 0644) // 6. 返回结果 // 方式一:返回 JSON 结构 c.JSON(http.StatusOK, gin.H{ "status": "success", "data": analysisResult, }) }