diff --git a/config/config.go b/config/config.go index 5ac0680..a4536dc 100644 --- a/config/config.go +++ b/config/config.go @@ -12,19 +12,23 @@ import ( var DB *gorm.DB var App AppConfig +type DBConfig struct { + Host string `mapstructure:"host" yaml:"host"` + Port string `mapstructure:"port" yaml:"port"` + User string `mapstructure:"user" yaml:"user"` + Password string `mapstructure:"password" yaml:"password"` + Name string `mapstructure:"name" yaml:"name"` +} + +type AIConfig struct { + BaseURL string `mapstructure:"base_url" yaml:"base_url"` + APIKey string `mapstructure:"api_key" yaml:"api_key"` + Model string `mapstructure:"model" yaml:"model"` +} + type AppConfig struct { - DB struct { - Host string `yaml:"host"` - Port string `yaml:"port"` - User string `yaml:"user"` - Password string `yaml:"password"` - Name string `yaml:"name"` - } `yaml:"database"` - AI struct { - BaseURL string `yaml:"base_url"` - APIKey string `yaml:"api_key"` - Model string `yaml:"model"` - } `yaml:"ai"` + DB DBConfig `mapstructure:"database" yaml:"database"` + AI AIConfig `mapstructure:"ai" yaml:"ai"` } func InitConfig() { @@ -40,6 +44,10 @@ func InitConfig() { } func ConnectDB() { + if err := validateDBConfig(App.DB); err != nil { + panic("Failed to connect database: " + err.Error()) + } + dsn := "host=" + App.DB.Host + " user=" + App.DB.User + " password=" + App.DB.Password + @@ -54,6 +62,22 @@ func ConnectDB() { } } +func validateDBConfig(cfg DBConfig) error { + if cfg.Host == "" { + return fmt.Errorf("missing config: database.host") + } + if cfg.Port == "" { + return fmt.Errorf("missing config: database.port") + } + if cfg.User == "" { + return fmt.Errorf("missing config: database.user") + } + if cfg.Name == "" { + return fmt.Errorf("missing config: database.name") + } + return nil +} + func GetAIConfig() (baseURL, apiKey, model string, err error) { if App.AI.BaseURL == "" { return "", "", "", fmt.Errorf("missing config: ai.base_url") diff --git a/controllers/ai.go b/controllers/ai.go index c242584..4d098df 100644 --- a/controllers/ai.go +++ b/controllers/ai.go @@ -17,6 +17,11 @@ import ( "os" ) +const ( + analysisTypeHeartRateOnly = "heart_rate_only" + analysisTypeHeartRateWithSteps = "heart_rate_with_steps" +) + // readDocxContent 读取 .docx 文件并将其转换为结构化文本 // 修改为先保存临时文件再读取 func readDocxContent(fileHeader *multipart.FileHeader) (string, error) { @@ -86,7 +91,65 @@ func readCSVContent(fileHeader *multipart.FileHeader) (string, error) { } // buildAnalysisPrompt 构建发送给 AI 的提示词 -func buildAnalysisPrompt(teachingPlanContent, heartRateContent string) string { +func buildAnalysisPrompt(teachingPlanContent, heartRateContent, analysisType, stepContent string) string { + if analysisType == analysisTypeHeartRateWithSteps { + return fmt.Sprintf(`请根据以下体育课堂的教案、心率监测数据和训练结束步数汇总,生成一份详细的课堂分析报告: + +## 教案内容: +%s + +## 心率监测数据: +%s + +## 训练结束步数汇总: +%s + +这是一份幼儿园体育课的教案、课程心率监测数据和训练结束步数汇总。请结合三类信息分析课程教学效果、运动量和运动负荷情况是否科学,并提出课程设计的优化方案。 + +分析要求: +1. 步数只作为移动量、活动密度和参与度的辅助参考,不能替代心率负荷判断。 +2. 请判断步数与心率是否一致。例如高步数高心率通常说明移动量较大;低步数高心率则可能是力量、支撑、跳跃、对抗或其他无氧/原地高强度活动。 +3. 不要简单以步数高低判断运动量是否合理,必须结合教案内容、动作形式和心率变化综合判断。 +4. 在教学建议中明确说明本节课是否适合继续使用步数作为辅助分析指标。 + +优化方案参考如下格式,教学过程需要详细一些: +# 幼儿体育教案(华侨大学版本) + +| 项目 | 内容 | +| ------------ | -------------------------------- | +| **课程名** | | +| **年段** | 小 中 大 | +| **教师姓名** | | +| **时间** | 年 月 日 | +| **地点** | | +| **人数** | 男: 女: | +| **时长** | 分钟 | +| **天气预报** | 晴 雨 阴;温度 ℃ | +| **器材准备** | | + +## 教学目标 + +| 类型 | 目标 | +| -------- | ------------ | +| **体能目标** | | +| **技能目标** | | +| **情感目标** | | + +## 教学过程 + +| 阶段 | 阶段 | 项目名称 | 引导语及教学方法 | 队形/站位/留意点 | 目标心率区间 | 时间(分) | +| ---------- | -------- | ----------------------------- | ------------------------ | --------------------- | ------------ | ---------- | +| **准备部分** | 热身 | | | | | 3 | +| | 注意力游戏 | | | | | 3 | +| **正课部分** | 基本素质练习及常规意识培养环节 | | | | | 5 | +| | 复习环节 | | | | | 5 | +| | 新授环节 | | | | | 8 | +| **结束部分** | 社会性及情感目标游戏 | | | | | 4 | +| | 整理放松 | | | | | 2 | + +请以专业体育教师的视角,提供详细的数据分析和教学建议。`, teachingPlanContent, heartRateContent, stepContent) + } + return fmt.Sprintf(`请根据以下体育课堂的教案和心率监测数据,生成一份详细的课堂分析报告: ## 教案内容: @@ -185,11 +248,20 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { // 2. 获取文件列表 docxFiles := form.File["teaching_plan"] // 假设前端字段名为 'teaching_plan' csvFiles := form.File["heart_rate_data"] // 假设前端字段名为 'heart_rate_data' + stepFiles := form.File["step_data"] + analysisType := c.PostForm("analysis_type") + if analysisType == "" { + analysisType = analysisTypeHeartRateOnly + } 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 } + if analysisType == analysisTypeHeartRateWithSteps && len(stepFiles) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Missing required file: step_data (.csv) for heart_rate_with_steps"}) + return + } // 3. 读取文件内容 // 注意:这里我们只取第一个上传的文件 @@ -210,8 +282,19 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { return } + stepContent := "" + if analysisType == analysisTypeHeartRateWithSteps { + stepFileHeader := stepFiles[0] + stepContent, err = readCSVContent(stepFileHeader) + if err != nil { + log.Printf("Error reading step file (%s): %v", stepFileHeader.Filename, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to process step file: %v", err)}) + return + } + } + // 4. 构建 Prompt - prompt := buildAnalysisPrompt(teachingPlanContent, heartRateContent) + prompt := buildAnalysisPrompt(teachingPlanContent, heartRateContent, analysisType, stepContent) // 5. 调用 AI 分析 analysisResult, err := callAIForAnalysis(prompt) diff --git a/main.go b/main.go index d857126..ac52e3d 100644 --- a/main.go +++ b/main.go @@ -29,5 +29,5 @@ func main() { // 启动服务 r := routes.SetupRouter() - r.Run(":8080") + r.Run(":8081") }