feat: pricing stat.

This commit is contained in:
2026-04-30 16:33:30 +08:00
parent bbe6dea436
commit e43a034e28
5 changed files with 88 additions and 6 deletions
+25
View File
@@ -4,6 +4,7 @@ package controllers
import ( import (
"context" // 在此处添加 context 导入 "context" // 在此处添加 context 导入
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "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{ record := models.AIAnalysisRecord{
RegionID: regionID, RegionID: regionID,
SourceType: teachingPlanSource, SourceType: teachingPlanSource,
AnalysisType: analysisType, AnalysisType: analysisType,
AnalysisResult: analysisResult.Content, AnalysisResult: analysisResult.Content,
CostJSON: costJSON,
TotalCost: totalCost,
InputTokens: analysisResult.InputTokens, InputTokens: analysisResult.InputTokens,
OutputTokens: analysisResult.OutputTokens, OutputTokens: analysisResult.OutputTokens,
InputSizeBytes: analysisResult.InputSizeBytes, InputSizeBytes: analysisResult.InputSizeBytes,
+25 -5
View File
@@ -123,6 +123,9 @@ type regionStatisticsItem struct {
AvgDurationMs float64 `json:"avgDurationMs"` AvgDurationMs float64 `json:"avgDurationMs"`
TotalOriginalFileSize int64 `json:"totalOriginalFileSize"` TotalOriginalFileSize int64 `json:"totalOriginalFileSize"`
TotalCompressedSize int64 `json:"totalCompressedSize"` TotalCompressedSize int64 `json:"totalCompressedSize"`
TotalCost float64 `json:"totalCost"`
TotalInputCost float64 `json:"totalInputCost"`
TotalOutputCost float64 `json:"totalOutputCost"`
AnalysisTypeCounts map[string]int64 `json:"analysisTypeCounts"` AnalysisTypeCounts map[string]int64 `json:"analysisTypeCounts"`
SourceTypeCounts map[string]int64 `json:"sourceTypeCounts"` SourceTypeCounts map[string]int64 `json:"sourceTypeCounts"`
FirstUsedAt *time.Time `json:"firstUsedAt"` FirstUsedAt *time.Time `json:"firstUsedAt"`
@@ -161,6 +164,9 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) {
TotalDurationMs int64 TotalDurationMs int64
TotalOriginalFileSize int64 TotalOriginalFileSize int64
TotalCompressedSize int64 TotalCompressedSize int64
TotalCost float64
TotalInputCost float64
TotalOutputCost float64
FirstUsedAt *int64 FirstUsedAt *int64
LastUsedAt *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(duration_ms), 0) as total_duration_ms,
COALESCE(SUM(original_file_size), 0) as total_original_file_size, COALESCE(SUM(original_file_size), 0) as total_original_file_size,
COALESCE(SUM(compressed_content_size), 0) as total_compressed_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, MIN(upload_time) as first_used_at,
MAX(upload_time) as last_used_at MAX(upload_time) as last_used_at
`).Group("region_id").Scan(&rawResults).Error `).Group("region_id").Scan(&rawResults).Error
@@ -290,6 +299,9 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) {
AvgDurationMs: avgDuration, AvgDurationMs: avgDuration,
TotalOriginalFileSize: r.TotalOriginalFileSize, TotalOriginalFileSize: r.TotalOriginalFileSize,
TotalCompressedSize: r.TotalCompressedSize, TotalCompressedSize: r.TotalCompressedSize,
TotalCost: r.TotalCost,
TotalInputCost: r.TotalInputCost,
TotalOutputCost: r.TotalOutputCost,
AnalysisTypeCounts: analysisTypeMap[regionID], AnalysisTypeCounts: analysisTypeMap[regionID],
SourceTypeCounts: sourceTypeMap[regionID], SourceTypeCounts: sourceTypeMap[regionID],
FirstUsedAt: firstUsedAt, FirstUsedAt: firstUsedAt,
@@ -306,6 +318,9 @@ func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) {
overall.TotalDurationMs += r.TotalDurationMs overall.TotalDurationMs += r.TotalDurationMs
overall.TotalOriginalFileSize += r.TotalOriginalFileSize overall.TotalOriginalFileSize += r.TotalOriginalFileSize
overall.TotalCompressedSize += r.TotalCompressedSize overall.TotalCompressedSize += r.TotalCompressedSize
overall.TotalCost += r.TotalCost
overall.TotalInputCost += r.TotalInputCost
overall.TotalOutputCost += r.TotalOutputCost
if firstUsedAt != nil { if firstUsedAt != nil {
if overall.FirstUsedAt == nil || firstUsedAt.Before(*overall.FirstUsedAt) { if overall.FirstUsedAt == nil || firstUsedAt.Before(*overall.FirstUsedAt) {
@@ -359,10 +374,11 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
} }
type timelineItem struct { type timelineItem struct {
Date string `json:"date"` Date string `json:"date"`
Count int64 `json:"count"` Count int64 `json:"count"`
InputTokens int64 `json:"inputTokens"` InputTokens int64 `json:"inputTokens"`
OutputTokens int64 `json:"outputTokens"` OutputTokens int64 `json:"outputTokens"`
TotalCost float64 `json:"totalCost"`
} }
type rawRegionTimeline struct { type rawRegionTimeline struct {
@@ -371,6 +387,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
Count int64 Count int64
InputTokens int64 InputTokens int64
OutputTokens int64 OutputTokens int64
TotalCost float64
} }
var rawResults []rawRegionTimeline var rawResults []rawRegionTimeline
@@ -379,7 +396,8 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
DATE(TO_TIMESTAMP(upload_time / 1000.0)) as date, DATE(TO_TIMESTAMP(upload_time / 1000.0)) as date,
COUNT(*) as count, COUNT(*) as count,
COALESCE(SUM(input_tokens), 0) as input_tokens, 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 `).Group("region_id, DATE(TO_TIMESTAMP(upload_time / 1000.0))").Order("region_id, date ASC").Scan(&rawResults).Error
if err != nil { if err != nil {
@@ -399,6 +417,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
overallMap[r.Date].Count += r.Count overallMap[r.Date].Count += r.Count
overallMap[r.Date].InputTokens += r.InputTokens overallMap[r.Date].InputTokens += r.InputTokens
overallMap[r.Date].OutputTokens += r.OutputTokens overallMap[r.Date].OutputTokens += r.OutputTokens
overallMap[r.Date].TotalCost += r.TotalCost
regionID := uint32(0) regionID := uint32(0)
if r.RegionID != nil { if r.RegionID != nil {
@@ -410,6 +429,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
Count: r.Count, Count: r.Count,
InputTokens: r.InputTokens, InputTokens: r.InputTokens,
OutputTokens: r.OutputTokens, OutputTokens: r.OutputTokens,
TotalCost: r.TotalCost,
}) })
if _, ok := regionIDSet[regionID]; !ok && regionID > 0 { if _, ok := regionIDSet[regionID]; !ok && regionID > 0 {
regionIDSet[regionID] = struct{}{} regionIDSet[regionID] = struct{}{}
+4
View File
@@ -38,6 +38,7 @@ func main() {
&models.MqttTrainingSessionRecord{}, &models.MqttTrainingSessionRecord{},
&models.Gateway{}, &models.Gateway{},
&models.AIAnalysisRecord{}, &models.AIAnalysisRecord{},
&models.AIPricingConfig{},
) )
if err := models.BackfillLegacyUserPermissions(config.DB); err != nil { if err := models.BackfillLegacyUserPermissions(config.DB); err != nil {
log.Printf("legacy user permission backfill failed: %v", err) log.Printf("legacy user permission backfill failed: %v", err)
@@ -45,6 +46,9 @@ func main() {
if err := models.EnsureDefaultAdmin(config.DB); err != nil { if err := models.EnsureDefaultAdmin(config.DB); err != nil {
log.Printf("default admin init failed: %v", err) 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 { if err := mqtt.Start(config.DB, config.App.MQTT); err != nil {
log.Printf("mqtt listener start failed: %v", err) log.Printf("mqtt listener start failed: %v", err)
+5 -1
View File
@@ -1,6 +1,8 @@
package models package models
import "gorm.io/gorm" import (
"gorm.io/gorm"
)
type AIAnalysisRecord struct { type AIAnalysisRecord struct {
gorm.Model gorm.Model
@@ -8,6 +10,8 @@ type AIAnalysisRecord struct {
SourceType string `gorm:"size:32" json:"sourceType"` SourceType string `gorm:"size:32" json:"sourceType"`
AnalysisType string `gorm:"size:32" json:"analysisType"` AnalysisType string `gorm:"size:32" json:"analysisType"`
AnalysisResult string `gorm:"type:text" json:"analysisResult"` AnalysisResult string `gorm:"type:text" json:"analysisResult"`
CostJSON string `gorm:"type:jsonb" json:"costJson"`
TotalCost float64 `json:"totalCost"`
InputTokens int `json:"inputTokens"` InputTokens int `json:"inputTokens"`
OutputTokens int `json:"outputTokens"` OutputTokens int `json:"outputTokens"`
InputSizeBytes int `json:"inputSizeBytes"` InputSizeBytes int `json:"inputSizeBytes"`
+29
View File
@@ -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
}