feat: pricing stat.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
@@ -363,6 +378,7 @@ func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
|
||||
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{}{}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+5
-1
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user