From 817f51b75a4e40b0c4e3536657c0bd9ed697bf52 Mon Sep 17 00:00:00 2001 From: wood chen Date: Thu, 1 May 2025 00:53:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=B7=E6=A0=BC=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=A4=9A=E4=B8=AA=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E4=BB=B7=E6=A0=BC=E5=80=8D=E7=8E=87=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=8D=E6=9D=82=E7=9A=84?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E5=B0=86=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E4=BB=8E=E8=A1=A8=E6=A0=BC=E5=B8=83=E5=B1=80=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=B8=83=E5=B1=80=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E5=92=8C=E5=8F=AF=E8=AF=BB?= =?UTF-8?q?=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/handlers/price.go | 167 +++++++++ backend/models/price.go | 60 ++-- frontend/src/views/Prices.vue | 631 +++++++++++++++++++++++++--------- 3 files changed, 674 insertions(+), 184 deletions(-) create mode 100644 backend/handlers/price.go diff --git a/backend/handlers/price.go b/backend/handlers/price.go new file mode 100644 index 0000000..08a1748 --- /dev/null +++ b/backend/handlers/price.go @@ -0,0 +1,167 @@ +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/models/price.go b/backend/models/price.go index 930bb5c..5b05efa 100644 --- a/backend/models/price.go +++ b/backend/models/price.go @@ -7,30 +7,44 @@ import ( ) type Price struct { - ID uint `json:"id" gorm:"primaryKey"` - Model string `json:"model" gorm:"not null;index:idx_model_channel"` - ModelType string `json:"model_type" gorm:"not null;index:idx_model_type"` // text2text, text2image, etc. - BillingType string `json:"billing_type" gorm:"not null"` // tokens or times - ChannelType uint `json:"channel_type" gorm:"not null;index:idx_model_channel"` - 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"` - 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"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` - CreatedBy string `json:"created_by" gorm:"not null"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + ID uint `json:"id" gorm:"primaryKey"` + Model string `json:"model" gorm:"not null;index:idx_model_channel"` + ModelType string `json:"model_type" gorm:"not null;index:idx_model_type"` // text2text, text2image, etc. + BillingType string `json:"billing_type" gorm:"not null"` // tokens or times + ChannelType uint `json:"channel_type" gorm:"not null;index:idx_model_channel"` + 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"` // 输出图片倍率 + 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"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + CreatedBy string `json:"created_by" gorm:"not null"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // 临时字段,用于存储待审核的更新 - TempModel *string `json:"temp_model,omitempty" gorm:"column:temp_model"` - TempModelType *string `json:"temp_model_type,omitempty" gorm:"column:temp_model_type"` - TempBillingType *string `json:"temp_billing_type,omitempty" gorm:"column:temp_billing_type"` - TempChannelType *uint `json:"temp_channel_type,omitempty" gorm:"column:temp_channel_type"` - TempCurrency *string `json:"temp_currency,omitempty" gorm:"column:temp_currency"` - TempInputPrice *float64 `json:"temp_input_price,omitempty" gorm:"column:temp_input_price"` - TempOutputPrice *float64 `json:"temp_output_price,omitempty" gorm:"column:temp_output_price"` - TempPriceSource *string `json:"temp_price_source,omitempty" gorm:"column:temp_price_source"` - UpdatedBy *string `json:"updated_by,omitempty" gorm:"column:updated_by"` + TempModel *string `json:"temp_model,omitempty" gorm:"column:temp_model"` + TempModelType *string `json:"temp_model_type,omitempty" gorm:"column:temp_model_type"` + TempBillingType *string `json:"temp_billing_type,omitempty" gorm:"column:temp_billing_type"` + TempChannelType *uint `json:"temp_channel_type,omitempty" gorm:"column:temp_channel_type"` + TempCurrency *string `json:"temp_currency,omitempty" gorm:"column:temp_currency"` + 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"` + TempCachedReadTokens *float64 `json:"temp_cached_read_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"` + TempInputImageTokens *float64 `json:"temp_input_image_tokens,omitempty"` + TempOutputImageTokens *float64 `json:"temp_output_image_tokens,omitempty"` + TempPriceSource *string `json:"temp_price_source,omitempty" gorm:"column:temp_price_source"` + UpdatedBy *string `json:"updated_by,omitempty" gorm:"column:updated_by"` } // TableName 指定表名 diff --git a/frontend/src/views/Prices.vue b/frontend/src/views/Prices.vue index 7d377e0..c647bcb 100644 --- a/frontend/src/views/Prices.vue +++ b/frontend/src/views/Prices.vue @@ -76,175 +76,174 @@ - - - - - - - - - - - - - - - - +
@@ -434,6 +433,51 @@ dall-e-3 按Token收费 OpenAI 美元 40.000000 40.000000" /> :controls="false" placeholder="请输入价格" /> + + 扩展价格(可选) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -472,6 +516,13 @@ const form = ref({ currency: 'USD', input_price: null, output_price: null, + input_audio_tokens: null, + cached_read_tokens: null, + reasoning_tokens: null, + input_text_tokens: null, + output_text_tokens: null, + input_image_tokens: null, + output_image_tokens: null, price_source: '', created_by: '' }) @@ -630,6 +681,13 @@ const handleAdd = () => { currency: 'USD', input_price: null, output_price: null, + input_audio_tokens: null, + cached_read_tokens: null, + reasoning_tokens: null, + input_text_tokens: null, + output_text_tokens: null, + input_image_tokens: null, + output_image_tokens: null, price_source: '', created_by: '' } @@ -652,6 +710,13 @@ const handleQuickEdit = (row) => { currency: row.currency, input_price: row.input_price, output_price: row.output_price, + input_audio_tokens: row.input_audio_tokens, + cached_read_tokens: row.cached_read_tokens, + reasoning_tokens: row.reasoning_tokens, + input_text_tokens: row.input_text_tokens, + output_text_tokens: row.output_text_tokens, + input_image_tokens: row.input_image_tokens, + output_image_tokens: row.output_image_tokens, price_source: row.price_source, created_by: props.user.username } @@ -727,6 +792,13 @@ const handleSubmitResponse = async (response) => { currency: 'USD', input_price: null, output_price: null, + input_audio_tokens: null, + cached_read_tokens: null, + reasoning_tokens: null, + input_text_tokens: null, + output_text_tokens: null, + input_image_tokens: null, + output_image_tokens: null, price_source: '', created_by: '' } @@ -814,6 +886,13 @@ const createNewRow = () => ({ currency: 'USD', input_price: null, output_price: null, + input_audio_tokens: null, + cached_read_tokens: null, + reasoning_tokens: null, + input_text_tokens: null, + output_text_tokens: null, + input_image_tokens: null, + output_image_tokens: null, price_source: '', created_by: props.user?.username || '' }) @@ -1144,6 +1223,17 @@ const handleSearch = () => { loadPrices() } +// 添加检查是否有扩展价格的方法 +const hasExtendedPrices = (row) => { + return row.input_audio_tokens || + row.cached_read_tokens || + row.reasoning_tokens || + row.input_text_tokens || + row.output_text_tokens || + row.input_image_tokens || + row.output_image_tokens +} + onMounted(() => { loadModelTypes() loadPrices() @@ -1409,6 +1499,225 @@ onMounted(() => { width: auto !important; min-width: 140px !important; } + +.extended-prices { + font-size: 12px; +} + +.price-item { + margin-bottom: 4px; + display: flex; + align-items: center; + gap: 8px; +} + +.price-label { + color: #666; + min-width: 100px; +} + +.price-value { + font-weight: 500; + color: #333; +} + +.el-tag { + margin-left: 4px; +} + +.price-cards-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + padding: 1rem 0; +} + +.price-card { + background: #fff; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + display: flex; + flex-direction: column; + gap: 1rem; + transition: all 0.2s ease; +} + +.price-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); +} + +.price-card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.provider-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.provider-icon { + width: 24px; + height: 24px; + border-radius: 4px; +} + +.provider-name { + font-weight: 500; + color: #333; +} + +.model-status { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.875rem; + font-weight: 500; +} + +.model-status.pending { + background: #fff7e6; + color: #d46b08; +} + +.model-status.approved { + background: #f6ffed; + color: #389e0d; +} + +.model-status.rejected { + background: #fff1f0; + color: #cf1322; +} + +.model-info { + margin-top: 0.5rem; +} + +.model-name { + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + margin: 0 0 0.5rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.model-meta { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.price-info { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.price-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.price-label { + color: #666; + min-width: 100px; +} + +.price-value { + font-weight: 500; + color: #333; +} + +.extended-prices { + border-top: 1px solid #f0f0f0; + padding-top: 1rem; + margin-top: 0.5rem; +} + +.section-title { + font-weight: 500; + color: #666; + margin-bottom: 0.75rem; +} + +.extended-price-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.extended-price-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.5rem; + background: #f9fafb; + border-radius: 8px; +} + +.ext-price-label { + font-size: 0.875rem; + color: #666; +} + +.ext-price-value { + font-weight: 500; + color: #333; +} + +.price-card-footer { + margin-top: auto; + padding-top: 1rem; + border-top: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.meta-info { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.875rem; + color: #666; +} + +.created-by { + font-weight: 500; +} + +.created-at { + color: #999; +} + +.action-buttons { + display: flex; + gap: 0.5rem; +} + +.skeleton { + min-height: 300px; +} + +:deep(.el-tag) { + margin: 0; +} + +:deep(.el-button) { + padding: 4px; +} + +:deep(.el-icon) { + font-size: 16px; +}