Просмотр исходного кода

refactor: 完整删除渠道选择功能后端代码

删除了用户通过 sk-{key}:{channelId} 指定渠道、Playground 下拉框选择渠道、
以及管理员设置默认渠道 (is_default) 的所有后端代码。

涉及修改:
- middleware/auth.go: 简化 parseTokenKey,移除 specific_channel_id 设置
- middleware/distributor.go: 移除指定渠道分发逻辑、默认通道检查、ModelRequest.ChannelId
- constant/context_key.go: 移除 ContextKeyTokenSpecificChannelId
- controller/playground_channels.go: 整个文件删除
- controller/relay.go: 移除 shouldRetry/shouldRetryTaskRelay 中的指定渠道跳过
- controller/channel.go: 移除 GetUserChannelsForBinding
- controller/channel_pricing.go: 移除 SetDefaultChannel/ClearDefaultChannel
- model/ability.go: 移除 GetModelChannelsForGroup
- model/channel.go: 移除 GetAllChannelsForBinding
- model/channel_pricing.go: 移除默认通道缓存及所有相关函数
- model/pricing.go: 移除 DefaultChannelName 字段
- router/api-router.go: 移除 4 条路由
- i18n: 移除 channel.id_format_error 和 distributor.invalid_channel_id
master
fengsilin 4 недель назад
Родитель
Сommit
baed9754d0
17 измененных файлов: 10 добавлений и 559 удалений
  1. +0
    -1
      constant/context_key.go
  2. +0
    -27
      controller/channel.go
  3. +0
    -47
      controller/channel_pricing.go
  4. +0
    -62
      controller/playground_channels.go
  5. +0
    -6
      controller/relay.go
  6. +0
    -2
      i18n/keys.go
  7. +0
    -2
      i18n/locales/en.yaml
  8. +0
    -2
      i18n/locales/zh-CN.yaml
  9. +0
    -2
      i18n/locales/zh-TW.yaml
  10. +2
    -23
      middleware/auth.go
  11. +3
    -69
      middleware/distributor.go
  12. +0
    -52
      model/ability.go
  13. +0
    -10
      model/channel.go
  14. +3
    -120
      model/channel_pricing.go
  15. +0
    -121
      model/channel_pricing_test.go
  16. +0
    -7
      model/pricing.go
  17. +2
    -6
      router/api-router.go

+ 0
- 1
constant/context_key.go Просмотреть файл

@@ -15,7 +15,6 @@ const (
ContextKeyTokenKey ContextKey = "token_key" ContextKeyTokenKey ContextKey = "token_key"
ContextKeyTokenId ContextKey = "token_id" ContextKeyTokenId ContextKey = "token_id"
ContextKeyTokenGroup ContextKey = "token_group" ContextKeyTokenGroup ContextKey = "token_group"
ContextKeyTokenSpecificChannelId ContextKey = "specific_channel_id"
ContextKeyTokenModelLimitEnabled ContextKey = "token_model_limit_enabled" ContextKeyTokenModelLimitEnabled ContextKey = "token_model_limit_enabled"
ContextKeyTokenModelLimit ContextKey = "token_model_limit" ContextKeyTokenModelLimit ContextKey = "token_model_limit"
ContextKeyTokenCrossGroupRetry ContextKey = "token_cross_group_retry" ContextKeyTokenCrossGroupRetry ContextKey = "token_cross_group_retry"


+ 0
- 27
controller/channel.go Просмотреть файл

@@ -2106,30 +2106,3 @@ func OllamaVersion(c *gin.Context) {
}) })
} }


// GetUserChannelsForBinding 获取用户可用于绑定的渠道列表
// 只返回 id、name、type 和 remark,不包含敏感信息(如key)
func GetUserChannelsForBinding(c *gin.Context) {
channels, err := model.GetAllChannelsForBinding()
if err != nil {
common.ApiError(c, err)
return
}

// 构建返回结果
result := make([]gin.H, 0, len(channels))
for _, ch := range channels {
result = append(result, gin.H{
"id": ch.Id,
"name": ch.Name,
"public_name": ch.PublicName,
"type": ch.Type,
"remark": ch.Remark,
})
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": result,
})
}

+ 0
- 47
controller/channel_pricing.go Просмотреть файл

@@ -357,50 +357,3 @@ func GetChannelPricingWithTags(c *gin.Context) {
}) })
} }


// SetDefaultChannelRequest 设置默认通道请求
type SetDefaultChannelRequest struct {
ModelName string `json:"model_name" binding:"required"`
ChannelId int `json:"channel_id" binding:"required"`
}

// SetDefaultChannel 设置指定模型的默认通道
func SetDefaultChannel(c *gin.Context) {
var req SetDefaultChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}

// 验证渠道定价记录存在
existing, err := model.GetChannelPricing(req.ModelName, req.ChannelId)
if err != nil || existing == nil {
common.ApiErrorMsg(c, "channel pricing not found for this model and channel")
return
}

if err := model.SetDefaultChannel(req.ModelName, req.ChannelId); err != nil {
common.ApiError(c, err)
return
}

common.SysLog(fmt.Sprintf("[ChannelPricing] set default: model=%s channel=%d", req.ModelName, req.ChannelId))
common.ApiSuccess(c, nil)
}

// ClearDefaultChannel 清除指定模型的默认通道
func ClearDefaultChannel(c *gin.Context) {
modelName := c.Param("name")
modelName = strings.TrimPrefix(modelName, "/")
if modelName == "" {
common.ApiErrorMsg(c, "model name is required")
return
}

if err := model.ClearDefaultChannel(modelName); err != nil {
common.ApiError(c, err)
return
}

common.SysLog(fmt.Sprintf("[ChannelPricing] cleared default: model=%s", modelName))
common.ApiSuccess(c, nil)
}

+ 0
- 62
controller/playground_channels.go Просмотреть файл

@@ -1,62 +0,0 @@
package controller

import (
"net/http"

"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
)

func GetModelChannels(c *gin.Context) {
modelName := c.Query("model")
if modelName == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "model parameter is required",
})
return
}

userId := c.GetInt("id")
if userId == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "unauthorized",
})
return
}

userCache, err := model.GetUserCache(userId)
if err != nil || userCache == nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "failed to get user info",
})
return
}
userGroup := userCache.Group
if userGroup == "" {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "failed to get user group",
})
return
}

channels, defaultChannelId, err := model.GetModelChannelsForGroup(modelName, userGroup)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "failed to query channels",
})
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"channels": channels,
"default_channel_id": defaultChannelId,
},
})
}

+ 0
- 6
controller/relay.go Просмотреть файл

@@ -328,9 +328,6 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
if retryTimes <= 0 { if retryTimes <= 0 {
return false return false
} }
if _, ok := c.Get("specific_channel_id"); ok {
return false
}
code := openaiErr.StatusCode code := openaiErr.StatusCode
if code >= 200 && code < 300 { if code >= 200 && code < 300 {
return false return false
@@ -614,9 +611,6 @@ func shouldRetryTaskRelay(c *gin.Context, channelId int, taskErr *dto.TaskError,
if retryTimes <= 0 { if retryTimes <= 0 {
return false return false
} }
if _, ok := c.Get("specific_channel_id"); ok {
return false
}
if taskErr.StatusCode == http.StatusTooManyRequests { if taskErr.StatusCode == http.StatusTooManyRequests {
return true return true
} }


+ 0
- 2
i18n/keys.go Просмотреть файл

@@ -152,7 +152,6 @@ const (
// Channel related messages // Channel related messages
const ( const (
MsgChannelNotExists = "channel.not_exists" MsgChannelNotExists = "channel.not_exists"
MsgChannelIdFormatError = "channel.id_format_error"
MsgChannelNoAvailableKey = "channel.no_available_key" MsgChannelNoAvailableKey = "channel.no_available_key"
MsgChannelGetListFailed = "channel.get_list_failed" MsgChannelGetListFailed = "channel.get_list_failed"
MsgChannelGetTagsFailed = "channel.get_tags_failed" MsgChannelGetTagsFailed = "channel.get_tags_failed"
@@ -291,7 +290,6 @@ const (
// Distributor related messages // Distributor related messages
const ( const (
MsgDistributorInvalidRequest = "distributor.invalid_request" MsgDistributorInvalidRequest = "distributor.invalid_request"
MsgDistributorInvalidChannelId = "distributor.invalid_channel_id"
MsgDistributorChannelDisabled = "distributor.channel_disabled" MsgDistributorChannelDisabled = "distributor.channel_disabled"
MsgDistributorTokenNoModelAccess = "distributor.token_no_model_access" MsgDistributorTokenNoModelAccess = "distributor.token_no_model_access"
MsgDistributorTokenModelForbidden = "distributor.token_model_forbidden" MsgDistributorTokenModelForbidden = "distributor.token_model_forbidden"


+ 0
- 2
i18n/locales/en.yaml Просмотреть файл

@@ -132,7 +132,6 @@ topup.invalid_quota: "Invalid top-up quota"


# Channel messages # Channel messages
channel.not_exists: "Channel does not exist" channel.not_exists: "Channel does not exist"
channel.id_format_error: "Channel ID format error"
channel.no_available_key: "No available channel keys" channel.no_available_key: "No available channel keys"
channel.get_list_failed: "Failed to get channel list, please try again later" channel.get_list_failed: "Failed to get channel list, please try again later"
channel.get_tags_failed: "Failed to get tags, please try again later" channel.get_tags_failed: "Failed to get tags, please try again later"
@@ -243,7 +242,6 @@ common.invalid_input: "Invalid input"


# Distributor messages # Distributor messages
distributor.invalid_request: "Invalid request: {{.Error}}" distributor.invalid_request: "Invalid request: {{.Error}}"
distributor.invalid_channel_id: "Invalid channel ID"
distributor.channel_disabled: "This channel has been disabled" distributor.channel_disabled: "This channel has been disabled"
distributor.token_no_model_access: "This token has no access to any models" distributor.token_no_model_access: "This token has no access to any models"
distributor.token_model_forbidden: "This token has no access to model {{.Model}}" distributor.token_model_forbidden: "This token has no access to model {{.Model}}"


+ 0
- 2
i18n/locales/zh-CN.yaml Просмотреть файл

@@ -133,7 +133,6 @@ topup.invalid_quota: "无效的充值额度"


# Channel messages # Channel messages
channel.not_exists: "渠道不存在" channel.not_exists: "渠道不存在"
channel.id_format_error: "渠道ID格式错误"
channel.no_available_key: "没有可用的渠道密钥" channel.no_available_key: "没有可用的渠道密钥"
channel.get_list_failed: "获取渠道列表失败,请稍后重试" channel.get_list_failed: "获取渠道列表失败,请稍后重试"
channel.get_tags_failed: "获取标签失败,请稍后重试" channel.get_tags_failed: "获取标签失败,请稍后重试"
@@ -244,7 +243,6 @@ common.invalid_input: "输入不合法"


# Distributor messages # Distributor messages
distributor.invalid_request: "无效的请求,{{.Error}}" distributor.invalid_request: "无效的请求,{{.Error}}"
distributor.invalid_channel_id: "无效的渠道 Id"
distributor.channel_disabled: "该渠道已被禁用" distributor.channel_disabled: "该渠道已被禁用"
distributor.token_no_model_access: "该令牌无权访问任何模型" distributor.token_no_model_access: "该令牌无权访问任何模型"
distributor.token_model_forbidden: "该令牌无权访问模型 {{.Model}}" distributor.token_model_forbidden: "该令牌无权访问模型 {{.Model}}"


+ 0
- 2
i18n/locales/zh-TW.yaml Просмотреть файл

@@ -133,7 +133,6 @@ topup.invalid_quota: "無效的充值額度"


# Channel messages # Channel messages
channel.not_exists: "管道不存在" channel.not_exists: "管道不存在"
channel.id_format_error: "管道ID格式錯誤"
channel.no_available_key: "沒有可用的管道密鑰" channel.no_available_key: "沒有可用的管道密鑰"
channel.get_list_failed: "獲取管道列表失敗,請稍後重試" channel.get_list_failed: "獲取管道列表失敗,請稍後重試"
channel.get_tags_failed: "獲取標籤失敗,請稍後重試" channel.get_tags_failed: "獲取標籤失敗,請稍後重試"
@@ -244,7 +243,6 @@ common.invalid_input: "輸入不合法"


# Distributor messages # Distributor messages
distributor.invalid_request: "無效的請求,{{.Error}}" distributor.invalid_request: "無效的請求,{{.Error}}"
distributor.invalid_channel_id: "無效的管道 Id"
distributor.channel_disabled: "該管道已被禁用" distributor.channel_disabled: "該管道已被禁用"
distributor.token_no_model_access: "該令牌無權存取任何模型" distributor.token_no_model_access: "該令牌無權存取任何模型"
distributor.token_model_forbidden: "該令牌無權存取模型 {{.Model}}" distributor.token_model_forbidden: "該令牌無權存取模型 {{.Model}}"


+ 2
- 23
middleware/auth.go Просмотреть файл

@@ -30,29 +30,12 @@ func validUserInfo(username string, role int) bool {
return true return true
} }


// parseTokenKey 解析令牌字符串,提取 key 和可选的 channelId
// 支持格式:sk-{key} 或 sk-{key}:{channelId} 或 sk-{key}-{channelId}
// 返回:key, parts(parts[0]=key, parts[1]=channelId 如果指定)
// parseTokenKey 解析令牌字符串,提取 key
// 支持格式:sk-{key}
func parseTokenKey(key string) (string, []string) { func parseTokenKey(key string) (string, []string) {
key = strings.TrimPrefix(key, "sk-") key = strings.TrimPrefix(key, "sk-")

// 先按冒号分割获取 channelId(新格式:sk-{key}:{channelId})
var colonChannelId string
if colonIdx := strings.Index(key, ":"); colonIdx != -1 {
colonChannelId = key[colonIdx+1:]
key = key[:colonIdx]
}

parts := strings.Split(key, "-") parts := strings.Split(key, "-")
key = parts[0] key = parts[0]

// 如果冒号方式指定了渠道,验证并追加到 parts
if colonChannelId != "" {
// 验证 channelId 为有效数字,防止 sk-abc:def:ghi 等格式
if _, err := strconv.Atoi(colonChannelId); err == nil {
parts = append(parts, colonChannelId)
}
}
return key, parts return key, parts
} }


@@ -409,9 +392,5 @@ func SetupContextForToken(c *gin.Context, token *model.Token, parts ...string) e
} }
common.SetContextKey(c, constant.ContextKeyTokenGroup, token.Group) common.SetContextKey(c, constant.ContextKeyTokenGroup, token.Group)
common.SetContextKey(c, constant.ContextKeyTokenCrossGroupRetry, token.CrossGroupRetry) common.SetContextKey(c, constant.ContextKeyTokenCrossGroupRetry, token.CrossGroupRetry)
// 所有用户都可以通过 sk-{key}:{channelId} 或 sk-{key}-{channelId} 指定渠道
if len(parts) > 1 {
c.Set("specific_channel_id", parts[1])
}
return nil return nil
} }

+ 3
- 69
middleware/distributor.go Просмотреть файл

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"slices" "slices"
"strconv"
"strings" "strings"
"time" "time"


@@ -23,9 +22,8 @@ import (
) )


type ModelRequest struct { type ModelRequest struct {
Model string `json:"model"`
Group string `json:"group,omitempty"`
ChannelId int `json:"channel_id,omitempty"`
Model string `json:"model"`
Group string `json:"group,omitempty"`
} }


func Distribute() func(c *gin.Context) { func Distribute() func(c *gin.Context) {
@@ -36,31 +34,7 @@ func Distribute() func(c *gin.Context) {
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()})) abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()}))
return return
} }
channelId, ok := common.GetContextKey(c, constant.ContextKeyTokenSpecificChannelId)
if ok {
id, err := strconv.Atoi(channelId.(string))
if err != nil {
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId))
return
}
channel, err = model.GetChannelById(id, true)
if err != nil {
abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId))
return
}
if channel.Status != common.ChannelStatusEnabled {
abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled))
return
}
// 验证指定渠道是否支持请求的模型
if shouldSelectChannel && modelRequest.Model != "" {
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
if !model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, channel.Id) {
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, i18n.T(c, i18n.MsgDistributorNoAvailableChannel, map[string]any{"Group": usingGroup, "Model": modelRequest.Model}), types.ErrorCodeModelNotFound)
return
}
}
} else {
// Normal channel selection logic // Normal channel selection logic
// check token model mapping // check token model mapping
modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled) modelLimitEnable := common.GetContextKeyBool(c, constant.ContextKeyTokenModelLimitEnabled)
@@ -108,39 +82,6 @@ func Distribute() func(c *gin.Context) {
} }
} }


// 默认通道检查(管理员配置的模型默认通道)
if channel == nil {
if defaultChannelId, ok := model.GetDefaultChannelId(modelRequest.Model); ok {
defaultCh, err := model.CacheGetChannel(defaultChannelId)
if err != nil || defaultCh == nil {
common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d not found in cache, fallback", modelRequest.Model, defaultChannelId))
} else if defaultCh.Status != common.ChannelStatusEnabled {
common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d disabled(status=%d), fallback", modelRequest.Model, defaultChannelId, defaultCh.Status))
} else if usingGroup == "auto" {
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
autoGroups := service.GetUserAutoGroup(userGroup)
for _, g := range autoGroups {
if model.IsChannelEnabledForGroupModel(g, modelRequest.Model, defaultCh.Id) {
channel = defaultCh
selectGroup = g
common.SetContextKey(c, constant.ContextKeyAutoGroup, g)
common.SysLog(fmt.Sprintf("[Distribute] model=%s using default_channel=%d (auto group=%s)", modelRequest.Model, defaultChannelId, g))
break
}
}
if channel == nil {
common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d not enabled for any auto group, fallback", modelRequest.Model, defaultChannelId))
}
} else if model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, defaultCh.Id) {
channel = defaultCh
selectGroup = usingGroup
common.SysLog(fmt.Sprintf("[Distribute] model=%s using default_channel=%d (group=%s)", modelRequest.Model, defaultChannelId, usingGroup))
} else {
common.SysLog(fmt.Sprintf("[Distribute] model=%s default_channel=%d not enabled for group=%s, fallback", modelRequest.Model, defaultChannelId, usingGroup))
}
}
}

// 通道亲和性检查(仅在未选中默认通道时生效) // 通道亲和性检查(仅在未选中默认通道时生效)
if channel == nil { if channel == nil {
if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found { if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found {
@@ -189,7 +130,6 @@ func Distribute() func(c *gin.Context) {
} }
} }
} }
}
common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now()) common.SetContextKey(c, constant.ContextKeyRequestStartTime, time.Now())
SetupContextForSelectedChannel(c, channel, modelRequest.Model) SetupContextForSelectedChannel(c, channel, modelRequest.Model)
c.Next() c.Next()
@@ -374,12 +314,6 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
modelRequest.Group = g modelRequest.Group = g
} }
common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group) common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group)
// channel_id: body 优先,fallback 到 header
if req.ChannelId > 0 {
common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, strconv.Itoa(req.ChannelId))
} else if ch := c.GetHeader("X-Channel-Id"); ch != "" {
common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, ch)
}
} }


if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" { if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" {


+ 0
- 52
model/ability.go Просмотреть файл

@@ -66,58 +66,6 @@ func GetAbilitiesByChannelId(channelId int) ([]*Ability, error) {
return abilities, err return abilities, err
} }


// GetModelChannelsForGroup 返回指定模型在指定分组下的可用渠道列表及默认渠道ID
func GetModelChannelsForGroup(modelName string, group string) ([]map[string]any, int, error) {
var channelIds []int
err := DB.Model(&Ability{}).
Where("model = ?", modelName).
Where("enabled = ?", true).
Where(commonGroupCol+" = ?", group).
Distinct("channel_id").
Pluck("channel_id", &channelIds).Error
if err != nil {
return nil, 0, err
}

if len(channelIds) == 0 {
return []map[string]any{}, 0, nil
}

type channelInfo struct {
Id int `json:"id"`
Name string `json:"name"`
PublicName string `json:"public_name"`
}
var channels []channelInfo
err = DB.Table("channels").
Where("id IN ? AND status = ?", channelIds, common.ChannelStatusEnabled).
Select("id, name, public_name").
Find(&channels).Error
if err != nil {
return nil, 0, err
}

defaultChannelId := 0
if defaultChId, ok := GetDefaultChannelId(modelName); ok {
for _, id := range channelIds {
if id == defaultChId {
defaultChannelId = defaultChId
break
}
}
}

result := make([]map[string]any, 0, len(channels))
for _, ch := range channels {
result = append(result, map[string]any{
"id": ch.Id,
"name": ch.Name,
"public_name": ch.PublicName,
})
}

return result, defaultChannelId, nil
}


func getPriority(group string, model string, retry int) (int, error) { func getPriority(group string, model string, retry int) (int, error) {




+ 0
- 10
model/channel.go Просмотреть файл

@@ -283,16 +283,6 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err return channels, err
} }


// GetAllChannelsForBinding 获取所有启用的渠道(用于用户绑定渠道)
// 只返回 id, name, type, remark,不包含敏感信息
func GetAllChannelsForBinding() ([]*Channel, error) {
var channels []*Channel
err := DB.Select("id, name, public_name, type, remark").
Where("status = ?", common.ChannelStatusEnabled).
Order("priority desc").
Find(&channels).Error
return channels, err
}


func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, error) { func GetChannelsByTag(tag string, idSort bool, selectAll bool) ([]*Channel, error) {
var channels []*Channel var channels []*Channel


+ 3
- 120
model/channel_pricing.go Просмотреть файл

@@ -16,10 +16,6 @@ import (
var ( var (
channelPricingCache = make(map[string]*ChannelPricing) // key: "modelName:channelId" channelPricingCache = make(map[string]*ChannelPricing) // key: "modelName:channelId"
channelPricingCacheLock sync.RWMutex channelPricingCacheLock sync.RWMutex

// 默认通道缓存:modelName → channelId
defaultChannelCache = make(map[string]int)
defaultChannelCacheLock sync.RWMutex
) )


// QuotaType 计费类型 // QuotaType 计费类型
@@ -85,9 +81,7 @@ func (cp *ChannelPricing) Insert() error {
err := DB.Create(cp).Error err := DB.Create(cp).Error
if err == nil { if err == nil {
setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp)
if cp.IsDefault {
setDefaultChannelCache(cp.ModelName, cp.ChannelId)
}

} }
return err return err
} }
@@ -101,11 +95,7 @@ func (cp *ChannelPricing) Update() error {
Updates(cp).Error Updates(cp).Error
if err == nil { if err == nil {
setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp) setCache(getChannelPricingCacheKey(cp.ModelName, cp.ChannelId), cp)
if cp.IsDefault {
setDefaultChannelCache(cp.ModelName, cp.ChannelId)
} else {
clearDefaultChannelCacheIfMatch(cp.ModelName, cp.Id)
}

} }
return err return err
} }
@@ -118,9 +108,7 @@ func (cp *ChannelPricing) Delete() error {
err := DB.Delete(cp).Error err := DB.Delete(cp).Error
if err == nil { if err == nil {
removeCache(getChannelPricingCacheKey(existing.ModelName, existing.ChannelId)) removeCache(getChannelPricingCacheKey(existing.ModelName, existing.ChannelId))
if existing.IsDefault {
clearDefaultChannelCache(existing.ModelName)
}

} }
return err return err
} }
@@ -243,7 +231,6 @@ func LoadChannelPricingCache() {
} }
channelPricingCacheLock.Unlock() channelPricingCacheLock.Unlock()


rebuildDefaultChannelCache(pricings)
common.SysLog(fmt.Sprintf("[ChannelPricing] cache loaded %d records", len(pricings))) common.SysLog(fmt.Sprintf("[ChannelPricing] cache loaded %d records", len(pricings)))
} }


@@ -344,107 +331,3 @@ func GetChannelPricingByModelWithChannelInfo(modelName string) ([]*ChannelPricin
return results, nil return results, nil
} }


// === 默认通道缓存 ===

// syncIsDefaultToCache 将默认标记的变更同步到 channelPricingCache
func syncIsDefaultToCache(modelName string, channelId int, isDefault bool) {
channelPricingCacheLock.Lock()
for key, cp := range channelPricingCache {
if cp.ModelName == modelName {
if isDefault {
cp.IsDefault = cp.ChannelId == channelId
} else {
cp.IsDefault = false
}
}
channelPricingCache[key] = cp
}
channelPricingCacheLock.Unlock()
}

// setDefaultChannelCache 设置默认通道缓存
func setDefaultChannelCache(modelName string, channelId int) {
defaultChannelCacheLock.Lock()
defaultChannelCache[modelName] = channelId
defaultChannelCacheLock.Unlock()
}

// clearDefaultChannelCache 清除指定模型的默认通道缓存
func clearDefaultChannelCache(modelName string) {
defaultChannelCacheLock.Lock()
delete(defaultChannelCache, modelName)
defaultChannelCacheLock.Unlock()
}

// clearDefaultChannelCacheIfMatch 如果默认通道的定价记录 ID 匹配则清除
func clearDefaultChannelCacheIfMatch(modelName string, pricingId int) {
defaultChannelCacheLock.RLock()
cachedId, ok := defaultChannelCache[modelName]
defaultChannelCacheLock.RUnlock()
if !ok {
return
}
// 需要通过缓存找到对应的 pricing 来比对
key := getChannelPricingCacheKey(modelName, cachedId)
channelPricingCacheLock.RLock()
cp, exists := channelPricingCache[key]
channelPricingCacheLock.RUnlock()
if exists && cp.Id == pricingId {
clearDefaultChannelCache(modelName)
}
}

// rebuildDefaultChannelCache 从全量数据构建默认通道缓存(启动时调用)
func rebuildDefaultChannelCache(pricings []*ChannelPricing) {
defaultChannelCacheLock.Lock()
defaultChannelCache = make(map[string]int)
for _, cp := range pricings {
if cp.IsDefault {
defaultChannelCache[cp.ModelName] = cp.ChannelId
}
}
defaultChannelCacheLock.Unlock()
common.SysLog(fmt.Sprintf("[ChannelPricing] default channel cache loaded %d records", len(defaultChannelCache)))
}

// GetDefaultChannelId 获取指定模型的默认通道 ID(纯内存读)
func GetDefaultChannelId(modelName string) (int, bool) {
defaultChannelCacheLock.RLock()
id, ok := defaultChannelCache[modelName]
defaultChannelCacheLock.RUnlock()
return id, ok
}

// SetDefaultChannel 设置指定模型的默认通道(事务保证互斥)
func SetDefaultChannel(modelName string, channelId int) error {
return DB.Transaction(func(tx *gorm.DB) error {
// 清除该模型所有现有的默认标记
if err := tx.Model(&ChannelPricing{}).
Where("model_name = ? AND is_default = ?", modelName, true).
Update("is_default", false).Error; err != nil {
return err
}
// 设置新的默认
if err := tx.Model(&ChannelPricing{}).
Where("model_name = ? AND channel_id = ?", modelName, channelId).
Update("is_default", true).Error; err != nil {
return err
}
// 更新缓存
setDefaultChannelCache(modelName, channelId)
syncIsDefaultToCache(modelName, channelId, true)
return nil
})
}

// ClearDefaultChannel 清除指定模型的默认通道标记
func ClearDefaultChannel(modelName string) error {
err := DB.Model(&ChannelPricing{}).
Where("model_name = ? AND is_default = ?", modelName, true).
Update("is_default", false).Error
if err == nil {
clearDefaultChannelCache(modelName)
syncIsDefaultToCache(modelName, 0, false)
}
return err
}

+ 0
- 121
model/channel_pricing_test.go Просмотреть файл

@@ -119,124 +119,3 @@ func TestGetEffectivePricing_NotFound(t *testing.T) {
assert.False(t, ok) assert.False(t, ok)
assert.Nil(t, found) assert.Nil(t, found)
} }

// === 默认通道测试 ===

func TestDefaultChannel_SetAndGet(t *testing.T) {
setupChannelPricingDB(t)

// 创建两条定价记录
cp1 := &ChannelPricing{ModelName: "default-test-model", ChannelId: 100, QuotaType: QuotaTypeByTokens, ModelRatio: 1.0}
cp2 := &ChannelPricing{ModelName: "default-test-model", ChannelId: 200, QuotaType: QuotaTypeByTokens, ModelRatio: 2.0}
require.NoError(t, cp1.Insert())
require.NoError(t, cp2.Insert())
t.Cleanup(func() { cp1.Delete(); cp2.Delete() })

// 初始没有默认
_, ok := GetDefaultChannelId("default-test-model")
assert.False(t, ok)

// 设置通道 100 为默认
require.NoError(t, SetDefaultChannel("default-test-model", 100))
id, ok := GetDefaultChannelId("default-test-model")
assert.True(t, ok)
assert.Equal(t, 100, id)

// 切换默认到通道 200
require.NoError(t, SetDefaultChannel("default-test-model", 200))
id, ok = GetDefaultChannelId("default-test-model")
assert.True(t, ok)
assert.Equal(t, 200, id)

// 验证旧的默认标记被清除
found, _ := GetEffectivePricing("default-test-model", 100)
assert.False(t, found.IsDefault)
found, _ = GetEffectivePricing("default-test-model", 200)
assert.True(t, found.IsDefault)
}

func TestDefaultChannel_Clear(t *testing.T) {
setupChannelPricingDB(t)

cp := &ChannelPricing{ModelName: "clear-test-model", ChannelId: 300, QuotaType: QuotaTypeByTokens, ModelRatio: 1.0}
require.NoError(t, cp.Insert())
t.Cleanup(func() { cp.Delete() })

require.NoError(t, SetDefaultChannel("clear-test-model", 300))
_, ok := GetDefaultChannelId("clear-test-model")
assert.True(t, ok)

require.NoError(t, ClearDefaultChannel("clear-test-model"))
_, ok = GetDefaultChannelId("clear-test-model")
assert.False(t, ok)
}

func TestDefaultChannel_DeleteClearsCache(t *testing.T) {
setupChannelPricingDB(t)

cp := &ChannelPricing{ModelName: "delete-test-model", ChannelId: 400, QuotaType: QuotaTypeByTokens, ModelRatio: 1.0, IsDefault: true}
require.NoError(t, cp.Insert())

id, ok := GetDefaultChannelId("delete-test-model")
assert.True(t, ok)
assert.Equal(t, 400, id)

// 删除记录后缓存应清除
require.NoError(t, cp.Delete())
_, ok = GetDefaultChannelId("delete-test-model")
assert.False(t, ok)
}

func TestDefaultChannel_LoadCache(t *testing.T) {
db := setupChannelPricingDB(t)

// 直接插入数据(绕过缓存)
db.Create(&ChannelPricing{ModelName: "load-model", ChannelId: 500, ModelRatio: 1.0, IsDefault: true})
db.Create(&ChannelPricing{ModelName: "load-model", ChannelId: 501, ModelRatio: 2.0, IsDefault: false})
db.Create(&ChannelPricing{ModelName: "other-model", ChannelId: 502, ModelRatio: 1.0, IsDefault: true})

// 全量加载缓存
LoadChannelPricingCache()

id, ok := GetDefaultChannelId("load-model")
assert.True(t, ok)
assert.Equal(t, 500, id)

id, ok = GetDefaultChannelId("other-model")
assert.True(t, ok)
assert.Equal(t, 502, id)

// 无默认的模型
_, ok = GetDefaultChannelId("nonexistent")
assert.False(t, ok)
}

func TestDefaultChannel_InsertWithDefault(t *testing.T) {
setupChannelPricingDB(t)

cp := &ChannelPricing{ModelName: "insert-default-model", ChannelId: 600, ModelRatio: 1.0, IsDefault: true}
require.NoError(t, cp.Insert())
t.Cleanup(func() { cp.Delete() })

id, ok := GetDefaultChannelId("insert-default-model")
assert.True(t, ok)
assert.Equal(t, 600, id)
}

func TestDefaultChannel_UpdateWithDefault(t *testing.T) {
setupChannelPricingDB(t)

cp1 := &ChannelPricing{ModelName: "update-default-model", ChannelId: 700, ModelRatio: 1.0, IsDefault: true}
cp2 := &ChannelPricing{ModelName: "update-default-model", ChannelId: 701, ModelRatio: 2.0}
require.NoError(t, cp1.Insert())
require.NoError(t, cp2.Insert())
t.Cleanup(func() { cp1.Delete(); cp2.Delete() })

// cp1 是默认,通过 Update 把 cp2 设为默认
cp2.IsDefault = true
require.NoError(t, cp2.Update())

id, ok := GetDefaultChannelId("update-default-model")
assert.True(t, ok)
assert.Equal(t, 701, id)
}

+ 0
- 7
model/pricing.go Просмотреть файл

@@ -32,7 +32,6 @@ type Pricing struct {
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
PricingVersion string `json:"pricing_version,omitempty"` PricingVersion string `json:"pricing_version,omitempty"`
Type int `json:"type"` Type int `json:"type"`
DefaultChannelName string `json:"default_channel_name,omitempty"`
} }


type PricingVendor struct { type PricingVendor struct {
@@ -350,12 +349,6 @@ func updatePricing() {


// 使用渠道定价表中的实际数据,选取最便宜的渠道 // 使用渠道定价表中的实际数据,选取最便宜的渠道
applyBestChannelPricing(&pricing, cpMap[model], model) applyBestChannelPricing(&pricing, cpMap[model], model)
// 填充默认通道名称
if chId, ok := GetDefaultChannelId(model); ok {
if name, found := channelNameMap[chId]; found {
pricing.DefaultChannelName = name
}
}


pricingMap = append(pricingMap, pricing) pricingMap = append(pricingMap, pricing)
} }


+ 2
- 6
router/api-router.go Просмотреть файл

@@ -77,9 +77,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.GET("/self/groups", controller.GetUserGroups) selfRoute.GET("/self/groups", controller.GetUserGroups)
selfRoute.GET("/self", controller.GetSelf) selfRoute.GET("/self", controller.GetSelf)
selfRoute.GET("/models", controller.GetUserModels) selfRoute.GET("/models", controller.GetUserModels)
selfRoute.GET("/model_channels", controller.GetModelChannels)
selfRoute.GET("/channels", controller.GetUserChannelsForBinding)
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf) selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/token", controller.GenerateAccessToken)
selfRoute.GET("/passkey", controller.PasskeyStatus) selfRoute.GET("/passkey", controller.PasskeyStatus)
@@ -204,9 +202,7 @@ func SetApiRouter(router *gin.Engine) {
channelPricingRoute.POST("/batch", controller.BatchCreateChannelPricing) channelPricingRoute.POST("/batch", controller.BatchCreateChannelPricing)
channelPricingRoute.POST("/copy_global/:channel_id", controller.CopyGlobalPricing) channelPricingRoute.POST("/copy_global/:channel_id", controller.CopyGlobalPricing)
channelPricingRoute.DELETE("/:id", controller.DeleteChannelPricing) channelPricingRoute.DELETE("/:id", controller.DeleteChannelPricing)
channelPricingRoute.POST("/set_default", controller.SetDefaultChannel)
channelPricingRoute.DELETE("/default/*name", controller.ClearDefaultChannel)
}
}


// 用户倍率路由(管理员权限) // 用户倍率路由(管理员权限)
userChannelRatioRoute := apiRouter.Group("/user_channel_ratio") userChannelRatioRoute := apiRouter.Group("/user_channel_ratio")


Загрузка…
Отмена
Сохранить