From 1dabed150b3a366931d807582da0a810ec79e053 Mon Sep 17 00:00:00 2001 From: laoboli <1293528695@qq.com> Date: Thu, 30 Apr 2026 08:54:07 +0800 Subject: [PATCH] refactor: statistics.go. --- controllers/ai.go | 11 +++-- controllers/statistics.go | 92 ++++++++++++++++++++++++++++++++------- models/analyze.go | 1 + 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/controllers/ai.go b/controllers/ai.go index 3a1dbfb..baaf0fd 100644 --- a/controllers/ai.go +++ b/controllers/ai.go @@ -27,6 +27,8 @@ import ( const ( analysisTypeHeartRateOnly = "heart_rate_only" analysisTypeHeartRateWithSteps = "heart_rate_with_steps" + sourceUpload = "upload" + sourceCloud = "cloud" ) // readDocxContent 读取 .docx 文件并将其转换为结构化文本 @@ -314,7 +316,7 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { analysisType = analysisTypeHeartRateOnly } if teachingPlanSource == "" { - teachingPlanSource = "upload" + teachingPlanSource = sourceUpload } if len(csvFiles) == 0 { @@ -391,6 +393,7 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { record := models.AIAnalysisRecord{ RegionID: regionID, SourceType: teachingPlanSource, + AnalysisType: analysisType, InputTokens: analysisResult.InputTokens, OutputTokens: analysisResult.OutputTokens, InputSizeBytes: analysisResult.InputSizeBytes, @@ -413,14 +416,14 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { func resolveTeachingPlanContent(c *gin.Context, form *multipart.Form, source string) (string, int64, error) { switch strings.ToLower(strings.TrimSpace(source)) { - case "upload": + case sourceUpload: docxFiles := form.File["teaching_plan"] if len(docxFiles) == 0 { return "", 0, fmt.Errorf("Missing required file: teaching_plan (.docx)") } content, err := readDocxContent(docxFiles[0]) return content, docxFiles[0].Size, err - case "cloud": + case sourceCloud: lessonPlanID := c.PostForm("lesson_plan_id") if strings.TrimSpace(lessonPlanID) == "" { return "", 0, fmt.Errorf("missing required field: lesson_plan_id") @@ -432,6 +435,6 @@ func resolveTeachingPlanContent(c *gin.Context, form *multipart.Form, source str content, err := readDocxContentFromPath(fileRecord.FilePath) return content, fileRecord.FileSize, err default: - return "", 0, fmt.Errorf("invalid teaching_plan_source, expected upload or cloud") + return "", 0, fmt.Errorf("invalid teaching_plan_source, expected %s or %s", sourceUpload, sourceCloud) } } diff --git a/controllers/statistics.go b/controllers/statistics.go index 8289563..987ae5f 100644 --- a/controllers/statistics.go +++ b/controllers/statistics.go @@ -111,19 +111,21 @@ func (sc *StatisticsController) DeleteAIAnalysisRecord(c *gin.Context) { // --- 统计接口 --- type regionStatisticsItem struct { - RegionID uint32 `json:"regionId"` - KindergartenName string `json:"kindergartenName"` - Count int64 `json:"count"` - TotalInputTokens int64 `json:"totalInputTokens"` - TotalOutputTokens int64 `json:"totalOutputTokens"` - TotalInputSizeBytes int64 `json:"totalInputSizeBytes"` - TotalOutputSizeBytes int64 `json:"totalOutputSizeBytes"` - TotalDurationMs int64 `json:"totalDurationMs"` - AvgDurationMs float64 `json:"avgDurationMs"` - TotalOriginalFileSize int64 `json:"totalOriginalFileSize"` - TotalCompressedSize int64 `json:"totalCompressedSize"` - FirstUsedAt *time.Time `json:"firstUsedAt"` - LastUsedAt *time.Time `json:"lastUsedAt"` + RegionID uint32 `json:"regionId"` + KindergartenName string `json:"kindergartenName"` + Count int64 `json:"count"` + TotalInputTokens int64 `json:"totalInputTokens"` + TotalOutputTokens int64 `json:"totalOutputTokens"` + TotalInputSizeBytes int64 `json:"totalInputSizeBytes"` + TotalOutputSizeBytes int64 `json:"totalOutputSizeBytes"` + TotalDurationMs int64 `json:"totalDurationMs"` + AvgDurationMs float64 `json:"avgDurationMs"` + TotalOriginalFileSize int64 `json:"totalOriginalFileSize"` + TotalCompressedSize int64 `json:"totalCompressedSize"` + AnalysisTypeCounts map[string]int64 `json:"analysisTypeCounts"` + SourceTypeCounts map[string]int64 `json:"sourceTypeCounts"` + FirstUsedAt *time.Time `json:"firstUsedAt"` + LastUsedAt *time.Time `json:"lastUsedAt"` } func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { @@ -173,8 +175,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { COALESCE(SUM(duration_ms), 0) as total_duration_ms, COALESCE(SUM(original_file_size), 0) as total_original_file_size, COALESCE(SUM(compressed_content_size), 0) as total_compressed_size, - MIN(created_at) as first_used_at, - MAX(created_at) as last_used_at + MIN(upload_time) as first_used_at, + MAX(upload_time) as last_used_at `).Group("region_id").Scan(&rawResults).Error if err != nil { @@ -182,6 +184,52 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { return } + type analysisTypeCount struct { + RegionID *uint32 + AnalysisType string + Count int64 + } + var analysisTypeResults []analysisTypeCount + if err := query.Select("region_id, analysis_type, COUNT(*) as count").Group("region_id, analysis_type").Scan(&analysisTypeResults).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to query analysis type statistics") + return + } + + type sourceTypeCount struct { + RegionID *uint32 + SourceType string + Count int64 + } + var sourceTypeResults []sourceTypeCount + if err := query.Select("region_id, source_type, COUNT(*) as count").Group("region_id, source_type").Scan(&sourceTypeResults).Error; err != nil { + writeError(c, http.StatusInternalServerError, "failed to query source type statistics") + return + } + + analysisTypeMap := make(map[uint32]map[string]int64) + for _, r := range analysisTypeResults { + regionID := uint32(0) + if r.RegionID != nil { + regionID = *r.RegionID + } + if analysisTypeMap[regionID] == nil { + analysisTypeMap[regionID] = make(map[string]int64) + } + analysisTypeMap[regionID][r.AnalysisType] = r.Count + } + + sourceTypeMap := make(map[uint32]map[string]int64) + for _, r := range sourceTypeResults { + regionID := uint32(0) + if r.RegionID != nil { + regionID = *r.RegionID + } + if sourceTypeMap[regionID] == nil { + sourceTypeMap[regionID] = make(map[string]int64) + } + sourceTypeMap[regionID][r.SourceType] = r.Count + } + // 收集所有 regionId 查询幼儿园名称 regionIDs := make([]uint32, 0, len(rawResults)) for _, r := range rawResults { @@ -199,7 +247,10 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { } } - overall := regionStatisticsItem{} + overall := regionStatisticsItem{ + AnalysisTypeCounts: make(map[string]int64), + SourceTypeCounts: make(map[string]int64), + } regions := make(map[string]regionStatisticsItem, len(rawResults)) for _, r := range rawResults { @@ -227,6 +278,8 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { AvgDurationMs: avgDuration, TotalOriginalFileSize: r.TotalOriginalFileSize, TotalCompressedSize: r.TotalCompressedSize, + AnalysisTypeCounts: analysisTypeMap[regionID], + SourceTypeCounts: sourceTypeMap[regionID], FirstUsedAt: r.FirstUsedAt, LastUsedAt: r.LastUsedAt, } @@ -254,6 +307,13 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { } } + for _, r := range analysisTypeResults { + overall.AnalysisTypeCounts[r.AnalysisType] += r.Count + } + for _, r := range sourceTypeResults { + overall.SourceTypeCounts[r.SourceType] += r.Count + } + if overall.Count > 0 { overall.AvgDurationMs = float64(overall.TotalDurationMs) / float64(overall.Count) } diff --git a/models/analyze.go b/models/analyze.go index 3830f60..7647689 100644 --- a/models/analyze.go +++ b/models/analyze.go @@ -6,6 +6,7 @@ type AIAnalysisRecord struct { gorm.Model RegionID *uint32 `gorm:"index" json:"regionId"` SourceType string `gorm:"size:32" json:"sourceType"` + AnalysisType string `gorm:"size:32" json:"analysisType"` InputTokens int `json:"inputTokens"` OutputTokens int `json:"outputTokens"` InputSizeBytes int `json:"inputSizeBytes"`