Kaynağa Gözat

refactor(channel-pricing): 提取辅助方法消除重复代码,修复缓存一致性

- 提取 PriceData.ApplyChannelPricingRatios 消除 ModelPriceHelper 和
  UpdatePriceDataForChannelPricing 中重复的 ~30 行比率回退逻辑
- 提取 ChannelPricing.ApplyFields 消除 controller 中 4 处相同的字段赋值
- 提取 setCache/removeCache 辅助函数统一写穿透缓存操作
- 将 claudeCacheCreation1hMultiplier 常量移至 types 包避免循环依赖
- 修复 BatchUpsertChannelPricing 成功后未刷新内存缓存的 bug

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
master
fengsilin 2 hafta önce
ebeveyn
işleme
00058cd671
4 değiştirilmiş dosya ile 123 ekleme ve 168 silme
  1. +27
    -79
      controller/channel_pricing.go
  2. +37
    -13
      model/channel_pricing.go
  3. +33
    -76
      relay/helper/price.go
  4. +26
    -0
      types/price_data.go

+ 27
- 79
controller/channel_pricing.go Dosyayı Görüntüle

@@ -66,6 +66,12 @@ type CreateChannelPricingRequest struct {
AudioCompletionRatio float64 `json:"audio_completion_ratio"`
}

// applyRequest 将请求字段应用到 ChannelPricing
func applyRequestFields(cp *model.ChannelPricing, req *CreateChannelPricingRequest) {
cp.ApplyFields(req.QuotaType, req.ModelRatio, req.CompletionRatio, req.ModelPrice, req.TagIds,
req.CacheRatio, req.CacheCreationRatio, req.ImageRatio, req.AudioRatio, req.AudioCompletionRatio)
}

// CreateChannelPricing 创建或更新渠道定价
func CreateChannelPricing(c *gin.Context) {
var req CreateChannelPricingRequest
@@ -82,17 +88,7 @@ func CreateChannelPricing(c *gin.Context) {
// 检查是否已存在
existing, _ := model.GetChannelPricing(req.ModelName, req.ChannelId)
if existing != nil {
// 更新
existing.QuotaType = req.QuotaType
existing.ModelRatio = req.ModelRatio
existing.CompletionRatio = req.CompletionRatio
existing.ModelPrice = req.ModelPrice
existing.TagIds = req.TagIds
existing.CacheRatio = req.CacheRatio
existing.CacheCreationRatio = req.CacheCreationRatio
existing.ImageRatio = req.ImageRatio
existing.AudioRatio = req.AudioRatio
existing.AudioCompletionRatio = req.AudioCompletionRatio
applyRequestFields(existing, &req)
if err := existing.Update(); err != nil {
common.ApiError(c, err)
return
@@ -104,19 +100,10 @@ func CreateChannelPricing(c *gin.Context) {

// 创建
cp := &model.ChannelPricing{
ModelName: req.ModelName,
ChannelId: req.ChannelId,
QuotaType: req.QuotaType,
ModelRatio: req.ModelRatio,
CompletionRatio: req.CompletionRatio,
ModelPrice: req.ModelPrice,
TagIds: req.TagIds,
CacheRatio: req.CacheRatio,
CacheCreationRatio: req.CacheCreationRatio,
ImageRatio: req.ImageRatio,
AudioRatio: req.AudioRatio,
AudioCompletionRatio: req.AudioCompletionRatio,
ModelName: req.ModelName,
ChannelId: req.ChannelId,
}
applyRequestFields(cp, &req)
if err := cp.Insert(); err != nil {
common.ApiError(c, err)
return
@@ -143,36 +130,20 @@ func BatchCreateChannelPricing(c *gin.Context) {

pricings := make([]*model.ChannelPricing, 0, len(req.Items))
for _, item := range req.Items {
pricings = append(pricings, &model.ChannelPricing{
ModelName: item.ModelName,
ChannelId: item.ChannelId,
QuotaType: item.QuotaType,
ModelRatio: item.ModelRatio,
CompletionRatio: item.CompletionRatio,
ModelPrice: item.ModelPrice,
TagIds: item.TagIds,
CacheRatio: item.CacheRatio,
CacheCreationRatio: item.CacheCreationRatio,
ImageRatio: item.ImageRatio,
AudioRatio: item.AudioRatio,
AudioCompletionRatio: item.AudioCompletionRatio,
})
cp := &model.ChannelPricing{
ModelName: item.ModelName,
ChannelId: item.ChannelId,
}
applyRequestFields(cp, item)
pricings = append(pricings, cp)
}

// 使用事务逐个处理(GORM 的批量 upsert 在不同数据库表现不一致)
for _, cp := range pricings {
existing, _ := model.GetChannelPricing(cp.ModelName, cp.ChannelId)
if existing != nil {
existing.QuotaType = cp.QuotaType
existing.ModelRatio = cp.ModelRatio
existing.CompletionRatio = cp.CompletionRatio
existing.ModelPrice = cp.ModelPrice
existing.TagIds = cp.TagIds
existing.CacheRatio = cp.CacheRatio
existing.CacheCreationRatio = cp.CacheCreationRatio
existing.ImageRatio = cp.ImageRatio
existing.AudioRatio = cp.AudioRatio
existing.AudioCompletionRatio = cp.AudioCompletionRatio
existing.ApplyFields(cp.QuotaType, cp.ModelRatio, cp.CompletionRatio, cp.ModelPrice, cp.TagIds,
cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio)
if err := existing.Update(); err != nil {
common.ApiError(c, err)
return
@@ -241,18 +212,9 @@ func CopyGlobalPricing(c *gin.Context) {
}

// 获取全局扩展比率
globalCacheRatio, hasCacheRatio := ratio_setting.GetCacheRatio(ability.Model)
if !hasCacheRatio {
globalCacheRatio = 0
}
globalCacheCreationRatio, hasCacheCreationRatio := ratio_setting.GetCreateCacheRatio(ability.Model)
if !hasCacheCreationRatio {
globalCacheCreationRatio = 0
}
globalImageRatio, hasImageRatio := ratio_setting.GetImageRatio(ability.Model)
if !hasImageRatio {
globalImageRatio = 0
}
globalCacheRatio, _ := ratio_setting.GetCacheRatio(ability.Model)
globalCacheCreationRatio, _ := ratio_setting.GetCreateCacheRatio(ability.Model)
globalImageRatio, _ := ratio_setting.GetImageRatio(ability.Model)
globalAudioRatio := ratio_setting.GetAudioRatio(ability.Model)
globalAudioCompletionRatio := ratio_setting.GetAudioCompletionRatio(ability.Model)

@@ -276,32 +238,18 @@ func CopyGlobalPricing(c *gin.Context) {
}

if existing != nil {
existing.QuotaType = quotaType
existing.ModelRatio = ratio
existing.CompletionRatio = completionRatio
existing.ModelPrice = price
existing.CacheRatio = globalCacheRatio
existing.CacheCreationRatio = globalCacheCreationRatio
existing.ImageRatio = globalImageRatio
existing.AudioRatio = globalAudioRatio
existing.AudioCompletionRatio = globalAudioCompletionRatio
existing.ApplyFields(quotaType, ratio, completionRatio, price, "",
globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio)
if err := existing.Update(); err == nil {
imported++
}
} else {
cp := &model.ChannelPricing{
ModelName: ability.Model,
ChannelId: channelId,
QuotaType: quotaType,
ModelRatio: ratio,
CompletionRatio: completionRatio,
ModelPrice: price,
CacheRatio: globalCacheRatio,
CacheCreationRatio: globalCacheCreationRatio,
ImageRatio: globalImageRatio,
AudioRatio: globalAudioRatio,
AudioCompletionRatio: globalAudioCompletionRatio,
ModelName: ability.Model,
ChannelId: channelId,
}
cp.ApplyFields(quotaType, ratio, completionRatio, price, "",
globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio)
if err := cp.Insert(); err == nil {
imported++
}


+ 37
- 13
model/channel_pricing.go Dosyayı Görüntüle

@@ -46,16 +46,40 @@ type ChannelPricing struct {
AudioCompletionRatio float64 `json:"audio_completion_ratio" gorm:"default:0"`
}

// setCache 写穿透缓存
func setCache(key string, cp *ChannelPricing) {
channelPricingCacheLock.Lock()
channelPricingCache[key] = cp
channelPricingCacheLock.Unlock()
}

func removeCache(key string) {
channelPricingCacheLock.Lock()
delete(channelPricingCache, key)
channelPricingCacheLock.Unlock()
}

// ApplyFields 批量设置定价字段(消除 controller 层的重复赋值)
func (cp *ChannelPricing) ApplyFields(quotaType int, modelRatio, completionRatio, modelPrice float64, tagIds string, cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio float64) {
cp.QuotaType = quotaType
cp.ModelRatio = modelRatio
cp.CompletionRatio = completionRatio
cp.ModelPrice = modelPrice
cp.TagIds = tagIds
cp.CacheRatio = cacheRatio
cp.CacheCreationRatio = cacheCreationRatio
cp.ImageRatio = imageRatio
cp.AudioRatio = audioRatio
cp.AudioCompletionRatio = audioCompletionRatio
}

func (cp *ChannelPricing) Insert() error {
now := common.GetTimestamp()
cp.CreatedTime = now
cp.UpdatedTime = now
err := DB.Create(cp).Error
if err == nil {
key := getChannelPricingCacheKey(cp.ModelName, cp.ChannelId)
channelPricingCacheLock.Lock()
channelPricingCache[key] = cp
channelPricingCacheLock.Unlock()
setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp)
}
return err
}
@@ -68,10 +92,7 @@ func (cp *ChannelPricing) Update() error {
"audio_ratio", "audio_completion_ratio", "updated_time").
Updates(cp).Error
if err == nil {
key := getChannelPricingCacheKey(cp.ModelName, cp.ChannelId)
channelPricingCacheLock.Lock()
channelPricingCache[key] = cp
channelPricingCacheLock.Unlock()
setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp)
}
return err
}
@@ -83,10 +104,7 @@ func (cp *ChannelPricing) Delete() error {
}
err := DB.Delete(cp).Error
if err == nil {
key := getChannelPricingCacheKey(existing.ModelName, existing.ChannelId)
channelPricingCacheLock.Lock()
delete(channelPricingCache, key)
channelPricingCacheLock.Unlock()
removeCache(getChannelPricingCacheKey(existing.ModelName, existing.ChannelId))
}
return err
}
@@ -134,7 +152,7 @@ func BatchUpsertChannelPricing(pricings []*ChannelPricing) error {
}
// 使用 GORM 的 OnConflict 实现 upsert
// 唯一索引为 idx_model_channel (model_name, channel_id)
return DB.Clauses(clause.OnConflict{
err := DB.Clauses(clause.OnConflict{
Columns: []clause.Column{
{Name: "model_name"},
{Name: "channel_id"},
@@ -153,6 +171,12 @@ func BatchUpsertChannelPricing(pricings []*ChannelPricing) error {
"updated_time",
}),
}).Create(&pricings).Error
if err == nil {
for _, cp := range pricings {
setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp)
}
}
return err
}

// getChannelPricingCacheKey 生成缓存键


+ 33
- 76
relay/helper/price.go Dosyayı Görüntüle

@@ -14,9 +14,6 @@ import (
"github.com/gin-gonic/gin"
)

// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration
const claudeCacheCreation1hMultiplier = 6 / 3.75

// HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present
func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo {
groupRatioInfo := types.GroupRatioInfo{
@@ -52,13 +49,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var modelRatio float64
var completionRatio float64
var channelPricingFound bool
var cacheRatio float64
var imageRatio float64
var cacheCreationRatio float64
var cacheCreationRatio5m float64
var cacheCreationRatio1h float64
var audioRatio float64
var audioCompletionRatio float64

// 尝试获取渠道定价(优先于全局定价)
channelMetaAvailable := info != nil && info.ChannelMeta != nil && info.ChannelId > 0
@@ -70,23 +60,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
modelPrice = cp.ModelPrice
usePrice = cp.QuotaType == model.QuotaTypeByCall
channelPricingFound = true
if cp.CacheRatio != 0 {
cacheRatio = cp.CacheRatio
}
if cp.CacheCreationRatio != 0 {
cacheCreationRatio = cp.CacheCreationRatio
cacheCreationRatio5m = cp.CacheCreationRatio
cacheCreationRatio1h = cp.CacheCreationRatio * claudeCacheCreation1hMultiplier
}
if cp.ImageRatio != 0 {
imageRatio = cp.ImageRatio
}
if cp.AudioRatio != 0 {
audioRatio = cp.AudioRatio
}
if cp.AudioCompletionRatio != 0 {
audioCompletionRatio = cp.AudioCompletionRatio
}
if common.DebugEnabled {
println(fmt.Sprintf("[ChannelPricing] hit: model=%s channel=%d source=cache", info.OriginModelName, info.ChannelId))
}
@@ -123,24 +96,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
}
completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName)
}
if cacheRatio == 0 {
cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
}
if cacheCreationRatio == 0 {
cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
cacheCreationRatio5m = cacheCreationRatio
// 固定1h和5min缓存写入价格的比例
cacheCreationRatio1h = cacheCreationRatio * claudeCacheCreation1hMultiplier
}
if imageRatio == 0 {
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
}
if audioRatio == 0 {
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
}
if audioCompletionRatio == 0 {
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
}
ratio := modelRatio * groupRatioInfo.GroupRatio
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
} else {
@@ -170,20 +125,38 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
}

priceData := types.PriceData{
FreeModel: freeModel,
ModelPrice: modelPrice,
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
GroupRatioInfo: groupRatioInfo,
UsePrice: usePrice,
CacheRatio: cacheRatio,
ImageRatio: imageRatio,
AudioRatio: audioRatio,
AudioCompletionRatio: audioCompletionRatio,
CacheCreationRatio: cacheCreationRatio,
CacheCreation5mRatio: cacheCreationRatio5m,
CacheCreation1hRatio: cacheCreationRatio1h,
QuotaToPreConsume: preConsumedQuota,
FreeModel: freeModel,
ModelPrice: modelPrice,
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
GroupRatioInfo: groupRatioInfo,
UsePrice: usePrice,
QuotaToPreConsume: preConsumedQuota,
}

// 应用渠道定价的扩展比率(非零值覆盖),然后回退全局比率
if channelMetaAvailable {
if cp, found := model.GetEffectivePricing(info.OriginModelName, info.ChannelId); found {
priceData.ApplyChannelPricingRatios(cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio)
}
}
// 全局比率作为回退(仅当对应字段仍为 0 时生效)
if priceData.CacheRatio == 0 {
priceData.CacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
}
if priceData.CacheCreationRatio == 0 {
priceData.CacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
priceData.CacheCreation5mRatio = priceData.CacheCreationRatio
priceData.CacheCreation1hRatio = priceData.CacheCreationRatio * types.ClaudeCacheCreation1hMultiplier
}
if priceData.ImageRatio == 0 {
priceData.ImageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
}
if priceData.AudioRatio == 0 {
priceData.AudioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
}
if priceData.AudioCompletionRatio == 0 {
priceData.AudioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
}

if common.DebugEnabled {
@@ -261,23 +234,7 @@ func UpdatePriceDataForChannelPricing(c *gin.Context, info *relaycommon.RelayInf
info.PriceData.ModelPrice = -1
}

if cp.CacheRatio != 0 {
info.PriceData.CacheRatio = cp.CacheRatio
}
if cp.CacheCreationRatio != 0 {
info.PriceData.CacheCreationRatio = cp.CacheCreationRatio
info.PriceData.CacheCreation5mRatio = cp.CacheCreationRatio
info.PriceData.CacheCreation1hRatio = cp.CacheCreationRatio * claudeCacheCreation1hMultiplier
}
if cp.ImageRatio != 0 {
info.PriceData.ImageRatio = cp.ImageRatio
}
if cp.AudioRatio != 0 {
info.PriceData.AudioRatio = cp.AudioRatio
}
if cp.AudioCompletionRatio != 0 {
info.PriceData.AudioCompletionRatio = cp.AudioCompletionRatio
}
info.PriceData.ApplyChannelPricingRatios(cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio)

if info.PriceData.UsePrice {
info.PriceData.QuotaToPreConsume = int(


+ 26
- 0
types/price_data.go Dosyayı Görüntüle

@@ -2,6 +2,10 @@ package types

import "fmt"

// ClaudeCacheCreation1hMultiplier 1小时缓存写入价格相对于5分钟的比例
// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration
const ClaudeCacheCreation1hMultiplier = 6 / 3.75

type GroupRatioInfo struct {
GroupRatio float64
GroupSpecialRatio float64
@@ -27,6 +31,28 @@ type PriceData struct {
GroupRatioInfo GroupRatioInfo
}

// ApplyChannelPricingRatios 将渠道定价的扩展比率应用到 PriceData(非零值覆盖)
// cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio
func (p *PriceData) ApplyChannelPricingRatios(cacheRatio, cacheCreationRatio, imageRatio, audioRatio, audioCompletionRatio float64) {
if cacheRatio != 0 {
p.CacheRatio = cacheRatio
}
if cacheCreationRatio != 0 {
p.CacheCreationRatio = cacheCreationRatio
p.CacheCreation5mRatio = cacheCreationRatio
p.CacheCreation1hRatio = cacheCreationRatio * ClaudeCacheCreation1hMultiplier
}
if imageRatio != 0 {
p.ImageRatio = imageRatio
}
if audioRatio != 0 {
p.AudioRatio = audioRatio
}
if audioCompletionRatio != 0 {
p.AudioCompletionRatio = audioCompletionRatio
}
}

func (p *PriceData) AddOtherRatio(key string, ratio float64) {
if p.OtherRatios == nil {
p.OtherRatios = make(map[string]float64)


Yükleniyor…
İptal
Kaydet