From e43a034e280b9c4484375d3e6439b34e0915fbdc Mon Sep 17 00:00:00 2001 From: laoboli <1293528695@qq.com> Date: Thu, 30 Apr 2026 16:33:30 +0800 Subject: [PATCH] feat: pricing stat. --- controllers/ai.go | 25 +++++++++++++++++++++++++ controllers/statistics.go | 30 +++++++++++++++++++++++++----- main.go | 4 ++++ models/analyze.go | 6 +++++- models/pricing.go | 29 +++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 models/pricing.go diff --git a/controllers/ai.go b/controllers/ai.go index 6bb2e47..0a055f5 100644 --- a/controllers/ai.go +++ b/controllers/ai.go @@ -4,6 +4,7 @@ package controllers import ( "context" // 在此处添加 context 导入 + "encoding/json" "errors" "fmt" "github.com/gin-gonic/gin" @@ -390,11 +391,35 @@ func (tc *TrainingController) AnalyzeByAI(c *gin.Context) { } } + // 计算费用 + var pricing models.AIPricingConfig + var costJSON string + var totalCost float64 + if err := config.DB.First(&pricing).Error; err == nil { + inputCost := float64(analysisResult.InputTokens) * pricing.InputPricePerMillion / 1_000_000 + outputCost := float64(analysisResult.OutputTokens) * pricing.OutputPricePerMillion / 1_000_000 + totalCost = inputCost + outputCost + + costInfo := map[string]interface{}{ + "pricingName": pricing.Name, + "provider": pricing.Provider, + "inputPricePerMillion": pricing.InputPricePerMillion, + "outputPricePerMillion": pricing.OutputPricePerMillion, + "inputCost": inputCost, + "outputCost": outputCost, + } + if b, err := json.Marshal(costInfo); err == nil { + costJSON = string(b) + } + } + record := models.AIAnalysisRecord{ RegionID: regionID, SourceType: teachingPlanSource, AnalysisType: analysisType, AnalysisResult: analysisResult.Content, + CostJSON: costJSON, + TotalCost: totalCost, InputTokens: analysisResult.InputTokens, OutputTokens: analysisResult.OutputTokens, InputSizeBytes: analysisResult.InputSizeBytes, diff --git a/controllers/statistics.go b/controllers/statistics.go index 43182b7..8c95fa3 100644 --- a/controllers/statistics.go +++ b/controllers/statistics.go @@ -123,6 +123,9 @@ type regionStatisticsItem struct { AvgDurationMs float64 `json:"avgDurationMs"` TotalOriginalFileSize int64 `json:"totalOriginalFileSize"` TotalCompressedSize int64 `json:"totalCompressedSize"` + TotalCost float64 `json:"totalCost"` + TotalInputCost float64 `json:"totalInputCost"` + TotalOutputCost float64 `json:"totalOutputCost"` AnalysisTypeCounts map[string]int64 `json:"analysisTypeCounts"` SourceTypeCounts map[string]int64 `json:"sourceTypeCounts"` FirstUsedAt *time.Time `json:"firstUsedAt"` @@ -161,6 +164,9 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { TotalDurationMs int64 TotalOriginalFileSize int64 TotalCompressedSize int64 + TotalCost float64 + TotalInputCost float64 + TotalOutputCost float64 FirstUsedAt *int64 LastUsedAt *int64 } @@ -176,6 +182,9 @@ 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, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM((cost_json::jsonb->>'inputCost')::float8), 0) as total_input_cost, + COALESCE(SUM((cost_json::jsonb->>'outputCost')::float8), 0) as total_output_cost, MIN(upload_time) as first_used_at, MAX(upload_time) as last_used_at `).Group("region_id").Scan(&rawResults).Error @@ -290,6 +299,9 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { AvgDurationMs: avgDuration, TotalOriginalFileSize: r.TotalOriginalFileSize, TotalCompressedSize: r.TotalCompressedSize, + TotalCost: r.TotalCost, + TotalInputCost: r.TotalInputCost, + TotalOutputCost: r.TotalOutputCost, AnalysisTypeCounts: analysisTypeMap[regionID], SourceTypeCounts: sourceTypeMap[regionID], FirstUsedAt: firstUsedAt, @@ -306,6 +318,9 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) { overall.TotalDurationMs += r.TotalDurationMs overall.TotalOriginalFileSize += r.TotalOriginalFileSize overall.TotalCompressedSize += r.TotalCompressedSize + overall.TotalCost += r.TotalCost + overall.TotalInputCost += r.TotalInputCost + overall.TotalOutputCost += r.TotalOutputCost if firstUsedAt != nil { if overall.FirstUsedAt == nil || firstUsedAt.Before(*overall.FirstUsedAt) { @@ -359,10 +374,11 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { } type timelineItem struct { - Date string `json:"date"` - Count int64 `json:"count"` - InputTokens int64 `json:"inputTokens"` - OutputTokens int64 `json:"outputTokens"` + Date string `json:"date"` + Count int64 `json:"count"` + InputTokens int64 `json:"inputTokens"` + OutputTokens int64 `json:"outputTokens"` + TotalCost float64 `json:"totalCost"` } type rawRegionTimeline struct { @@ -371,6 +387,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { Count int64 InputTokens int64 OutputTokens int64 + TotalCost float64 } var rawResults []rawRegionTimeline @@ -379,7 +396,8 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { DATE(TO_TIMESTAMP(upload_time / 1000.0)) as date, COUNT(*) as count, COALESCE(SUM(input_tokens), 0) as input_tokens, - COALESCE(SUM(output_tokens), 0) as output_tokens + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(total_cost), 0) as total_cost `).Group("region_id, DATE(TO_TIMESTAMP(upload_time / 1000.0))").Order("region_id, date ASC").Scan(&rawResults).Error if err != nil { @@ -399,6 +417,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { overallMap[r.Date].Count += r.Count overallMap[r.Date].InputTokens += r.InputTokens overallMap[r.Date].OutputTokens += r.OutputTokens + overallMap[r.Date].TotalCost += r.TotalCost regionID := uint32(0) if r.RegionID != nil { @@ -410,6 +429,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) { Count: r.Count, InputTokens: r.InputTokens, OutputTokens: r.OutputTokens, + TotalCost: r.TotalCost, }) if _, ok := regionIDSet[regionID]; !ok && regionID > 0 { regionIDSet[regionID] = struct{}{} diff --git a/main.go b/main.go index 36c2e9d..606d969 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ func main() { &models.MqttTrainingSessionRecord{}, &models.Gateway{}, &models.AIAnalysisRecord{}, + &models.AIPricingConfig{}, ) if err := models.BackfillLegacyUserPermissions(config.DB); err != nil { log.Printf("legacy user permission backfill failed: %v", err) @@ -45,6 +46,9 @@ func main() { if err := models.EnsureDefaultAdmin(config.DB); err != nil { log.Printf("default admin init failed: %v", err) } + if err := models.EnsureDefaultAIPricing(config.DB); err != nil { + log.Printf("default ai pricing init failed: %v", err) + } if err := mqtt.Start(config.DB, config.App.MQTT); err != nil { log.Printf("mqtt listener start failed: %v", err) diff --git a/models/analyze.go b/models/analyze.go index 4946fa4..353ee49 100644 --- a/models/analyze.go +++ b/models/analyze.go @@ -1,6 +1,8 @@ package models -import "gorm.io/gorm" +import ( + "gorm.io/gorm" +) type AIAnalysisRecord struct { gorm.Model @@ -8,6 +10,8 @@ type AIAnalysisRecord struct { SourceType string `gorm:"size:32" json:"sourceType"` AnalysisType string `gorm:"size:32" json:"analysisType"` AnalysisResult string `gorm:"type:text" json:"analysisResult"` + CostJSON string `gorm:"type:jsonb" json:"costJson"` + TotalCost float64 `json:"totalCost"` InputTokens int `json:"inputTokens"` OutputTokens int `json:"outputTokens"` InputSizeBytes int `json:"inputSizeBytes"` diff --git a/models/pricing.go b/models/pricing.go new file mode 100644 index 0000000..ad6919e --- /dev/null +++ b/models/pricing.go @@ -0,0 +1,29 @@ +package models + +import ( + "gorm.io/gorm" +) + +type AIPricingConfig struct { + gorm.Model + Name string `gorm:"size:64;uniqueIndex" json:"name"` + Provider string `gorm:"size:64" json:"provider"` + InputPricePerMillion float64 `json:"inputPricePerMillion"` + OutputPricePerMillion float64 `json:"outputPricePerMillion"` +} + +func EnsureDefaultAIPricing(db *gorm.DB) error { + var count int64 + if err := db.Model(&AIPricingConfig{}).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return nil + } + return db.Create(&AIPricingConfig{ + Name: "deepseek-v4-flash", + Provider: "tencentmaas", + InputPricePerMillion: 1, + OutputPricePerMillion: 2, + }).Error +}