diff --git a/controller/channel_pricing.go b/controller/channel_pricing.go new file mode 100644 index 0000000..16bcf0e --- /dev/null +++ b/controller/channel_pricing.go @@ -0,0 +1,305 @@ +package controller + +import ( + "strconv" + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/setting/ratio_setting" + + "github.com/gin-gonic/gin" +) + +// GetAllChannelPricing 获取所有渠道定价(分页) +func GetAllChannelPricing(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("p", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + offset := (page - 1) * pageSize + + list, total, err := model.GetAllChannelPricing(offset, pageSize) + if err != nil { + common.ApiError(c, err) + return + } + + common.ApiSuccess(c, gin.H{ + "page": page, + "page_size": pageSize, + "total": total, + "items": list, + }) +} + +// GetChannelPricingByModel 获取指定模型的所有渠道定价 +func GetChannelPricingByModel(c *gin.Context) { + modelName := c.Param("name") + if modelName == "" { + common.ApiErrorMsg(c, "model name is required") + return + } + + list, err := model.GetChannelPricingByModel(modelName) + if err != nil { + common.ApiError(c, err) + return + } + + common.ApiSuccess(c, list) +} + +// CreateChannelPricingRequest 创建渠道定价请求 +type CreateChannelPricingRequest struct { + Id int `json:"id"` + ModelName string `json:"model_name" binding:"required"` + ChannelId int `json:"channel_id" binding:"required"` + QuotaType int `json:"quota_type"` + ModelRatio float64 `json:"model_ratio"` + CompletionRatio float64 `json:"completion_ratio"` + ModelPrice float64 `json:"model_price"` + TagIds string `json:"tag_ids"` +} + +// CreateChannelPricing 创建或更新渠道定价 +func CreateChannelPricing(c *gin.Context) { + var req CreateChannelPricingRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.ApiError(c, err) + return + } + + // 检查是否已存在 + 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 + if err := existing.Update(); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, existing) + return + } + + // 创建 + cp := &model.ChannelPricing{ + ModelName: req.ModelName, + ChannelId: req.ChannelId, + QuotaType: req.QuotaType, + ModelRatio: req.ModelRatio, + CompletionRatio: req.CompletionRatio, + ModelPrice: req.ModelPrice, + TagIds: req.TagIds, + } + if err := cp.Insert(); err != nil { + common.ApiError(c, err) + return + } + + common.ApiSuccess(c, cp) +} + +// BatchCreateChannelPricingRequest 批量创建请求 +type BatchCreateChannelPricingRequest struct { + Items []*CreateChannelPricingRequest `json:"items" binding:"required"` +} + +// BatchCreateChannelPricing 批量创建或更新渠道定价 +func BatchCreateChannelPricing(c *gin.Context) { + var req BatchCreateChannelPricingRequest + if err := c.ShouldBindJSON(&req); err != nil { + common.ApiError(c, err) + return + } + + 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, + }) + } + + // 使用事务逐个处理(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 + if err := existing.Update(); err != nil { + common.ApiError(c, err) + return + } + } else { + if err := cp.Insert(); err != nil { + common.ApiError(c, err) + return + } + } + } + + common.ApiSuccess(c, gin.H{"affected": len(pricings)}) +} + +// DeleteChannelPricing 删除渠道定价 +func DeleteChannelPricing(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.Atoi(idStr) + if err != nil { + common.ApiError(c, err) + return + } + + cp := &model.ChannelPricing{Id: id} + if err := cp.Delete(); err != nil { + common.ApiError(c, err) + return + } + + common.ApiSuccess(c, nil) +} + +// CopyGlobalPricingRequest 复制全局定价请求 +type CopyGlobalPricingRequest struct { + Overwrite bool `json:"overwrite"` // 是否覆盖已存在的渠道定价 +} + +// CopyGlobalPricing 复制全局定价到指定渠道 +// 从 ratio_setting 读取全局定价信息,复制到 channel_pricing 表 +func CopyGlobalPricing(c *gin.Context) { + channelIdStr := c.Param("channel_id") + channelId, err := strconv.Atoi(channelIdStr) + if err != nil { + common.ApiError(c, err) + return + } + + var req CopyGlobalPricingRequest + c.ShouldBindJSON(&req) + + // 获取该渠道支持的所有模型(使用 GetAbilitiesByChannelId) + abilities, err := model.GetAbilitiesByChannelId(channelId) + if err != nil { + common.ApiError(c, err) + return + } + + imported := 0 + for _, ability := range abilities { + // 检查是否已存在 + existing, _ := model.GetChannelPricing(ability.Model, channelId) + if existing != nil && !req.Overwrite { + continue + } + + // 确定定价类型 + var quotaType int + var ratio, completionRatio, price float64 + + // 优先检查是否有按次计费的价格 + modelPrice, hasPrice := ratio_setting.GetModelPrice(ability.Model, false) + if hasPrice { + quotaType = model.QuotaTypeByCall + price = modelPrice + } else { + // 使用按量计费 + quotaType = model.QuotaTypeByTokens + modelRatio, hasRatio, _ := ratio_setting.GetModelRatio(ability.Model) + if hasRatio { + ratio = modelRatio + } + completionRatio = ratio_setting.GetCompletionRatio(ability.Model) + } + + if existing != nil { + existing.QuotaType = quotaType + existing.ModelRatio = ratio + existing.CompletionRatio = completionRatio + existing.ModelPrice = price + if err := existing.Update(); err == nil { + imported++ + } + } else { + cp := &model.ChannelPricing{ + ModelName: ability.Model, + ChannelId: channelId, + QuotaType: quotaType, + ModelRatio: ratio, + CompletionRatio: completionRatio, + ModelPrice: price, + } + if err := cp.Insert(); err == nil { + imported++ + } + } + } + + common.ApiSuccess(c, gin.H{ + "total": len(abilities), + "imported": imported, + }) +} + +// ChannelPricingWithTags 带标签详情的渠道定价响应 +type ChannelPricingWithTags struct { + *model.ChannelPricing + Tags []*model.PricingTag `json:"tags"` +} + +// GetChannelPricingWithTags 获取渠道定价(带标签详情) +func GetChannelPricingWithTags(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("p", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + offset := (page - 1) * pageSize + + list, total, err := model.GetAllChannelPricing(offset, pageSize) + if err != nil { + common.ApiError(c, err) + return + } + + // 获取所有标签 + tags, _ := model.GetAllPricingTags() + tagMap := make(map[int]*model.PricingTag) + for _, tag := range tags { + tagMap[tag.Id] = tag + } + + // 为每个定价填充标签详情 + result := make([]*ChannelPricingWithTags, 0, len(list)) + for _, cp := range list { + item := &ChannelPricingWithTags{ + ChannelPricing: cp, + Tags: make([]*model.PricingTag, 0), + } + if cp.TagIds != "" { + for _, idStr := range strings.Split(cp.TagIds, ",") { + if id, err := strconv.Atoi(idStr); err == nil { + if tag, ok := tagMap[id]; ok { + item.Tags = append(item.Tags, tag) + } + } + } + } + result = append(result, item) + } + + common.ApiSuccess(c, gin.H{ + "page": page, + "page_size": pageSize, + "total": total, + "items": result, + }) +}