From eedee45861473aca13de7952fe4b548fa977a9f7 Mon Sep 17 00:00:00 2001 From: wood chen Date: Thu, 1 May 2025 01:27:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BB=B7=E6=A0=BC=E5=A4=84?= =?UTF-8?q?=E7=90=86=E7=9B=B8=E5=85=B3=E7=9A=84=E6=97=A7=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E4=BB=B7=E6=A0=BC=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=9A=84=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E5=AD=97=E6=AE=B5=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E5=92=8C?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/handlers/one_hub/price_rates.go | 257 ++++++++++- backend/handlers/price.go | 167 ------- backend/handlers/prices.go | 247 ++++++++++- backend/models/price.go | 20 +- frontend/src/views/Prices.vue | 560 ++++++++++++++++++++---- 5 files changed, 956 insertions(+), 295 deletions(-) delete mode 100644 backend/handlers/price.go diff --git a/backend/handlers/one_hub/price_rates.go b/backend/handlers/one_hub/price_rates.go index 310209f..e994eb5 100644 --- a/backend/handlers/one_hub/price_rates.go +++ b/backend/handlers/one_hub/price_rates.go @@ -11,13 +11,28 @@ import ( "aimodels-prices/models" ) +// ExtraRatios 扩展价格倍率结构 +type ExtraRatios struct { + InputAudioTokens *float64 `json:"input_audio_tokens,omitempty"` + OutputAudioTokens *float64 `json:"output_audio_tokens,omitempty"` + CachedTokens *float64 `json:"cached_tokens,omitempty"` + CachedReadTokens *float64 `json:"cached_read_tokens,omitempty"` + CachedWriteTokens *float64 `json:"cached_write_tokens,omitempty"` + ReasoningTokens *float64 `json:"reasoning_tokens,omitempty"` + InputTextTokens *float64 `json:"input_text_tokens,omitempty"` + OutputTextTokens *float64 `json:"output_text_tokens,omitempty"` + InputImageTokens *float64 `json:"input_image_tokens,omitempty"` + OutputImageTokens *float64 `json:"output_image_tokens,omitempty"` +} + // PriceRate 价格倍率结构 type PriceRate struct { - Model string `json:"model"` - Type string `json:"type"` - ChannelType uint `json:"channel_type"` - Input float64 `json:"input"` - Output float64 `json:"output"` + Model string `json:"model"` + Type string `json:"type"` + ChannelType uint `json:"channel_type"` + Input float64 `json:"input"` + Output float64 `json:"output"` + ExtraRatios *ExtraRatios `json:"extra_ratios,omitempty"` } // GetPriceRates 获取价格倍率 @@ -34,7 +49,7 @@ func GetPriceRates(c *gin.Context) { // 使用索引优化查询,只查询需要的字段 var prices []models.Price - if err := database.DB.Select("model, billing_type, channel_type, input_price, output_price, currency, status"). + if err := database.DB.Select("model, billing_type, channel_type, input_price, output_price, currency, status, input_audio_tokens, output_audio_tokens, cached_tokens, cached_read_tokens, cached_write_tokens, reasoning_tokens, input_text_tokens, output_text_tokens, input_image_tokens, output_image_tokens"). Where("status = 'approved'"). Find(&prices).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch prices"}) @@ -59,6 +74,119 @@ func GetPriceRates(c *gin.Context) { outputRate = round(price.OutputPrice/14, 4) } + // 创建额外价格倍率 + var extraRatios *ExtraRatios + + // 只有当至少有一个扩展价格字段不为nil时才创建ExtraRatios + if price.InputAudioTokens != nil || price.OutputAudioTokens != nil || + price.CachedTokens != nil || price.CachedReadTokens != nil || price.CachedWriteTokens != nil || + price.ReasoningTokens != nil || price.InputTextTokens != nil || price.OutputTextTokens != nil || + price.InputImageTokens != nil || price.OutputImageTokens != nil { + + extraRatios = &ExtraRatios{} + + // 计算各扩展价格字段的倍率 + if price.InputAudioTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.InputAudioTokens/2, 4) + } else { + rate = round(*price.InputAudioTokens/14, 4) + } + extraRatios.InputAudioTokens = &rate + } + + if price.OutputAudioTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.OutputAudioTokens/2, 4) + } else { + rate = round(*price.OutputAudioTokens/14, 4) + } + extraRatios.OutputAudioTokens = &rate + } + + if price.CachedTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.CachedTokens/2, 4) + } else { + rate = round(*price.CachedTokens/14, 4) + } + extraRatios.CachedTokens = &rate + } + + if price.CachedReadTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.CachedReadTokens/2, 4) + } else { + rate = round(*price.CachedReadTokens/14, 4) + } + extraRatios.CachedReadTokens = &rate + } + + if price.CachedWriteTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.CachedWriteTokens/2, 4) + } else { + rate = round(*price.CachedWriteTokens/14, 4) + } + extraRatios.CachedWriteTokens = &rate + } + + if price.ReasoningTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.ReasoningTokens/2, 4) + } else { + rate = round(*price.ReasoningTokens/14, 4) + } + extraRatios.ReasoningTokens = &rate + } + + if price.InputTextTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.InputTextTokens/2, 4) + } else { + rate = round(*price.InputTextTokens/14, 4) + } + extraRatios.InputTextTokens = &rate + } + + if price.OutputTextTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.OutputTextTokens/2, 4) + } else { + rate = round(*price.OutputTextTokens/14, 4) + } + extraRatios.OutputTextTokens = &rate + } + + if price.InputImageTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.InputImageTokens/2, 4) + } else { + rate = round(*price.InputImageTokens/14, 4) + } + extraRatios.InputImageTokens = &rate + } + + if price.OutputImageTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.OutputImageTokens/2, 4) + } else { + rate = round(*price.OutputImageTokens/14, 4) + } + extraRatios.OutputImageTokens = &rate + } + } + // 创建当前价格的PriceRate currentRate := PriceRate{ Model: price.Model, @@ -66,6 +194,7 @@ func GetPriceRates(c *gin.Context) { ChannelType: price.ChannelType, Input: inputRate, Output: outputRate, + ExtraRatios: extraRatios, } // 转换为小写以实现不区分大小写比较 @@ -115,7 +244,7 @@ func GetOfficialPriceRates(c *gin.Context) { // 使用索引优化查询,只查询需要的字段,并添加厂商ID筛选条件 var prices []models.Price result := database.DB.Model(&models.Price{}). - Select("model", "billing_type", "channel_type", "input_price", "output_price", "currency", "status"). + Select("model, billing_type, channel_type, input_price, output_price, currency, status, input_audio_tokens, output_audio_tokens, cached_tokens, cached_read_tokens, cached_write_tokens, reasoning_tokens, input_text_tokens, output_text_tokens, input_image_tokens, output_image_tokens"). Where(&models.Price{Status: "approved"}). Where("channel_type < ?", 1000). Find(&prices) @@ -143,6 +272,119 @@ func GetOfficialPriceRates(c *gin.Context) { outputRate = round(price.OutputPrice/14, 4) } + // 创建额外价格倍率 + var extraRatios *ExtraRatios + + // 只有当至少有一个扩展价格字段不为nil时才创建ExtraRatios + if price.InputAudioTokens != nil || price.OutputAudioTokens != nil || + price.CachedTokens != nil || price.CachedReadTokens != nil || price.CachedWriteTokens != nil || + price.ReasoningTokens != nil || price.InputTextTokens != nil || price.OutputTextTokens != nil || + price.InputImageTokens != nil || price.OutputImageTokens != nil { + + extraRatios = &ExtraRatios{} + + // 计算各扩展价格字段的倍率 + if price.InputAudioTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.InputAudioTokens/2, 4) + } else { + rate = round(*price.InputAudioTokens/14, 4) + } + extraRatios.InputAudioTokens = &rate + } + + if price.OutputAudioTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.OutputAudioTokens/2, 4) + } else { + rate = round(*price.OutputAudioTokens/14, 4) + } + extraRatios.OutputAudioTokens = &rate + } + + if price.CachedTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.CachedTokens/2, 4) + } else { + rate = round(*price.CachedTokens/14, 4) + } + extraRatios.CachedTokens = &rate + } + + if price.CachedReadTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.CachedReadTokens/2, 4) + } else { + rate = round(*price.CachedReadTokens/14, 4) + } + extraRatios.CachedReadTokens = &rate + } + + if price.CachedWriteTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.CachedWriteTokens/2, 4) + } else { + rate = round(*price.CachedWriteTokens/14, 4) + } + extraRatios.CachedWriteTokens = &rate + } + + if price.ReasoningTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.ReasoningTokens/2, 4) + } else { + rate = round(*price.ReasoningTokens/14, 4) + } + extraRatios.ReasoningTokens = &rate + } + + if price.InputTextTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.InputTextTokens/2, 4) + } else { + rate = round(*price.InputTextTokens/14, 4) + } + extraRatios.InputTextTokens = &rate + } + + if price.OutputTextTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.OutputTextTokens/2, 4) + } else { + rate = round(*price.OutputTextTokens/14, 4) + } + extraRatios.OutputTextTokens = &rate + } + + if price.InputImageTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.InputImageTokens/2, 4) + } else { + rate = round(*price.InputImageTokens/14, 4) + } + extraRatios.InputImageTokens = &rate + } + + if price.OutputImageTokens != nil { + var rate float64 + if price.Currency == "USD" { + rate = round(*price.OutputImageTokens/2, 4) + } else { + rate = round(*price.OutputImageTokens/14, 4) + } + extraRatios.OutputImageTokens = &rate + } + } + // 创建当前价格的PriceRate currentRate := PriceRate{ Model: price.Model, @@ -150,6 +392,7 @@ func GetOfficialPriceRates(c *gin.Context) { ChannelType: price.ChannelType, Input: inputRate, Output: outputRate, + ExtraRatios: extraRatios, } // 转换为小写以实现不区分大小写比较 diff --git a/backend/handlers/price.go b/backend/handlers/price.go deleted file mode 100644 index 08a1748..0000000 --- a/backend/handlers/price.go +++ /dev/null @@ -1,167 +0,0 @@ -package handlers - -import ( - "aimodels-prices/database" - "aimodels-prices/models" - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -// 在createPrice函数中添加新字段的处理 -func createPrice(c *gin.Context) { - var price models.Price - if err := c.ShouldBindJSON(&price); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 设置创建时间和状态 - price.Status = "pending" - price.CreatedAt = time.Now() - - // 验证必填字段 - if price.Model == "" || price.ModelType == "" || price.BillingType == "" || - price.Currency == "" || price.CreatedBy == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "必填字段不能为空"}) - return - } - - // 验证扩展价格字段(如果提供) - if price.InputAudioTokens != nil && *price.InputAudioTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "音频输入倍率不能为负数"}) - return - } - if price.CachedReadTokens != nil && *price.CachedReadTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "缓存读取倍率不能为负数"}) - return - } - if price.ReasoningTokens != nil && *price.ReasoningTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "推理倍率不能为负数"}) - return - } - if price.InputTextTokens != nil && *price.InputTextTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "输入文本倍率不能为负数"}) - return - } - if price.OutputTextTokens != nil && *price.OutputTextTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "输出文本倍率不能为负数"}) - return - } - if price.InputImageTokens != nil && *price.InputImageTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "输入图片倍率不能为负数"}) - return - } - if price.OutputImageTokens != nil && *price.OutputImageTokens < 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": "输出图片倍率不能为负数"}) - return - } - - // 创建价格记录 - if err := database.DB.Create(&price).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusCreated, price) -} - -// 在updatePrice函数中添加新字段的处理 -func updatePrice(c *gin.Context) { - var price models.Price - if err := c.ShouldBindJSON(&price); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // 获取现有价格记录 - var existingPrice models.Price - if err := database.DB.First(&existingPrice, price.ID).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "价格记录不存在"}) - return - } - - // 更新临时字段 - updates := map[string]interface{}{ - "temp_model": price.Model, - "temp_model_type": price.ModelType, - "temp_billing_type": price.BillingType, - "temp_channel_type": price.ChannelType, - "temp_currency": price.Currency, - "temp_input_price": price.InputPrice, - "temp_output_price": price.OutputPrice, - "temp_input_audio_tokens": price.InputAudioTokens, - "temp_cached_read_tokens": price.CachedReadTokens, - "temp_reasoning_tokens": price.ReasoningTokens, - "temp_input_text_tokens": price.InputTextTokens, - "temp_output_text_tokens": price.OutputTextTokens, - "temp_input_image_tokens": price.InputImageTokens, - "temp_output_image_tokens": price.OutputImageTokens, - "temp_price_source": price.PriceSource, - "updated_by": price.UpdatedBy, - "status": "pending", - } - - if err := database.DB.Model(&existingPrice).Updates(updates).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, existingPrice) -} - -// 在approvePrice函数中添加新字段的处理 -func approvePrice(c *gin.Context) { - id := c.Param("id") - - var price models.Price - if err := database.DB.First(&price, id).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "价格记录不存在"}) - return - } - - // 更新字段 - updates := map[string]interface{}{ - "model": price.TempModel, - "model_type": price.TempModelType, - "billing_type": price.TempBillingType, - "channel_type": price.TempChannelType, - "currency": price.TempCurrency, - "input_price": price.TempInputPrice, - "output_price": price.TempOutputPrice, - "input_audio_tokens": price.TempInputAudioTokens, - "cached_read_tokens": price.TempCachedReadTokens, - "reasoning_tokens": price.TempReasoningTokens, - "input_text_tokens": price.TempInputTextTokens, - "output_text_tokens": price.TempOutputTextTokens, - "input_image_tokens": price.TempInputImageTokens, - "output_image_tokens": price.TempOutputImageTokens, - "price_source": price.TempPriceSource, - "status": "approved", - // 清空临时字段 - "temp_model": nil, - "temp_model_type": nil, - "temp_billing_type": nil, - "temp_channel_type": nil, - "temp_currency": nil, - "temp_input_price": nil, - "temp_output_price": nil, - "temp_input_audio_tokens": nil, - "temp_cached_read_tokens": nil, - "temp_reasoning_tokens": nil, - "temp_input_text_tokens": nil, - "temp_output_text_tokens": nil, - "temp_input_image_tokens": nil, - "temp_output_image_tokens": nil, - "temp_price_source": nil, - "updated_by": nil, - } - - if err := database.DB.Model(&price).Updates(updates).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusOK, price) -} diff --git a/backend/handlers/prices.go b/backend/handlers/prices.go index b4a1467..7553e68 100644 --- a/backend/handlers/prices.go +++ b/backend/handlers/prices.go @@ -115,6 +115,17 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, return math.Abs(a-b) < epsilon } + // 比较指针类型的浮点数是否相等 + pointerPriceEqual := func(a, b *float64) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return priceEqual(*a, *b) + } + // 检查价格是否有变化 if isAdmin { // 管理员直接更新主字段,检查是否有实际变化 @@ -125,6 +136,16 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, existingPrice.Currency == price.Currency && priceEqual(existingPrice.InputPrice, price.InputPrice) && priceEqual(existingPrice.OutputPrice, price.OutputPrice) && + pointerPriceEqual(existingPrice.InputAudioTokens, price.InputAudioTokens) && + pointerPriceEqual(existingPrice.OutputAudioTokens, price.OutputAudioTokens) && + pointerPriceEqual(existingPrice.CachedTokens, price.CachedTokens) && + pointerPriceEqual(existingPrice.CachedReadTokens, price.CachedReadTokens) && + pointerPriceEqual(existingPrice.CachedWriteTokens, price.CachedWriteTokens) && + pointerPriceEqual(existingPrice.ReasoningTokens, price.ReasoningTokens) && + pointerPriceEqual(existingPrice.InputTextTokens, price.InputTextTokens) && + pointerPriceEqual(existingPrice.OutputTextTokens, price.OutputTextTokens) && + pointerPriceEqual(existingPrice.InputImageTokens, price.InputImageTokens) && + pointerPriceEqual(existingPrice.OutputImageTokens, price.OutputImageTokens) && existingPrice.PriceSource == price.PriceSource { // 没有变化,不需要更新 return *existingPrice, false, nil @@ -138,6 +159,16 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, existingPrice.Currency = price.Currency existingPrice.InputPrice = price.InputPrice existingPrice.OutputPrice = price.OutputPrice + existingPrice.InputAudioTokens = price.InputAudioTokens + existingPrice.OutputAudioTokens = price.OutputAudioTokens + existingPrice.CachedTokens = price.CachedTokens + existingPrice.CachedReadTokens = price.CachedReadTokens + existingPrice.CachedWriteTokens = price.CachedWriteTokens + existingPrice.ReasoningTokens = price.ReasoningTokens + existingPrice.InputTextTokens = price.InputTextTokens + existingPrice.OutputTextTokens = price.OutputTextTokens + existingPrice.InputImageTokens = price.InputImageTokens + existingPrice.OutputImageTokens = price.OutputImageTokens existingPrice.PriceSource = price.PriceSource existingPrice.Status = "approved" existingPrice.UpdatedBy = &username @@ -148,6 +179,16 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, existingPrice.TempCurrency = nil existingPrice.TempInputPrice = nil existingPrice.TempOutputPrice = nil + existingPrice.TempInputAudioTokens = nil + existingPrice.TempOutputAudioTokens = nil + existingPrice.TempCachedTokens = nil + existingPrice.TempCachedReadTokens = nil + existingPrice.TempCachedWriteTokens = nil + existingPrice.TempReasoningTokens = nil + existingPrice.TempInputTextTokens = nil + existingPrice.TempOutputTextTokens = nil + existingPrice.TempInputImageTokens = nil + existingPrice.TempOutputImageTokens = nil existingPrice.TempPriceSource = nil // 保存更新 @@ -168,6 +209,16 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, existingPrice.Currency != price.Currency || !priceEqual(existingPrice.InputPrice, price.InputPrice) || !priceEqual(existingPrice.OutputPrice, price.OutputPrice) || + !pointerPriceEqual(existingPrice.InputAudioTokens, price.InputAudioTokens) || + !pointerPriceEqual(existingPrice.OutputAudioTokens, price.OutputAudioTokens) || + !pointerPriceEqual(existingPrice.CachedTokens, price.CachedTokens) || + !pointerPriceEqual(existingPrice.CachedReadTokens, price.CachedReadTokens) || + !pointerPriceEqual(existingPrice.CachedWriteTokens, price.CachedWriteTokens) || + !pointerPriceEqual(existingPrice.ReasoningTokens, price.ReasoningTokens) || + !pointerPriceEqual(existingPrice.InputTextTokens, price.InputTextTokens) || + !pointerPriceEqual(existingPrice.OutputTextTokens, price.OutputTextTokens) || + !pointerPriceEqual(existingPrice.InputImageTokens, price.InputImageTokens) || + !pointerPriceEqual(existingPrice.OutputImageTokens, price.OutputImageTokens) || existingPrice.PriceSource != price.PriceSource { hasChanges = true } @@ -182,6 +233,16 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, (existingPrice.TempCurrency == nil || *existingPrice.TempCurrency == price.Currency) && (existingPrice.TempInputPrice == nil || priceEqual(*existingPrice.TempInputPrice, price.InputPrice)) && (existingPrice.TempOutputPrice == nil || priceEqual(*existingPrice.TempOutputPrice, price.OutputPrice)) && + (existingPrice.TempInputAudioTokens == nil || pointerPriceEqual(existingPrice.TempInputAudioTokens, price.InputAudioTokens)) && + (existingPrice.TempOutputAudioTokens == nil || pointerPriceEqual(existingPrice.TempOutputAudioTokens, price.OutputAudioTokens)) && + (existingPrice.TempCachedTokens == nil || pointerPriceEqual(existingPrice.TempCachedTokens, price.CachedTokens)) && + (existingPrice.TempCachedReadTokens == nil || pointerPriceEqual(existingPrice.TempCachedReadTokens, price.CachedReadTokens)) && + (existingPrice.TempCachedWriteTokens == nil || pointerPriceEqual(existingPrice.TempCachedWriteTokens, price.CachedWriteTokens)) && + (existingPrice.TempReasoningTokens == nil || pointerPriceEqual(existingPrice.TempReasoningTokens, price.ReasoningTokens)) && + (existingPrice.TempInputTextTokens == nil || pointerPriceEqual(existingPrice.TempInputTextTokens, price.InputTextTokens)) && + (existingPrice.TempOutputTextTokens == nil || pointerPriceEqual(existingPrice.TempOutputTextTokens, price.OutputTextTokens)) && + (existingPrice.TempInputImageTokens == nil || pointerPriceEqual(existingPrice.TempInputImageTokens, price.InputImageTokens)) && + (existingPrice.TempOutputImageTokens == nil || pointerPriceEqual(existingPrice.TempOutputImageTokens, price.OutputImageTokens)) && (existingPrice.TempPriceSource == nil || *existingPrice.TempPriceSource == price.PriceSource) { // 与之前提交的临时值相同,不需要更新 hasChanges = false @@ -201,6 +262,16 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, existingPrice.TempCurrency = &price.Currency existingPrice.TempInputPrice = &price.InputPrice existingPrice.TempOutputPrice = &price.OutputPrice + existingPrice.TempInputAudioTokens = price.InputAudioTokens + existingPrice.TempOutputAudioTokens = price.OutputAudioTokens + existingPrice.TempCachedTokens = price.CachedTokens + existingPrice.TempCachedReadTokens = price.CachedReadTokens + existingPrice.TempCachedWriteTokens = price.CachedWriteTokens + existingPrice.TempReasoningTokens = price.ReasoningTokens + existingPrice.TempInputTextTokens = price.InputTextTokens + existingPrice.TempOutputTextTokens = price.OutputTextTokens + existingPrice.TempInputImageTokens = price.InputImageTokens + existingPrice.TempOutputImageTokens = price.OutputImageTokens existingPrice.TempPriceSource = &price.PriceSource existingPrice.Status = "pending" existingPrice.UpdatedBy = &username @@ -219,6 +290,38 @@ func ProcessPrice(price models.Price, existingPrice *models.Price, isAdmin bool, } price.CreatedBy = username + // 验证扩展价格字段(如果提供) + if price.InputAudioTokens != nil && *price.InputAudioTokens < 0 { + return price, false, fmt.Errorf("音频输入价格不能为负数") + } + if price.OutputAudioTokens != nil && *price.OutputAudioTokens < 0 { + return price, false, fmt.Errorf("音频输出价格不能为负数") + } + if price.CachedTokens != nil && *price.CachedTokens < 0 { + return price, false, fmt.Errorf("缓存价格不能为负数") + } + if price.CachedReadTokens != nil && *price.CachedReadTokens < 0 { + return price, false, fmt.Errorf("缓存读取价格不能为负数") + } + if price.CachedWriteTokens != nil && *price.CachedWriteTokens < 0 { + return price, false, fmt.Errorf("缓存写入价格不能为负数") + } + if price.ReasoningTokens != nil && *price.ReasoningTokens < 0 { + return price, false, fmt.Errorf("推理价格不能为负数") + } + if price.InputTextTokens != nil && *price.InputTextTokens < 0 { + return price, false, fmt.Errorf("输入文本价格不能为负数") + } + if price.OutputTextTokens != nil && *price.OutputTextTokens < 0 { + return price, false, fmt.Errorf("输出文本价格不能为负数") + } + if price.InputImageTokens != nil && *price.InputImageTokens < 0 { + return price, false, fmt.Errorf("输入图片价格不能为负数") + } + if price.OutputImageTokens != nil && *price.OutputImageTokens < 0 { + return price, false, fmt.Errorf("输出图片价格不能为负数") + } + // 保存新记录 if err := database.DB.Create(&price).Error; err != nil { return price, false, err @@ -326,6 +429,36 @@ func UpdatePriceStatus(c *gin.Context) { if price.TempOutputPrice != nil { updateMap["output_price"] = *price.TempOutputPrice } + if price.TempInputAudioTokens != nil { + updateMap["input_audio_tokens"] = *price.TempInputAudioTokens + } + if price.TempOutputAudioTokens != nil { + updateMap["output_audio_tokens"] = *price.TempOutputAudioTokens + } + if price.TempCachedTokens != nil { + updateMap["cached_tokens"] = *price.TempCachedTokens + } + if price.TempCachedReadTokens != nil { + updateMap["cached_read_tokens"] = *price.TempCachedReadTokens + } + if price.TempCachedWriteTokens != nil { + updateMap["cached_write_tokens"] = *price.TempCachedWriteTokens + } + if price.TempReasoningTokens != nil { + updateMap["reasoning_tokens"] = *price.TempReasoningTokens + } + if price.TempInputTextTokens != nil { + updateMap["input_text_tokens"] = *price.TempInputTextTokens + } + if price.TempOutputTextTokens != nil { + updateMap["output_text_tokens"] = *price.TempOutputTextTokens + } + if price.TempInputImageTokens != nil { + updateMap["input_image_tokens"] = *price.TempInputImageTokens + } + if price.TempOutputImageTokens != nil { + updateMap["output_image_tokens"] = *price.TempOutputImageTokens + } if price.TempPriceSource != nil { updateMap["price_source"] = *price.TempPriceSource } @@ -338,6 +471,16 @@ func UpdatePriceStatus(c *gin.Context) { updateMap["temp_currency"] = nil updateMap["temp_input_price"] = nil updateMap["temp_output_price"] = nil + updateMap["temp_input_audio_tokens"] = nil + updateMap["temp_output_audio_tokens"] = nil + updateMap["temp_cached_tokens"] = nil + updateMap["temp_cached_read_tokens"] = nil + updateMap["temp_cached_write_tokens"] = nil + updateMap["temp_reasoning_tokens"] = nil + updateMap["temp_input_text_tokens"] = nil + updateMap["temp_output_text_tokens"] = nil + updateMap["temp_input_image_tokens"] = nil + updateMap["temp_output_image_tokens"] = nil updateMap["temp_price_source"] = nil updateMap["updated_by"] = nil @@ -361,17 +504,27 @@ func UpdatePriceStatus(c *gin.Context) { } else { // 如果是更新的价格,恢复到原始状态(清除临时字段并设置状态为approved) if err := tx.Model(&price).Updates(map[string]interface{}{ - "status": "approved", // 恢复为已批准状态 - "updated_at": time.Now(), - "temp_model": nil, - "temp_model_type": nil, - "temp_billing_type": nil, - "temp_channel_type": nil, - "temp_currency": nil, - "temp_input_price": nil, - "temp_output_price": nil, - "temp_price_source": nil, - "updated_by": nil, + "status": "approved", // 恢复为已批准状态 + "updated_at": time.Now(), + "temp_model": nil, + "temp_model_type": nil, + "temp_billing_type": nil, + "temp_channel_type": nil, + "temp_currency": nil, + "temp_input_price": nil, + "temp_output_price": nil, + "temp_input_audio_tokens": nil, + "temp_output_audio_tokens": nil, + "temp_cached_tokens": nil, + "temp_cached_read_tokens": nil, + "temp_cached_write_tokens": nil, + "temp_reasoning_tokens": nil, + "temp_input_text_tokens": nil, + "temp_output_text_tokens": nil, + "temp_input_image_tokens": nil, + "temp_output_image_tokens": nil, + "temp_price_source": nil, + "updated_by": nil, }).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price status"}) @@ -538,6 +691,36 @@ func ApproveAllPrices(c *gin.Context) { if price.TempOutputPrice != nil { updateMap["output_price"] = *price.TempOutputPrice } + if price.TempInputAudioTokens != nil { + updateMap["input_audio_tokens"] = *price.TempInputAudioTokens + } + if price.TempOutputAudioTokens != nil { + updateMap["output_audio_tokens"] = *price.TempOutputAudioTokens + } + if price.TempCachedTokens != nil { + updateMap["cached_tokens"] = *price.TempCachedTokens + } + if price.TempCachedReadTokens != nil { + updateMap["cached_read_tokens"] = *price.TempCachedReadTokens + } + if price.TempCachedWriteTokens != nil { + updateMap["cached_write_tokens"] = *price.TempCachedWriteTokens + } + if price.TempReasoningTokens != nil { + updateMap["reasoning_tokens"] = *price.TempReasoningTokens + } + if price.TempInputTextTokens != nil { + updateMap["input_text_tokens"] = *price.TempInputTextTokens + } + if price.TempOutputTextTokens != nil { + updateMap["output_text_tokens"] = *price.TempOutputTextTokens + } + if price.TempInputImageTokens != nil { + updateMap["input_image_tokens"] = *price.TempInputImageTokens + } + if price.TempOutputImageTokens != nil { + updateMap["output_image_tokens"] = *price.TempOutputImageTokens + } if price.TempPriceSource != nil { updateMap["price_source"] = *price.TempPriceSource } @@ -550,6 +733,16 @@ func ApproveAllPrices(c *gin.Context) { updateMap["temp_currency"] = nil updateMap["temp_input_price"] = nil updateMap["temp_output_price"] = nil + updateMap["temp_input_audio_tokens"] = nil + updateMap["temp_output_audio_tokens"] = nil + updateMap["temp_cached_tokens"] = nil + updateMap["temp_cached_read_tokens"] = nil + updateMap["temp_cached_write_tokens"] = nil + updateMap["temp_reasoning_tokens"] = nil + updateMap["temp_input_text_tokens"] = nil + updateMap["temp_output_text_tokens"] = nil + updateMap["temp_input_image_tokens"] = nil + updateMap["temp_output_image_tokens"] = nil updateMap["temp_price_source"] = nil updateMap["updated_by"] = nil @@ -575,17 +768,27 @@ func ApproveAllPrices(c *gin.Context) { } else { // 如果是更新的价格,恢复到原始状态(清除临时字段并设置状态为approved) if err := tx.Model(&price).Updates(map[string]interface{}{ - "status": "approved", // 恢复为已批准状态 - "updated_at": time.Now(), - "temp_model": nil, - "temp_model_type": nil, - "temp_billing_type": nil, - "temp_channel_type": nil, - "temp_currency": nil, - "temp_input_price": nil, - "temp_output_price": nil, - "temp_price_source": nil, - "updated_by": nil, + "status": "approved", // 恢复为已批准状态 + "updated_at": time.Now(), + "temp_model": nil, + "temp_model_type": nil, + "temp_billing_type": nil, + "temp_channel_type": nil, + "temp_currency": nil, + "temp_input_price": nil, + "temp_output_price": nil, + "temp_input_audio_tokens": nil, + "temp_output_audio_tokens": nil, + "temp_cached_tokens": nil, + "temp_cached_read_tokens": nil, + "temp_cached_write_tokens": nil, + "temp_reasoning_tokens": nil, + "temp_input_text_tokens": nil, + "temp_output_text_tokens": nil, + "temp_input_image_tokens": nil, + "temp_output_image_tokens": nil, + "temp_price_source": nil, + "updated_by": nil, }).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject prices"}) diff --git a/backend/models/price.go b/backend/models/price.go index 5b05efa..0e55f55 100644 --- a/backend/models/price.go +++ b/backend/models/price.go @@ -15,13 +15,16 @@ type Price struct { Currency string `json:"currency" gorm:"not null"` // USD or CNY InputPrice float64 `json:"input_price" gorm:"not null"` OutputPrice float64 `json:"output_price" gorm:"not null"` - InputAudioTokens *float64 `json:"input_audio_tokens,omitempty"` // 音频输入倍率 - CachedReadTokens *float64 `json:"cached_read_tokens,omitempty"` // 缓存读取倍率 - ReasoningTokens *float64 `json:"reasoning_tokens,omitempty"` // 推理倍率 - InputTextTokens *float64 `json:"input_text_tokens,omitempty"` // 输入文本倍率 - OutputTextTokens *float64 `json:"output_text_tokens,omitempty"` // 输出文本倍率 - InputImageTokens *float64 `json:"input_image_tokens,omitempty"` // 输入图片倍率 - OutputImageTokens *float64 `json:"output_image_tokens,omitempty"` // 输出图片倍率 + InputAudioTokens *float64 `json:"input_audio_tokens,omitempty"` // 音频输入价格 + OutputAudioTokens *float64 `json:"output_audio_tokens,omitempty"` // 音频输出价格 + CachedTokens *float64 `json:"cached_tokens,omitempty"` // 缓存价格 + CachedReadTokens *float64 `json:"cached_read_tokens,omitempty"` // 缓存读取价格 + CachedWriteTokens *float64 `json:"cached_write_tokens,omitempty"` // 缓存写入价格 + ReasoningTokens *float64 `json:"reasoning_tokens,omitempty"` // 推理价格 + InputTextTokens *float64 `json:"input_text_tokens,omitempty"` // 输入文本价格 + OutputTextTokens *float64 `json:"output_text_tokens,omitempty"` // 输出文本价格 + InputImageTokens *float64 `json:"input_image_tokens,omitempty"` // 输入图片价格 + OutputImageTokens *float64 `json:"output_image_tokens,omitempty"` // 输出图片价格 PriceSource string `json:"price_source" gorm:"not null"` Status string `json:"status" gorm:"not null;default:pending;index:idx_status"` // pending, approved, rejected CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_created_at"` @@ -37,7 +40,10 @@ type Price struct { TempInputPrice *float64 `json:"temp_input_price,omitempty" gorm:"column:temp_input_price"` TempOutputPrice *float64 `json:"temp_output_price,omitempty" gorm:"column:temp_output_price"` TempInputAudioTokens *float64 `json:"temp_input_audio_tokens,omitempty"` + TempOutputAudioTokens *float64 `json:"temp_output_audio_tokens,omitempty"` + TempCachedTokens *float64 `json:"temp_cached_tokens,omitempty"` TempCachedReadTokens *float64 `json:"temp_cached_read_tokens,omitempty"` + TempCachedWriteTokens *float64 `json:"temp_cached_write_tokens,omitempty"` TempReasoningTokens *float64 `json:"temp_reasoning_tokens,omitempty"` TempInputTextTokens *float64 `json:"temp_input_text_tokens,omitempty"` TempOutputTextTokens *float64 `json:"temp_output_text_tokens,omitempty"` diff --git a/frontend/src/views/Prices.vue b/frontend/src/views/Prices.vue index c647bcb..d5e01dc 100644 --- a/frontend/src/views/Prices.vue +++ b/frontend/src/views/Prices.vue @@ -84,7 +84,7 @@