删除价格处理相关的旧代码,更新价格模型以支持新的扩展价格字段,优化前端展示逻辑,提升用户体验和代码可维护性。

This commit is contained in:
wood chen 2025-05-01 01:27:06 +08:00
parent 817f51b75a
commit eedee45861
5 changed files with 956 additions and 295 deletions

View File

@ -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,
}
// 转换为小写以实现不区分大小写比较

View File

@ -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)
}

View File

@ -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"})

View File

@ -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"`

View File

@ -84,7 +84,7 @@
</div>
</template>
<template v-else>
<div v-for="price in prices" :key="price.id" class="price-card">
<div v-for="price in prices" :key="price.id" class="price-card" :class="price.status">
<div class="price-card-header">
<div class="provider-info">
<el-image
@ -133,72 +133,99 @@
</div>
</div>
<div v-if="hasExtendedPrices(price)" class="extended-prices">
<div class="extended-prices" v-if="hasAnyExtendedPrices(price)">
<div class="section-title">扩展价格</div>
<div class="extended-price-grid">
<template v-if="price.input_audio_tokens">
<template v-if="hasSpecificPrice(price.input_audio_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">音频输入倍率</span>
<span class="ext-price-label">音频输入价格</span>
<span class="ext-price-value">{{ price.input_audio_tokens }}</span>
<el-tag v-if="price.temp_input_audio_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_input_audio_tokens }}
</el-tag>
</div>
</template>
<template v-if="price.cached_read_tokens">
<template v-if="hasSpecificPrice(price.output_audio_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">缓存读取倍率</span>
<span class="ext-price-label">音频输出价格</span>
<span class="ext-price-value">{{ price.output_audio_tokens }}</span>
<el-tag v-if="price.temp_output_audio_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_output_audio_tokens }}
</el-tag>
</div>
</template>
<template v-if="hasSpecificPrice(price.cached_read_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">缓存读取价格</span>
<span class="ext-price-value">{{ price.cached_read_tokens }}</span>
<el-tag v-if="price.temp_cached_read_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_cached_read_tokens }}
</el-tag>
</div>
</template>
<template v-if="price.reasoning_tokens">
<template v-if="hasSpecificPrice(price.cached_write_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">推理倍率</span>
<span class="ext-price-label">缓存写入价格</span>
<span class="ext-price-value">{{ price.cached_write_tokens }}</span>
<el-tag v-if="price.temp_cached_write_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_cached_write_tokens }}
</el-tag>
</div>
</template>
<template v-if="hasSpecificPrice(price.reasoning_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">推理价格</span>
<span class="ext-price-value">{{ price.reasoning_tokens }}</span>
<el-tag v-if="price.temp_reasoning_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_reasoning_tokens }}
</el-tag>
</div>
</template>
<template v-if="price.input_text_tokens">
<template v-if="hasSpecificPrice(price.input_text_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">输入文本倍率</span>
<span class="ext-price-label">输入文本价格</span>
<span class="ext-price-value">{{ price.input_text_tokens }}</span>
<el-tag v-if="price.temp_input_text_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_input_text_tokens }}
</el-tag>
</div>
</template>
<template v-if="price.output_text_tokens">
<template v-if="hasSpecificPrice(price.output_text_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">输出文本倍率</span>
<span class="ext-price-label">输出文本价格</span>
<span class="ext-price-value">{{ price.output_text_tokens }}</span>
<el-tag v-if="price.temp_output_text_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_output_text_tokens }}
</el-tag>
</div>
</template>
<template v-if="price.input_image_tokens">
<template v-if="hasSpecificPrice(price.input_image_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">输入图片倍率</span>
<span class="ext-price-label">输入图片价格</span>
<span class="ext-price-value">{{ price.input_image_tokens }}</span>
<el-tag v-if="price.temp_input_image_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_input_image_tokens }}
</el-tag>
</div>
</template>
<template v-if="price.output_image_tokens">
<template v-if="hasSpecificPrice(price.output_image_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">输出图片倍率</span>
<span class="ext-price-label">输出图片价格</span>
<span class="ext-price-value">{{ price.output_image_tokens }}</span>
<el-tag v-if="price.temp_output_image_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_output_image_tokens }}
</el-tag>
</div>
</template>
<template v-if="hasSpecificPrice(price.cached_tokens)">
<div class="extended-price-item">
<span class="ext-price-label">缓存价格</span>
<span class="ext-price-value">{{ price.cached_tokens }}</span>
<el-tag v-if="price.temp_cached_tokens" type="warning" size="small" effect="light">
待审核: {{ price.temp_cached_tokens }}
</el-tag>
</div>
</template>
</div>
</div>
@ -358,6 +385,84 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000" />
:controls="false" placeholder="请输入价格" />
</template>
</el-table-column>
<el-table-column label="扩展价格" min-width="280">
<template #default="{ row }">
<el-dropdown trigger="click">
<el-button type="primary" plain size="small">
设置扩展价格 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<div class="extended-price-dropdown">
<div class="dropdown-title">扩展价格设置</div>
<div class="dropdown-row">
<span>音频输入价格:</span>
<el-input-number v-model="row.input_audio_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>音频输出价格:</span>
<el-input-number v-model="row.output_audio_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>缓存读取价格:</span>
<el-input-number v-model="row.cached_read_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>缓存写入价格:</span>
<el-input-number v-model="row.cached_write_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>推理价格:</span>
<el-input-number v-model="row.reasoning_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>输入文本价格:</span>
<el-input-number v-model="row.input_text_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>输出文本价格:</span>
<el-input-number v-model="row.output_text_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>输入图片价格:</span>
<el-input-number v-model="row.input_image_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>输出图片价格:</span>
<el-input-number v-model="row.output_image_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
<div class="dropdown-row">
<span>缓存价格:</span>
<el-input-number v-model="row.cached_tokens" :precision="4" :step="0.0001"
:controls="false" :min="0" placeholder="请输入价格" />
</div>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
<div v-if="hasAnyExtendedPrices(row)" class="batch-extended-prices">
<div v-if="row.input_audio_tokens" class="batch-price-tag">音频输入: {{ row.input_audio_tokens }}</div>
<div v-if="row.output_audio_tokens" class="batch-price-tag">音频输出: {{ row.output_audio_tokens }}</div>
<div v-if="row.cached_read_tokens" class="batch-price-tag">缓存读取: {{ row.cached_read_tokens }}</div>
<div v-if="row.cached_write_tokens" class="batch-price-tag">缓存写入: {{ row.cached_write_tokens }}</div>
<div v-if="row.reasoning_tokens" class="batch-price-tag">推理: {{ row.reasoning_tokens }}</div>
<div v-if="row.input_text_tokens" class="batch-price-tag">输入文本: {{ row.input_text_tokens }}</div>
<div v-if="row.output_text_tokens" class="batch-price-tag">输出文本: {{ row.output_text_tokens }}</div>
<div v-if="row.input_image_tokens" class="batch-price-tag">输入图片: {{ row.input_image_tokens }}</div>
<div v-if="row.output_image_tokens" class="batch-price-tag">输出图片: {{ row.output_image_tokens }}</div>
<div v-if="row.cached_tokens" class="batch-price-tag">缓存: {{ row.cached_tokens }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="价格来源" min-width="200" width="200">
<template #default="{ row }">
<el-input v-model="row.price_source" placeholder="请输入价格来源" />
@ -434,49 +539,49 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-divider>扩展价格可选</el-divider>
<div class="extended-price-header">
<h3>扩展价格可选</h3>
<p class="extended-price-tip">这些价格字段仅适用于支持特定功能的模型留空表示不使用</p>
</div>
</el-col>
<el-col :span="12">
<el-form-item label="音频输入倍率">
<el-input-number v-model="form.input_audio_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="缓存读取倍率">
<el-input-number v-model="form.cached_read_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="推理倍率">
<el-input-number v-model="form.reasoning_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="输入文本倍率">
<el-input-number v-model="form.input_text_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="输出文本倍率">
<el-input-number v-model="form.output_text_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="输入图片倍率">
<el-input-number v-model="form.input_image_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="输出图片倍率">
<el-input-number v-model="form.output_image_tokens" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入倍率" />
</el-form-item>
<el-col :span="24">
<div class="extended-price-container">
<div v-if="selectedExtensionPrices.length === 0" class="no-extensions">
暂无扩展价格点击下方按钮添加
</div>
<template v-else>
<div v-for="(type, index) in selectedExtensionPrices" :key="type" class="extension-item">
<div class="extension-header">
<span class="extension-label">{{ getExtensionLabel(type) }}</span>
<el-button type="danger" size="small" circle @click="removeExtension(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-input-number v-model="form[type]" :precision="4" :step="0.0001" style="width: 100%"
:controls="false" placeholder="请输入价格" />
</div>
</template>
<el-dropdown @command="addExtension" trigger="click">
<el-button type="primary" class="add-extension-btn">
添加扩展价格 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<div class="extended-type-dropdown">
<div class="dropdown-title">选择扩展价格类型</div>
<el-dropdown-item
v-for="(label, type) in availableExtensionTypes"
:key="type"
:disabled="isExtensionSelected(type)"
:command="type"
>
{{ label }}
</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-col>
<el-col :span="24">
<el-form-item label="价格来源">
@ -500,7 +605,7 @@ import { ref, computed, onMounted, watch } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { Edit, Delete, Check, Close, Document, Search } from '@element-plus/icons-vue'
import { Edit, Delete, Check, Close, Document, Search, ArrowDown } from '@element-plus/icons-vue'
const props = defineProps({
user: Object
@ -517,7 +622,10 @@ const form = ref({
input_price: null,
output_price: null,
input_audio_tokens: null,
output_audio_tokens: null,
cached_tokens: null,
cached_read_tokens: null,
cached_write_tokens: null,
reasoning_tokens: null,
input_text_tokens: null,
output_text_tokens: null,
@ -638,6 +746,7 @@ const loadPrices = async () => {
const handleEdit = (price) => {
editingPrice.value = price
form.value = { ...price }
setExtensionPricesFromPrice(price)
dialogVisible.value = true
}
@ -682,7 +791,10 @@ const handleAdd = () => {
input_price: null,
output_price: null,
input_audio_tokens: null,
output_audio_tokens: null,
cached_tokens: null,
cached_read_tokens: null,
cached_write_tokens: null,
reasoning_tokens: null,
input_text_tokens: null,
output_text_tokens: null,
@ -691,6 +803,7 @@ const handleAdd = () => {
price_source: '',
created_by: ''
}
resetExtensionPrices()
dialogVisible.value = true
}
@ -711,7 +824,10 @@ const handleQuickEdit = (row) => {
input_price: row.input_price,
output_price: row.output_price,
input_audio_tokens: row.input_audio_tokens,
output_audio_tokens: row.output_audio_tokens,
cached_tokens: row.cached_tokens,
cached_read_tokens: row.cached_read_tokens,
cached_write_tokens: row.cached_write_tokens,
reasoning_tokens: row.reasoning_tokens,
input_text_tokens: row.input_text_tokens,
output_text_tokens: row.output_text_tokens,
@ -720,6 +836,7 @@ const handleQuickEdit = (row) => {
price_source: row.price_source,
created_by: props.user.username
}
setExtensionPricesFromPrice(row)
dialogVisible.value = true
}
@ -793,7 +910,10 @@ const handleSubmitResponse = async (response) => {
input_price: null,
output_price: null,
input_audio_tokens: null,
output_audio_tokens: null,
cached_tokens: null,
cached_read_tokens: null,
cached_write_tokens: null,
reasoning_tokens: null,
input_text_tokens: null,
output_text_tokens: null,
@ -887,7 +1007,10 @@ const createNewRow = () => ({
input_price: null,
output_price: null,
input_audio_tokens: null,
output_audio_tokens: null,
cached_tokens: null,
cached_read_tokens: null,
cached_write_tokens: null,
reasoning_tokens: null,
input_text_tokens: null,
output_text_tokens: null,
@ -1226,7 +1349,9 @@ const handleSearch = () => {
//
const hasExtendedPrices = (row) => {
return row.input_audio_tokens ||
row.output_audio_tokens ||
row.cached_read_tokens ||
row.cached_write_tokens ||
row.reasoning_tokens ||
row.input_text_tokens ||
row.output_text_tokens ||
@ -1234,6 +1359,90 @@ const hasExtendedPrices = (row) => {
row.output_image_tokens
}
//
const hasAnyExtendedPrices = (row) => {
return hasSpecificPrice(row.input_audio_tokens) ||
hasSpecificPrice(row.output_audio_tokens) ||
hasSpecificPrice(row.cached_tokens) ||
hasSpecificPrice(row.cached_read_tokens) ||
hasSpecificPrice(row.cached_write_tokens) ||
hasSpecificPrice(row.reasoning_tokens) ||
hasSpecificPrice(row.input_text_tokens) ||
hasSpecificPrice(row.output_text_tokens) ||
hasSpecificPrice(row.input_image_tokens) ||
hasSpecificPrice(row.output_image_tokens)
}
//
const hasSpecificPrice = (price) => {
return price !== null && price !== undefined && price !== ''
}
//
const extensionTypes = {
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: '输出图片价格'
}
//
const selectedExtensionPrices = ref([])
//
const availableExtensionTypes = computed(() => {
return extensionTypes
})
//
const getExtensionLabel = (type) => {
return extensionTypes[type] || type
}
//
const isExtensionSelected = (type) => {
return selectedExtensionPrices.value.includes(type)
}
//
const addExtension = (type) => {
if (!isExtensionSelected(type)) {
selectedExtensionPrices.value.push(type)
form.value[type] = null
}
}
//
const removeExtension = (index) => {
const type = selectedExtensionPrices.value[index]
form.value[type] = null
selectedExtensionPrices.value.splice(index, 1)
}
//
const resetExtensionPrices = () => {
selectedExtensionPrices.value = []
Object.keys(extensionTypes).forEach(type => {
form.value[type] = null
})
}
//
const setExtensionPricesFromPrice = (price) => {
selectedExtensionPrices.value = []
Object.keys(extensionTypes).forEach(type => {
if (price[type] !== undefined && price[type] !== null) {
selectedExtensionPrices.value.push(type)
}
})
}
onMounted(() => {
loadModelTypes()
loadPrices()
@ -1527,8 +1736,8 @@ onMounted(() => {
.price-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1.2rem;
padding: 1rem 0;
}
@ -1536,16 +1745,38 @@ onMounted(() => {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
gap: 1rem;
transition: all 0.2s ease;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
height: auto;
min-height: 280px;
}
.price-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.price-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(to right, #36d1dc, #5b86e5);
}
.price-card.pending::before {
background: linear-gradient(to right, #f7b733, #fc4a1a);
}
.price-card.rejected::before {
background: linear-gradient(to right, #ED213A, #93291E);
}
.price-card-header {
@ -1557,25 +1788,32 @@ onMounted(() => {
.provider-info {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.8rem;
}
.provider-icon {
width: 24px;
height: 24px;
border-radius: 4px;
width: 28px;
height: 28px;
border-radius: 6px;
object-fit: contain;
background-color: #f5f7fa;
padding: 2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.provider-name {
font-weight: 500;
font-weight: 600;
color: #333;
font-size: 1rem;
}
.model-status {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.model-status.pending {
@ -1594,46 +1832,52 @@ onMounted(() => {
}
.model-info {
margin-top: 0.5rem;
margin-top: 0.75rem;
}
.model-name {
font-size: 1.25rem;
font-size: 1.35rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.5rem 0;
margin: 0 0 0.75rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
line-height: 1.3;
}
.model-meta {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.price-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
gap: 0.7rem;
margin-top: 0.75rem;
padding: 0.75rem;
background-color: #f9fafc;
border-radius: 8px;
}
.price-row {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.75rem;
}
.price-label {
color: #666;
color: #606266;
min-width: 100px;
font-weight: 500;
}
.price-value {
font-weight: 500;
color: #333;
font-weight: 600;
color: #303133;
}
.extended-prices {
@ -1643,14 +1887,15 @@ onMounted(() => {
}
.section-title {
font-weight: 500;
color: #666;
font-weight: 600;
color: #606266;
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.extended-price-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.75rem;
}
@ -1658,19 +1903,28 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem;
padding: 0.75rem;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #f0f0f0;
transition: all 0.2s ease;
}
.extended-price-item:hover {
background: #f0f2f5;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.ext-price-label {
font-size: 0.875rem;
color: #666;
font-size: 0.8rem;
color: #606266;
font-weight: 500;
}
.ext-price-value {
font-weight: 500;
color: #333;
font-weight: 600;
color: #303133;
font-size: 0.95rem;
}
.price-card-footer {
@ -1686,8 +1940,8 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
color: #666;
font-size: 0.8rem;
color: #909399;
}
.created-by {
@ -1695,7 +1949,7 @@ onMounted(() => {
}
.created-at {
color: #999;
color: #c0c4cc;
}
.action-buttons {
@ -1709,6 +1963,7 @@ onMounted(() => {
:deep(.el-tag) {
margin: 0;
font-size: 0.7rem;
}
:deep(.el-button) {
@ -1718,6 +1973,127 @@ onMounted(() => {
:deep(.el-icon) {
font-size: 16px;
}
.extended-price-header {
margin: 20px 0 10px 0;
border-top: 1px solid #EBEEF5;
padding-top: 20px;
}
.extended-price-header h3 {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 10px 0;
}
.extended-price-tip {
font-size: 13px;
color: #909399;
margin: 0;
}
.extended-price-dropdown {
padding: 15px;
min-width: 300px;
}
.dropdown-title {
font-weight: 600;
color: #303133;
margin-bottom: 15px;
font-size: 15px;
border-bottom: 1px solid #EBEEF5;
padding-bottom: 10px;
}
.dropdown-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.dropdown-row span {
flex: 0 0 110px;
font-size: 14px;
color: #606266;
}
.dropdown-row .el-input-number {
flex: 1;
}
.batch-extended-prices {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.batch-price-tag {
background-color: #ecf5ff;
color: #409EFF;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
.extended-price-container {
border: 1px solid #EBEEF5;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
background-color: #f9fafc;
}
.no-extensions {
color: #909399;
text-align: center;
margin-bottom: 20px;
padding: 20px 0;
font-size: 14px;
}
.extension-item {
border: 1px solid #EBEEF5;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
background-color: #fff;
}
.extension-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.extension-label {
font-weight: 500;
color: #303133;
font-size: 15px;
}
.add-extension-btn {
width: 100%;
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.extended-type-dropdown {
min-width: 220px;
padding: 15px;
}
:deep(.el-dropdown-menu__item.is-disabled) {
color: #C0C4CC;
cursor: not-allowed;
}
</style>
<style>