From 87a0f94975a79d54847ccf2b823c895e4b8e6d2c Mon Sep 17 00:00:00 2001 From: fengsilin Date: Wed, 3 Jun 2026 11:22:08 +0800 Subject: [PATCH] refactor: remove user-channel-ratio feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除用户渠道倍率(UserChannelRatio)功能,清理全链路相关代码: 后端删除 model/controller/router 层实现,前端删除 UI 组件和计费展示逻辑。 同时恢复 Token 分组选择和价格侧边栏分组过滤器的注释代码。 Co-Authored-By: Claude Sonnet 4.6 --- controller/pricing.go | 7 +- controller/pricing_user_test.go | 129 +--------- controller/relay.go | 2 - controller/user_channel_ratio.go | 116 --------- ...26-06-02-overseas-user-migration-design.md | 86 +++++-- model/main.go | 5 +- model/user_channel_ratio.go | 129 ---------- model/user_channel_ratio_test.go | 105 -------- relay/compatible_handler.go | 4 +- relay/helper/price.go | 31 --- relay/helper/price_test.go | 35 --- router/api-router.go | 10 - service/log_info_generate.go | 3 - service/quota.go | 13 +- types/price_data.go | 3 +- .../model-pricing/layout/PricingSidebar.jsx | 4 +- .../table/tokens/modals/EditTokenModal.jsx | 10 +- .../table/users/modals/EditUserModal.jsx | 7 - .../table/users/modals/UserRatioSection.jsx | 241 ------------------ web/src/helpers/render.jsx | 30 +-- web/src/hooks/usage-logs/useUsageLogsData.jsx | 10 - .../Ratio/ModelSettingsVisualEditor.jsx | 88 +------ 22 files changed, 91 insertions(+), 977 deletions(-) delete mode 100644 controller/user_channel_ratio.go delete mode 100644 model/user_channel_ratio.go delete mode 100644 model/user_channel_ratio_test.go delete mode 100644 web/src/components/table/users/modals/UserRatioSection.jsx diff --git a/controller/pricing.go b/controller/pricing.go index ea41bad..2b07b9f 100644 --- a/controller/pricing.go +++ b/controller/pricing.go @@ -115,14 +115,14 @@ func GetUserPricing(c *gin.Context) { return } - loggedIn, userId, userGroup, groupRatio := resolveCurrentUser(c) + loggedIn, _, userGroup, groupRatio := resolveCurrentUser(c) if !loggedIn { respondOriginalPrice(c, pricingData) return } - bestUserChannelRatio := model.GetBestUserChannelRatio(userId, modelName) - totalRatio := groupRatio * bestUserChannelRatio + + totalRatio := groupRatio savingsPercent := int(math.Round((1 - totalRatio) * 100)) result := gin.H{ @@ -131,7 +131,6 @@ func GetUserPricing(c *gin.Context) { "quota_type": pricingData.QuotaType, "group": userGroup, "group_ratio": groupRatio, - "user_channel_ratio": bestUserChannelRatio, "savings_percent": savingsPercent, "logged_in": true, } diff --git a/controller/pricing_user_test.go b/controller/pricing_user_test.go index 8f86a8f..31326b8 100644 --- a/controller/pricing_user_test.go +++ b/controller/pricing_user_test.go @@ -29,7 +29,7 @@ func setupPricingTestDB(t *testing.T) *gorm.DB { common.UsingSQLite = true common.RedisEnabled = false - require.NoError(t, db.AutoMigrate(&model.UserChannelRatio{}, &model.User{})) + require.NoError(t, db.AutoMigrate(&model.User{})) t.Cleanup(func() { model.DB = origDB @@ -226,94 +226,7 @@ func TestGetUserPricing_LoggedIn_GroupDiscount(t *testing.T) { assert.Equal(t, "8折", resp["discount"]) } -// TestGetUserPricing_LoggedIn_UserChannelRatio 已登录,有用户渠道倍率 -func TestGetUserPricing_LoggedIn_UserChannelRatio(t *testing.T) { - db := setupPricingTestDB(t) - - setPricingCache([]model.Pricing{ - { - ModelName: "gpt-4o", - QuotaType: 0, - ModelRatio: 15, - CompletionRatio: 4, - EnableGroup: []string{"default"}, - }, - }) - - // 创建用户(default 分组,倍率为1) - user := &model.User{Id: 300, Group: "default", Username: "specialuser", Status: 1} - require.NoError(t, db.Create(user).Error) - - // 插入用户渠道倍率 - ucr := &model.UserChannelRatio{ - UserId: 300, - ModelName: "gpt-4o", - ChannelId: 1, - Ratio: 0.9, - } - require.NoError(t, ucr.Insert()) - - router := setupAuthRouter(300) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/pricing/user/gpt-4o", nil) - router.ServeHTTP(w, req) - - var resp map[string]interface{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.True(t, resp["success"].(bool)) - assert.Equal(t, true, resp["logged_in"]) - // group_ratio=1 * user_channel_ratio=0.9 = 0.9 - assert.Equal(t, float64(0.9), resp["user_channel_ratio"]) - assert.Equal(t, float64(10), resp["savings_percent"]) - // 输入用户价: 30 * 0.9 = 27 - assert.Equal(t, float64(27), resp["user_input"]) -} - -// TestGetUserPricing_LoggedIn_BothDiscounts 分组倍率 + 用户渠道倍率叠加 -func TestGetUserPricing_LoggedIn_BothDiscounts(t *testing.T) { - db := setupPricingTestDB(t) - - setPricingCache([]model.Pricing{ - { - ModelName: "gpt-4o", - QuotaType: 0, - ModelRatio: 15, - CompletionRatio: 4, - EnableGroup: []string{"default", "vip"}, - }, - }) - - user := &model.User{Id: 400, Group: "vip", Username: "bothdiscount", Status: 1} - require.NoError(t, db.Create(user).Error) - - // VIP 分组倍率 0.8 - withGroupRatio(t, `{"default":1,"vip":0.8}`) - - // 用户渠道倍率 0.9 - ucr := &model.UserChannelRatio{ - UserId: 400, - ModelName: "gpt-4o", - ChannelId: 1, - Ratio: 0.9, - } - require.NoError(t, ucr.Insert()) - router := setupAuthRouter(400) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/pricing/user/gpt-4o", nil) - router.ServeHTTP(w, req) - - var resp map[string]interface{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.True(t, resp["success"].(bool)) - assert.Equal(t, true, resp["logged_in"]) - // total = 0.8 * 0.9 = 0.72, savings = 28% - assert.Equal(t, float64(28), resp["savings_percent"]) - // 输入: 30 * 0.72 = 21.6 - assert.Equal(t, float64(21.6), resp["user_input"]) - // 输出: 120 * 0.72 = 86.4 - assert.Equal(t, float64(86.4), resp["user_output"]) -} // TestGetUserPricing_PerCall_WithDiscount 按次计费 + 折扣 func TestGetUserPricing_PerCall_WithDiscount(t *testing.T) { @@ -368,43 +281,3 @@ func TestFormatDiscount(t *testing.T) { } } -// TestGetUserPricing_MultipleUserChannelRatios 多个渠道倍率取最低值 -func TestGetUserPricing_MultipleUserChannelRatios(t *testing.T) { - db := setupPricingTestDB(t) - - setPricingCache([]model.Pricing{ - { - ModelName: "gpt-4o", - QuotaType: 0, - ModelRatio: 15, - CompletionRatio: 4, - EnableGroup: []string{"default"}, - }, - }) - - user := &model.User{Id: 600, Group: "default", Username: "multichannel", Status: 1} - require.NoError(t, db.Create(user).Error) - - // 多个渠道倍率,取最低值 0.7 - for i, ratio := range []float64{0.9, 0.7, 0.8} { - ucr := &model.UserChannelRatio{ - UserId: 600, - ModelName: "gpt-4o", - ChannelId: i + 1, - Ratio: ratio, - } - require.NoError(t, ucr.Insert()) - } - - router := setupAuthRouter(600) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/pricing/user/gpt-4o", nil) - router.ServeHTTP(w, req) - - var resp map[string]interface{} - require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) - assert.True(t, resp["success"].(bool)) - // 应取最低倍率 0.7 - assert.Equal(t, float64(0.7), resp["user_channel_ratio"]) - assert.Equal(t, float64(30), resp["savings_percent"]) -} diff --git a/controller/relay.go b/controller/relay.go index e495c9e..4ca1497 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -204,8 +204,6 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { break } - // 在渠道选择后更新价格数据以使用渠道定价 - helper.UpdatePriceDataForChannelPricing(c, relayInfo, channel.Id) addUsedChannel(c, channel.Id) bodyStorage, bodyErr := common.GetBodyStorage(c) diff --git a/controller/user_channel_ratio.go b/controller/user_channel_ratio.go deleted file mode 100644 index 293b1ab..0000000 --- a/controller/user_channel_ratio.go +++ /dev/null @@ -1,116 +0,0 @@ -package controller - -import ( - "net/http" - "strconv" - - "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/model" - - "github.com/gin-gonic/gin" -) - -func GetUserChannelRatios(c *gin.Context) { - userIdStr := c.Param("user_id") - userId, err := strconv.Atoi(userIdStr) - if err != nil { - common.ApiError(c, err) - return - } - - ratios, err := model.GetUserChannelRatiosByUserId(userId) - if err != nil { - common.ApiError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": ratios, - }) -} - -type createUserChannelRatioRequest struct { - UserId int `json:"user_id"` - ModelName string `json:"model_name"` - ChannelId int `json:"channel_id"` - Ratio float64 `json:"ratio"` -} - -func CreateUserChannelRatio(c *gin.Context) { - var req createUserChannelRatioRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.ApiError(c, err) - return - } - - if req.Ratio <= 0 { - common.ApiErrorMsg(c, "ratio must be positive") - return - } - - ucr := &model.UserChannelRatio{ - UserId: req.UserId, - ModelName: req.ModelName, - ChannelId: req.ChannelId, - Ratio: req.Ratio, - } - if err := ucr.Insert(); err != nil { - common.ApiError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": ucr, - }) -} - -func UpdateUserChannelRatio(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.Atoi(idStr) - if err != nil { - common.ApiError(c, err) - return - } - - var req struct { - Ratio float64 `json:"ratio"` - } - if err := c.ShouldBindJSON(&req); err != nil { - common.ApiError(c, err) - return - } - if req.Ratio <= 0 { - common.ApiErrorMsg(c, "ratio must be positive") - return - } - - ucr := &model.UserChannelRatio{ - Id: id, - Ratio: req.Ratio, - } - if err := ucr.Update(); err != nil { - common.ApiError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "data": ucr, - }) -} - -func DeleteUserChannelRatio(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.Atoi(idStr) - if err != nil { - common.ApiError(c, err) - return - } - - if err := model.DeleteUserChannelRatioById(id); err != nil { - common.ApiError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - }) -} diff --git a/docs/superpowers/specs/2026-06-02-overseas-user-migration-design.md b/docs/superpowers/specs/2026-06-02-overseas-user-migration-design.md index f7ab3d0..5539f05 100644 --- a/docs/superpowers/specs/2026-06-02-overseas-user-migration-design.md +++ b/docs/superpowers/specs/2026-06-02-overseas-user-migration-design.md @@ -324,23 +324,43 @@ - 无冲突:推荐 `create_new` - 仅 `aff_code` 冲突:推荐 `create_new` -- 同邮箱且无其他矛盾:推荐 `merge_into_existing` +- 同邮箱且无其他矛盾、且目标国内用户尚未在海外存在其他 `synced` 副本:推荐 `merge_into_existing` - 任意 OAuth 多对一、一对多关系:强制人工处理 --- +## 执行一致性约束 + +- 预检查快照只用于冲突决策和界面展示,不作为最终额度结算依据。 +- 执行阶段必须重新拉取海外用户最新快照,并以该快照中的最终额度为准。 +- V1 版本要求在运维冻结窗口内执行批次。冻结窗口至少要覆盖待迁用户的充值、兑换码、额度转移和新请求入口,并等待已在途请求自然结束。 +- 如果执行时最新快照与预检查结论发生冲突性漂移(例如邮箱归属变化、OAuth 绑定目标变化、原本计划合并的国内账号已存在其他海外 `synced` 副本),该条目必须失败并要求重新扫描,不得静默沿用旧决议。 + +--- + ## 单用户迁移执行流程 -执行顺序不能改变,必须先构建国内主账号,再将海外原账号降级。 +执行顺序不能改变,必须先构建国内主账号,再将海外原账号降级。执行阶段读取的“最新快照”必须发生在冻结窗口内。 ### `create_new` -1. 从海外拉取该用户最新快照 +1. 在冻结窗口内,从海外拉取该用户最新快照 2. 在国内开启事务 3. 通过“按哈希导入”的专用逻辑创建国内主账号 - 不能走普通 `Insert()` - 否则密码会再次哈希 -4. 复制主字段: +4. 在同一个国内事务中完成以下动作: + - 创建国内主账号 + - 复制主字段 + - 复制内置 OAuth 字段 + - 复制 `user_oauth_bindings` + - 处理 `aff_code` 冲突 + - 回写 `user_migration_items.target_user_id` +5. 如在执行阶段遇到新的唯一键冲突: + - 直接将该迁移项标记为失败 + - 要求重新扫描或人工修正 + - 不允许按用户名/邮箱自动认领为“上次已创建成功”的账号 +6. 复制主字段: - `username` - `password` - `email` @@ -350,31 +370,35 @@ - `group` - `created_at` - `setting` -5. 复制内置 OAuth ID 字段 -6. 复制 `user_oauth_bindings` +7. 复制内置 OAuth ID 字段 +8. 复制 `user_oauth_bindings` - 先通过 `slug` 将海外 `provider_id` 映射为国内 `provider_id` -7. 处理邀请码 +9. 处理邀请码 - 如冲突则生成新的 `aff_code` -8. 将国内额度设置为海外当前最终额度 -9. 国内事务提交,得到 `cn_user_id` -10. 调海外内部接口,将原海外用户转换为 `synced` - - `source = synced` - - `remote_user_id = cn_user_id` - - `synced_quota = 当前额度` - - `last_sync_at = now` -11. 更新迁移项为成功 +10. 将国内额度设置为“执行时最新快照中的最终额度” +11. 国内事务提交,得到 `cn_user_id` +12. 调海外内部接口,将原海外用户转换为 `synced` + - `source = synced` + - `remote_user_id = cn_user_id` + - `synced_quota = 国内提交后的最终额度` + - `last_sync_at = now` +13. 更新迁移项为成功 ### `merge_into_existing` -1. 锁定目标国内账号 -2. 重新校验该用户仍应合并到这个目标 -3. 补齐国内为空、海外有值的字段 -4. 补齐国内缺失的 OAuth 绑定 -5. 额度合并规则: - - `国内最终额度 = 国内原额度 + 海外当前额度` -6. 提交国内事务 -7. 调海外接口将原账号转换为 `synced` -8. 更新迁移项为成功 +1. 在冻结窗口内拉取该用户最新快照 +2. 锁定目标国内账号 +3. 重新校验该用户仍应合并到这个目标 + - 同邮箱或唯一 OAuth 目标仍然成立 + - 目标国内账号在海外不存在其他 `synced` 副本 +4. 补齐国内为空、海外有值的字段 +5. 补齐国内缺失的 OAuth 绑定 +6. 额度合并规则: + - `国内最终额度 = 国内原额度 + 执行时最新海外额度` + - 通过 `migration_token` 保证每个迁移项最多叠加一次 +7. 提交国内事务 +8. 调海外接口将原账号转换为 `synced` +9. 更新迁移项为成功 ### 为什么不删除海外原用户 @@ -393,8 +417,11 @@ - `GET /api/internal/migration/users` - 分页返回候选用户快照 + - 仅返回 `source=local` 的海外用户 - `GET /api/internal/migration/users/:id` - 返回单用户最新快照 +- `GET /api/internal/migration/users/check-synced-copy?cn_user_id=:id` + - 检查某个国内用户是否已在海外存在 `synced` 副本 - `POST /api/internal/migration/users/:id/convert-to-synced` - 将海外原用户降级为 `synced` @@ -406,6 +433,7 @@ - 用户存在 - 用户尚未迁移,或已迁移且目标一致 - `remote_user_id` 不为空 +- 如果用户已是 `synced` 但 `remote_user_id` 与请求不一致,返回冲突错误 幂等语义: @@ -486,6 +514,7 @@ - 触发预检查 - `POST /api/user-migrations/items/:id/resolve` - 设置处理策略 + - 成功后重新统计批次状态;若所有项都已变为 `ready` 或 `skipped`,批次自动转为 `ready` - `POST /api/user-migrations/batches/:id/execute` - 执行迁移 - `POST /api/user-migrations/batches/:id/retry` @@ -537,9 +566,9 @@ ## 冻结窗口设计 -本次迁移不要求全站停机,但建议对“待迁批次中的海外用户”做短冻结窗口。 +本次迁移不要求全站停机,但 V1 版本将“冻结窗口”定义为执行前的强制运维前置条件,而不是可选建议。 -冻结范围建议仅限该批次用户: +冻结范围应仅限该批次用户: - 禁止充值 - 禁止兑换码 @@ -547,6 +576,11 @@ - 禁止发起新请求 - 等待已在途请求自然结束 +执行要求: + +- root 操作员在点击“开始迁移”前确认冻结窗口已经生效 +- 批次执行期间如果发现冻结条件被破坏,应立即停止后续条目并重新扫描 + 目的: - 稳定海外最终额度 diff --git a/model/main.go b/model/main.go index 3e5f139..371702c 100644 --- a/model/main.go +++ b/model/main.go @@ -207,8 +207,7 @@ func InitDB() (err error) { return err } LoadEmailQuotaCache() - LoadUserChannelRatioCache() - return nil + return nil } else { common.FatalLog(err) } @@ -283,7 +282,6 @@ func migrateDB() error { &SubscriptionPreConsumeRecord{}, &CustomOAuthProvider{}, &UserOAuthBinding{}, - &UserChannelRatio{}, &PendingSyncRecord{}, &QuotaSyncLog{}, &EmailQuotaRule{}, @@ -349,7 +347,6 @@ func migrateDBFast() error { {&SubscriptionPreConsumeRecord{}, "SubscriptionPreConsumeRecord"}, {&CustomOAuthProvider{}, "CustomOAuthProvider"}, {&UserOAuthBinding{}, "UserOAuthBinding"}, - {&UserChannelRatio{}, "UserChannelRatio"}, {&PendingSyncRecord{}, "PendingSyncRecord"}, {&QuotaSyncLog{}, "QuotaSyncLog"}, {&EmailQuotaRule{}, "EmailQuotaRule"}, diff --git a/model/user_channel_ratio.go b/model/user_channel_ratio.go deleted file mode 100644 index d722a91..0000000 --- a/model/user_channel_ratio.go +++ /dev/null @@ -1,129 +0,0 @@ -package model - -import ( - "strconv" - "strings" - "sync" - - "github.com/QuantumNous/new-api/common" -) - -var ( - userChannelRatioCache = make(map[string]float64) // key: "userId:modelName:channelId" -> ratio - userChannelRatioCacheLock sync.RWMutex -) - -// UserChannelRatio 用户-模型-渠道倍率表 -type UserChannelRatio struct { - Id int `json:"id" gorm:"primaryKey"` - UserId int `json:"user_id" gorm:"not null;uniqueIndex:idx_user_model_channel,priority:1"` - ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:idx_user_model_channel,priority:2"` - ChannelId int `json:"channel_id" gorm:"not null;uniqueIndex:idx_user_model_channel,priority:3"` - Ratio float64 `json:"ratio" gorm:"default:1"` - CreatedAt int64 `json:"created_at" gorm:"bigint"` - UpdatedAt int64 `json:"updated_at" gorm:"bigint"` -} - -func getUserChannelRatioCacheKey(userId int, modelName string, channelId int) string { - return strconv.Itoa(userId) + ":" + modelName + ":" + strconv.Itoa(channelId) -} - -func setUserChannelRatioCache(key string, ratio float64) { - userChannelRatioCacheLock.Lock() - userChannelRatioCache[key] = ratio - userChannelRatioCacheLock.Unlock() -} - -func removeUserChannelRatioCache(key string) { - userChannelRatioCacheLock.Lock() - delete(userChannelRatioCache, key) - userChannelRatioCacheLock.Unlock() -} - -// GetUserChannelRatio 获取用户在指定模型+渠道的倍率(纯内存读) -// 未命中返回 1.0(不影响计费) -func GetUserChannelRatio(userId int, modelName string, channelId int) float64 { - key := getUserChannelRatioCacheKey(userId, modelName, channelId) - userChannelRatioCacheLock.RLock() - ratio, ok := userChannelRatioCache[key] - userChannelRatioCacheLock.RUnlock() - if !ok { - return 1.0 - } - return ratio -} - -// LoadUserChannelRatioCache 全量加载到内存(启动时调用) -func LoadUserChannelRatioCache() { - var records []*UserChannelRatio - if err := DB.Find(&records).Error; err != nil { - common.SysError("[UserChannelRatio] LoadCache failed: " + err.Error()) - return - } - userChannelRatioCacheLock.Lock() - userChannelRatioCache = make(map[string]float64, len(records)) - for _, r := range records { - key := getUserChannelRatioCacheKey(r.UserId, r.ModelName, r.ChannelId) - userChannelRatioCache[key] = r.Ratio - } - userChannelRatioCacheLock.Unlock() - common.SysLog("[UserChannelRatio] cache loaded " + strconv.Itoa(len(records)) + " records") -} - -func (ucr *UserChannelRatio) Insert() error { - ucr.CreatedAt = common.GetTimestamp() - ucr.UpdatedAt = common.GetTimestamp() - err := DB.Create(ucr).Error - if err == nil { - setUserChannelRatioCache(getUserChannelRatioCacheKey(ucr.UserId, ucr.ModelName, ucr.ChannelId), ucr.Ratio) - } - return err -} - -func (ucr *UserChannelRatio) Update() error { - var existing UserChannelRatio - if err := DB.First(&existing, ucr.Id).Error; err != nil { - return err - } - ucr.UpdatedAt = common.GetTimestamp() - err := DB.Model(&UserChannelRatio{}).Where("id = ?", ucr.Id). - Select("ratio", "updated_at"). - Updates(ucr).Error - if err == nil { - setUserChannelRatioCache(getUserChannelRatioCacheKey(existing.UserId, existing.ModelName, existing.ChannelId), ucr.Ratio) - } - return err -} - -func DeleteUserChannelRatioById(id int) error { - var existing UserChannelRatio - if err := DB.First(&existing, id).Error; err != nil { - return err - } - err := DB.Delete(&existing).Error - if err == nil { - removeUserChannelRatioCache(getUserChannelRatioCacheKey(existing.UserId, existing.ModelName, existing.ChannelId)) - } - return err -} - -// GetUserChannelRatiosByUserId 获取指定用户的所有倍率记录 -func GetUserChannelRatiosByUserId(userId int) ([]*UserChannelRatio, error) { - var list []*UserChannelRatio - err := DB.Where("user_id = ?", userId).Find(&list).Error - return list, err -} - -// GetBestUserChannelRatio 从内存缓存中查找指定用户+模型的最低渠道倍率(未命中返回 1.0) -func GetBestUserChannelRatio(userId int, modelName string) float64 { - best := 1.0 - prefix := strconv.Itoa(userId) + ":" + modelName + ":" - userChannelRatioCacheLock.RLock() - for key, ratio := range userChannelRatioCache { - if len(key) > len(prefix) && strings.HasPrefix(key, prefix) && ratio < best { - best = ratio - } - } - userChannelRatioCacheLock.RUnlock() - return best -} diff --git a/model/user_channel_ratio_test.go b/model/user_channel_ratio_test.go deleted file mode 100644 index d3c3053..0000000 --- a/model/user_channel_ratio_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package model - -import ( - "testing" - - "github.com/QuantumNous/new-api/common" - "github.com/glebarez/sqlite" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gorm.io/gorm" -) - -func setupUserChannelRatioDB(t *testing.T) *gorm.DB { - t.Helper() - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) - require.NoError(t, err) - sqlDB, _ := db.DB() - sqlDB.SetMaxOpenConns(1) - - origDB := DB - DB = db - common.UsingSQLite = true - common.RedisEnabled = false - - require.NoError(t, db.AutoMigrate(&UserChannelRatio{})) - - t.Cleanup(func() { - DB = origDB - sqlDB.Close() - }) - return db -} - -func TestUserChannelRatioCRUD(t *testing.T) { - setupUserChannelRatioDB(t) - LoadUserChannelRatioCache() - - ucr := &UserChannelRatio{ - UserId: 1, - ModelName: "gpt-4o", - ChannelId: 5, - Ratio: 0.8, - } - err := ucr.Insert() - require.NoError(t, err) - assert.True(t, ucr.Id > 0) - - ratio := GetUserChannelRatio(1, "gpt-4o", 5) - assert.Equal(t, 0.8, ratio) - - ratio = GetUserChannelRatio(999, "nonexistent", 999) - assert.Equal(t, 1.0, ratio) - - ucr.Ratio = 1.2 - err = ucr.Update() - require.NoError(t, err) - ratio = GetUserChannelRatio(1, "gpt-4o", 5) - assert.Equal(t, 1.2, ratio) - - list, err := GetUserChannelRatiosByUserId(1) - require.NoError(t, err) - assert.Len(t, list, 1) - assert.Equal(t, "gpt-4o", list[0].ModelName) - - err = DeleteUserChannelRatioById(ucr.Id) - require.NoError(t, err) - ratio = GetUserChannelRatio(1, "gpt-4o", 5) - assert.Equal(t, 1.0, ratio) - - list, err = GetUserChannelRatiosByUserId(1) - require.NoError(t, err) - assert.Len(t, list, 0) -} - -func TestUserChannelRatioUniqueConstraint(t *testing.T) { - setupUserChannelRatioDB(t) - LoadUserChannelRatioCache() - - ucr1 := &UserChannelRatio{UserId: 1, ModelName: "gpt-4o", ChannelId: 5, Ratio: 0.8} - err := ucr1.Insert() - require.NoError(t, err) - - ucr2 := &UserChannelRatio{UserId: 1, ModelName: "gpt-4o", ChannelId: 5, Ratio: 1.0} - err = ucr2.Insert() - assert.Error(t, err) - - ucr3 := &UserChannelRatio{UserId: 1, ModelName: "gpt-4o", ChannelId: 6, Ratio: 1.5} - err = ucr3.Insert() - assert.NoError(t, err) -} - -func TestLoadUserChannelRatioCache(t *testing.T) { - setupUserChannelRatioDB(t) - - ucr1 := &UserChannelRatio{UserId: 1, ModelName: "gpt-4o", ChannelId: 5, Ratio: 0.8} - ucr2 := &UserChannelRatio{UserId: 2, ModelName: "claude-3", ChannelId: 10, Ratio: 1.5} - require.NoError(t, ucr1.Insert()) - require.NoError(t, ucr2.Insert()) - - LoadUserChannelRatioCache() - - assert.Equal(t, 0.8, GetUserChannelRatio(1, "gpt-4o", 5)) - assert.Equal(t, 1.5, GetUserChannelRatio(2, "claude-3", 10)) - assert.Equal(t, 1.0, GetUserChannelRatio(3, "nonexistent", 99)) -} diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index d0923a0..5dfe7ae 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -287,7 +287,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage dCachedCreationRatio := decimal.NewFromFloat(cachedCreationRatio) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) - ratio := dModelRatio.Mul(dGroupRatio).Mul(decimal.NewFromFloat(relayInfo.PriceData.UserChannelRatio)) + ratio := dModelRatio.Mul(dGroupRatio) // openai web search 工具计费 var dWebSearchQuota decimal.Decimal @@ -401,7 +401,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage quotaCalculateDecimal = decimal.NewFromInt(1) } } else { - quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio).Mul(decimal.NewFromFloat(relayInfo.PriceData.UserChannelRatio)) + quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio) } // 添加 responses tools call 调用的配额 quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota) diff --git a/relay/helper/price.go b/relay/helper/price.go index f893d11..2b98fa5 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -4,9 +4,7 @@ import ( "fmt" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/logger" - "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/setting/ratio_setting" @@ -116,7 +114,6 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens CacheCreationRatio: cacheCreationRatio, CacheCreation5mRatio: cacheCreationRatio, CacheCreation1hRatio: cacheCreationRatio * types.ClaudeCacheCreation1hMultiplier, - UserChannelRatio: 1.0, } if common.DebugEnabled { @@ -154,7 +151,6 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) types. ModelPrice: modelPrice, Quota: quota, GroupRatioInfo: groupRatioInfo, - UserChannelRatio: 1.0, } return priceData } @@ -170,30 +166,3 @@ func ContainPriceOrRatio(modelName string) bool { } return false } - -// UpdatePriceDataForChannelPricing 保留原调用点,仅在选定渠道后应用用户渠道倍率。 -func UpdatePriceDataForChannelPricing(c *gin.Context, info *relaycommon.RelayInfo, channelId int) { - if info == nil || channelId <= 0 { - return - } - ApplyUserChannelRatio(c, info, channelId) -} - -// ApplyUserChannelRatio 在渠道选定后应用用户倍率 -func ApplyUserChannelRatio(c *gin.Context, info *relaycommon.RelayInfo, channelId int) { - if info == nil || channelId <= 0 { - return - } - userId := common.GetContextKeyInt(c, constant.ContextKeyUserId) - if userId <= 0 { - return - } - userRatio := model.GetUserChannelRatio(userId, info.OriginModelName, channelId) - if userRatio == 1.0 { - return - } - info.PriceData.UserChannelRatio = userRatio - if info.PriceData.QuotaToPreConsume > 0 { - info.PriceData.QuotaToPreConsume = int(float64(info.PriceData.QuotaToPreConsume) * userRatio) - } -} diff --git a/relay/helper/price_test.go b/relay/helper/price_test.go index 09289e8..42a25e8 100644 --- a/relay/helper/price_test.go +++ b/relay/helper/price_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/QuantumNous/new-api/common" - "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" @@ -39,8 +38,6 @@ func setupUsePriceSwitchTest(t *testing.T) { common.UsingSQLite = true common.RedisEnabled = false - require.NoError(t, db.AutoMigrate(&model.UserChannelRatio{})) - require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{"`+testModelUsePriceSwitch+`":0.5}`)) require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{"`+testModelUsePriceSwitch+`":15}`)) require.NoError(t, ratio_setting.UpdateCompletionRatioByJSONString(`{"`+testModelUsePriceSwitch+`":3}`)) @@ -119,38 +116,6 @@ func TestGlobalRatiosFilledWhenUseRatioMode(t *testing.T) { assert.Equal(t, 2.0, priceData.ImageRatio) } -func TestUpdatePriceDataForChannelPricingOnlyAppliesUserChannelRatio(t *testing.T) { - setupUsePriceSwitchTest(t) - - ratio := &model.UserChannelRatio{ - UserId: 9, - ModelName: testModelUsePriceSwitch, - ChannelId: 9901, - Ratio: 0.8, - } - require.NoError(t, ratio.Insert()) - - c := buildTestContext(t) - c.Set(string(constant.ContextKeyUserId), 9) - - info := &relaycommon.RelayInfo{ - OriginModelName: testModelUsePriceSwitch, - PriceData: types.PriceData{ - ModelPrice: 0.5, - UsePrice: true, - QuotaToPreConsume: 1000, - UserChannelRatio: 1.0, - }, - } - - UpdatePriceDataForChannelPricing(c, info, 9901) - - assert.Equal(t, 0.8, info.PriceData.UserChannelRatio) - assert.Equal(t, 800, info.PriceData.QuotaToPreConsume) - assert.Equal(t, 0.5, info.PriceData.ModelPrice) -} - -// --- HandleGroupRatio tests --- func TestHandleGroupRatio_DefaultGroup(t *testing.T) { setupUsePriceSwitchTest(t) diff --git a/router/api-router.go b/router/api-router.go index 946b316..ab70558 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -191,16 +191,6 @@ func SetApiRouter(router *gin.Engine) { optionRoute.POST("/migrate_console_setting", controller.MigrateConsoleSetting) // 用于迁移检测的旧键,下个版本会删除 } - // 用户倍率路由(管理员权限) - userChannelRatioRoute := apiRouter.Group("/user_channel_ratio") - userChannelRatioRoute.Use(middleware.AdminAuth()) - { - userChannelRatioRoute.GET("/:user_id", controller.GetUserChannelRatios) - userChannelRatioRoute.POST("/", controller.CreateUserChannelRatio) - userChannelRatioRoute.PUT("/:id", controller.UpdateUserChannelRatio) - userChannelRatioRoute.DELETE("/:id", controller.DeleteUserChannelRatio) - } - // 邮箱后缀额度规则路由(管理员权限) emailQuotaRuleRoute := apiRouter.Group("/email_quota_rule") emailQuotaRuleRoute.Use(middleware.AdminAuth()) diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 46cca48..1c44091 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -41,9 +41,6 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m other["cache_ratio"] = cacheRatio other["model_price"] = modelPrice other["user_group_ratio"] = userGroupRatio - if relayInfo != nil && relayInfo.PriceData.UserChannelRatio != 1.0 { - other["user_channel_ratio"] = relayInfo.PriceData.UserChannelRatio - } other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli()) if relayInfo.ReasoningEffort != "" { other["reasoning_effort"] = relayInfo.ReasoningEffort diff --git a/service/quota.go b/service/quota.go index 63442b5..7446237 100644 --- a/service/quota.go +++ b/service/quota.go @@ -37,7 +37,6 @@ type QuotaInfo struct { ModelPrice float64 ModelRatio float64 GroupRatio float64 - UserChannelRatio float64 } func hasCustomModelRatio(modelName string, currentRatio float64) bool { @@ -54,7 +53,7 @@ func calculateAudioQuota(info QuotaInfo) int { quotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) groupRatio := decimal.NewFromFloat(info.GroupRatio) - quota := modelPrice.Mul(quotaPerUnit).Mul(groupRatio).Mul(decimal.NewFromFloat(info.UserChannelRatio)) + quota := modelPrice.Mul(quotaPerUnit).Mul(groupRatio) return int(quota.IntPart()) } @@ -64,7 +63,7 @@ func calculateAudioQuota(info QuotaInfo) int { groupRatio := decimal.NewFromFloat(info.GroupRatio) modelRatio := decimal.NewFromFloat(info.ModelRatio) - ratio := groupRatio.Mul(modelRatio).Mul(decimal.NewFromFloat(info.UserChannelRatio)) + ratio := groupRatio.Mul(modelRatio) inputTextTokens := decimal.NewFromInt(int64(info.InputDetails.TextTokens)) outputTextTokens := decimal.NewFromInt(int64(info.OutputDetails.TextTokens)) @@ -135,7 +134,6 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag UsePrice: relayInfo.UsePrice, ModelRatio: modelRatio, GroupRatio: actualGroupRatio, - UserChannelRatio: relayInfo.PriceData.UserChannelRatio, } quota := calculateAudioQuota(quotaInfo) @@ -189,7 +187,6 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod UsePrice: usePrice, ModelRatio: modelRatio, GroupRatio: groupRatio, - UserChannelRatio: relayInfo.PriceData.UserChannelRatio, } quota := calculateAudioQuota(quotaInfo) @@ -252,7 +249,6 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, completionRatio := relayInfo.PriceData.CompletionRatio modelRatio := relayInfo.PriceData.ModelRatio groupRatio := relayInfo.PriceData.GroupRatioInfo.GroupRatio - userChannelRatio := relayInfo.PriceData.UserChannelRatio modelPrice := relayInfo.PriceData.ModelPrice cacheRatio := relayInfo.PriceData.CacheRatio cacheTokens := usage.PromptTokensDetails.CachedTokens @@ -287,9 +283,9 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, calculateQuota += float64(remainingCacheCreationTokens) * cacheCreationRatio } calculateQuota += float64(completionTokens) * completionRatio - calculateQuota = calculateQuota * groupRatio * modelRatio * userChannelRatio + calculateQuota = calculateQuota * groupRatio * modelRatio } else { - calculateQuota = modelPrice * common.QuotaPerUnit * groupRatio * userChannelRatio + calculateQuota = modelPrice * common.QuotaPerUnit * groupRatio } if modelRatio != 0 && calculateQuota <= 0 { @@ -394,7 +390,6 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, u UsePrice: usePrice, ModelRatio: modelRatio, GroupRatio: groupRatio, - UserChannelRatio: relayInfo.PriceData.UserChannelRatio, } quota := calculateAudioQuota(quotaInfo) diff --git a/types/price_data.go b/types/price_data.go index 892e8cc..6076f3b 100644 --- a/types/price_data.go +++ b/types/price_data.go @@ -29,7 +29,6 @@ type PriceData struct { Quota int // 按次计费的最终额度(MJ / Task) QuotaToPreConsume int // 按量计费的预消耗额度 GroupRatioInfo GroupRatioInfo - UserChannelRatio float64 // 用户-模型-渠道倍率(默认 1.0) } // ApplyChannelPricingRatios 将渠道定价的扩展比率应用到 PriceData(非零值覆盖) @@ -65,5 +64,5 @@ func (p *PriceData) AddOtherRatio(key string, ratio float64) { } func (p *PriceData) ToSetting() string { - return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f, UserChannelRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio, p.UserChannelRatio) + return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio) } diff --git a/web/src/components/table/model-pricing/layout/PricingSidebar.jsx b/web/src/components/table/model-pricing/layout/PricingSidebar.jsx index a1977aa..28e48ce 100644 --- a/web/src/components/table/model-pricing/layout/PricingSidebar.jsx +++ b/web/src/components/table/model-pricing/layout/PricingSidebar.jsx @@ -122,7 +122,7 @@ const PricingSidebar = ({ t={t} /> */} - {/* */} + /> { } } setGroups(localGroupOptions); - // if (statusState?.status?.default_use_auto_group && formApiRef.current) { - // formApiRef.current.setValue('group', 'auto'); - // } + if (statusState?.status?.default_use_auto_group && formApiRef.current) { + formApiRef.current.setValue('group', 'auto'); + } } else { showError(t(message)); } @@ -360,7 +360,7 @@ const EditTokenModal = (props) => { showClear /> - {/* + {groups.length > 0 ? ( { '开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道', )} /> - */} + { )} - {/* 倍率设置 */} - {userId && ( - - - - )} {/* 模型速率限制 */} {userId && ( diff --git a/web/src/components/table/users/modals/UserRatioSection.jsx b/web/src/components/table/users/modals/UserRatioSection.jsx deleted file mode 100644 index bd91586..0000000 --- a/web/src/components/table/users/modals/UserRatioSection.jsx +++ /dev/null @@ -1,241 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { API, showError, showSuccess } from '../../../../helpers'; -import { - Button, - Table, - InputNumber, - Modal, - Form, - Avatar, - Typography, - Popconfirm, -} from '@douyinfe/semi-ui'; -import { IconStar } from '@douyinfe/semi-icons'; - -const { Text } = Typography; - -const UserRatioSection = ({ userId }) => { - const { t } = useTranslation(); - const [ratios, setRatios] = useState([]); - const [loading, setLoading] = useState(false); - const [addModalVisible, setAddModalVisible] = useState(false); - const [channelOptions, setChannelOptions] = useState([]); - const [modelOptions, setModelOptions] = useState([]); - const formApiRef = useRef(null); - - const loadRatios = async () => { - if (!userId) return; - setLoading(true); - try { - const res = await API.get(`/api/user_channel_ratio/${userId}`); - const { success, data, message } = res.data; - if (success) { - setRatios(data || []); - } else { - showError(message); - } - } catch (e) { - showError(e.message); - } - setLoading(false); - }; - - const loadChannels = async () => { - try { - const res = await API.get('/api/channel/?p=0&page_size=500'); - const { success, data, message } = res.data; - if (success) { - const options = (data?.items || []).map((ch) => ({ - label: ch.remark - ? `${ch.name} (${ch.remark})` - : `${ch.name} (ID: ${ch.id})`, - value: ch.id, - models: (ch.models || '').split(',').map((m) => m.trim()).filter(Boolean), - })); - setChannelOptions(options); - } else { - showError(message); - } - } catch (e) { - showError(e.message); - } - }; - - useEffect(() => { - loadRatios(); - loadChannels(); - }, [userId]); - - const handleAdd = async (values) => { - const res = await API.post('/api/user_channel_ratio/', { - user_id: userId, - model_name: values.model_name, - channel_id: values.channel_id, - ratio: values.ratio, - }); - const { success, message } = res.data; - if (success) { - showSuccess(t('添加成功')); - setAddModalVisible(false); - loadRatios(); - } else { - showError(message); - } - }; - - const handleUpdate = async (id, newRatio) => { - const res = await API.put(`/api/user_channel_ratio/${id}`, { - ratio: newRatio, - }); - const { success, message } = res.data; - if (success) { - showSuccess(t('更新成功')); - loadRatios(); - } else { - showError(message); - } - }; - - const handleDelete = async (id) => { - const res = await API.delete(`/api/user_channel_ratio/${id}`); - const { success, message } = res.data; - if (success) { - showSuccess(t('删除成功')); - loadRatios(); - } else { - showError(message); - } - }; - - const columns = [ - { title: t('模型'), dataIndex: 'model_name', key: 'model_name' }, - { - title: t('渠道'), - dataIndex: 'channel_id', - key: 'channel_id', - render: (channelId) => { - const ch = channelOptions.find((c) => c.value === channelId); - return ch ? ch.label : channelId; - }, - }, - { - title: t('倍率'), - dataIndex: 'ratio', - key: 'ratio', - render: (text, record) => ( - { - if (val !== text) handleUpdate(record.id, val); - }} - style={{ width: 100 }} - /> - ), - }, - { - title: t('操作'), - key: 'action', - render: (_, record) => ( - handleDelete(record.id)} - > - - - ), - }, - ]; - - return ( - <> -
-
- - - -
- {t('倍率设置')} -
- {t( - '为该用户设置特定模型+渠道的倍率乘数,未设置时默认为 1', - )} -
-
-
- -
- - - - formApiRef.current?.submitForm()} - onCancel={() => setAddModalVisible(false)} - > -
(formApiRef.current = api)} - onSubmit={handleAdd} - > - { - formApiRef.current?.setValue('model_name', undefined); - const selected = channelOptions.find((c) => c.value === val); - if (selected?.models) { - setModelOptions(selected.models.map((m) => ({ label: m, value: m }))); - } else { - setModelOptions([]); - } - }} - /> - - - -
- - ); -}; - -export default UserRatioSection; diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index 6da72ca..118affa 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -1184,8 +1184,7 @@ function getEffectiveRatio(groupRatio, user_group_ratio) { const useUserGroupRatio = isValidGroupRatio(user_group_ratio); const ratioLabel = useUserGroupRatio ? i18next.t('专属倍率') - // : i18next.t('分组倍率'); // 原文字,已改为"倍率" - : i18next.t('倍率'); + : i18next.t('分组倍率'); const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio; return { @@ -1316,7 +1315,6 @@ export function renderModelPrice( audioInputPrice = 0, imageGenerationCall = false, imageGenerationCallPrice = 0, - userChannelRatio, ) { const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( groupRatio, @@ -1327,12 +1325,11 @@ export function renderModelPrice( // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); - const ucr = (userChannelRatio != null && userChannelRatio !== 1.0) ? userChannelRatio : null; if (modelPrice !== -1) { const displayPrice = (modelPrice * rate).toFixed(6); - const displayTotal = (modelPrice * groupRatio * (ucr || 1) * rate).toFixed(6); - const ratioParts = `${ratioLabel}:${groupRatio}` + (ucr ? ` * 用户倍率:${ucr}` : ''); + const displayTotal = (modelPrice * groupRatio * rate).toFixed(6); + const ratioParts = `${ratioLabel}:${groupRatio}` ; return i18next.t( '模型价格:{{symbol}}{{price}} * {{ratioParts}} = {{symbol}}{{total}}', { @@ -1370,9 +1367,6 @@ export function renderModelPrice( (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + imageGenerationCallPrice * groupRatio; - if (ucr) { - price *= ucr; - } return ( <> @@ -1501,14 +1495,14 @@ export function renderModelPrice( // 构建输出部分描述 const outputDesc = i18next.t( - '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}{{userRatio}}', + '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}', { completion: completionTokens, symbol: symbol, compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, - userRatio: ucr ? ` * 用户倍率 ${ucr}` : '', + }, ); @@ -1583,7 +1577,6 @@ export function renderLogContent( webSearchCallCount = 0, fileSearch = false, fileSearchCallCount = 0, - userChannelRatio, ) { const { ratio, @@ -1593,10 +1586,7 @@ export function renderLogContent( // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); - - const userRatioSuffix = (userChannelRatio != null && userChannelRatio !== 1.0) - ? i18next.t(',用户倍率 {{userChannelRatio}}', { userChannelRatio }) - : ''; + const userRatioSuffix = ''; let result; if (modelPrice !== -1) { @@ -1897,7 +1887,6 @@ export function renderClaudeModelPrice( cacheCreationRatio5m = 1.0, cacheCreationTokens1h = 0, cacheCreationRatio1h = 1.0, - userChannelRatio, ) { const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( groupRatio, @@ -1955,7 +1944,7 @@ export function renderClaudeModelPrice( cacheCreationTokens5m * cacheCreationRatio5m + cacheCreationTokens1h * cacheCreationRatio1h; - const userRatio = userChannelRatio ?? 1.0; + const userRatio = 1.0; let price = ((effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + (completionTokens / 1000000) * completionRatioPrice * groupRatio) * @@ -2169,7 +2158,6 @@ export function renderClaudeLogContent( cacheCreationRatio5m = 1.0, cacheCreationTokens1h = 0, cacheCreationRatio1h = 1.0, - userChannelRatio, ) { const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( groupRatio, @@ -2179,10 +2167,8 @@ export function renderClaudeLogContent( // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); + const userRatioSuffix = ''; - const userRatioSuffix = (userChannelRatio != null && userChannelRatio !== 1.0) - ? i18next.t(',用户倍率 {{userChannelRatio}}', { userChannelRatio }) - : ''; if (modelPrice !== -1) { return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx index 0680f41..50977b0 100644 --- a/web/src/hooks/usage-logs/useUsageLogsData.jsx +++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx @@ -431,7 +431,6 @@ export const useLogsData = () => { other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0, - other?.user_channel_ratio, ) : renderLogContent( other?.model_ratio, @@ -446,7 +445,6 @@ export const useLogsData = () => { other.web_search_call_count || 0, other.file_search || false, other.file_search_call_count || 0, - other?.user_channel_ratio, ), }); if (logs[i]?.content) { @@ -522,7 +520,6 @@ export const useLogsData = () => { other.cache_creation_ratio_1h || other.cache_creation_ratio || 1.0, - other.user_channel_ratio, ); } else { content = renderModelPrice( @@ -549,7 +546,6 @@ export const useLogsData = () => { other?.audio_input_price || 0, other?.image_generation_call || false, other?.image_generation_call_price || 0, - other?.user_channel_ratio, ); } expandDataLocal.push({ @@ -563,12 +559,6 @@ export const useLogsData = () => { value: other.reasoning_effort, }); } - if (other?.user_channel_ratio) { - expandDataLocal.push({ - key: t('用户倍率'), - value: other.user_channel_ratio, - }); - } } if (logs[i].type === 6) { if (other?.task_id) { diff --git a/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx b/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx index f82cd4b..65bfe81 100644 --- a/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx +++ b/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect, useState, useRef, useCallback } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Table, Button, @@ -38,9 +38,7 @@ import { IconEdit, } from '@douyinfe/semi-icons'; import { API, showError, showSuccess, getQuotaPerUnit } from '../../../helpers'; -import { channelPricingApi, pricingTagApi } from '../../../helpers/api'; import { useTranslation } from 'react-i18next'; -import ChannelPricingView from './ChannelPricingView'; export default function ModelSettingsVisualEditor(props) { const { t } = useTranslation(); @@ -58,12 +56,6 @@ export default function ModelSettingsVisualEditor(props) { const pageSize = 10; const quotaPerUnit = getQuotaPerUnit(); - // Channel pricing view mode - const [viewMode, setViewMode] = useState('global'); // 'global' | 'channel' - const [channels, setChannels] = useState([]); - const [channelPricings, setChannelPricings] = useState([]); - const [tags, setTags] = useState([]); - useEffect(() => { try { const modelPrice = JSON.parse(props.options.ModelPrice || '{}'); @@ -98,53 +90,6 @@ export default function ModelSettingsVisualEditor(props) { } }, [props.options]); - // Load channels for channel pricing view - const loadChannels = useCallback(async () => { - try { - const res = await API.get('/api/channel/?page_size=100'); - if (res.data.success) { - // API returns {items: [...], total: ...}, extract items array - const data = res.data.data; - setChannels(Array.isArray(data) ? data : (data?.items || [])); - } - } catch (e) { - console.error('Failed to load channels', e); - } - }, []); - - // Load channel pricings - const loadChannelPricings = useCallback(async () => { - try { - const res = await channelPricingApi.getWithTags(1, 1000); - if (res.data.success) { - setChannelPricings(res.data.data.items || []); - } - } catch (e) { - console.error('Failed to load channel pricings', e); - } - }, []); - - // Load tags - const loadTags = useCallback(async () => { - try { - const res = await pricingTagApi.getAll(); - if (res.data.success) { - setTags(res.data.data || []); - } - } catch (e) { - console.error('Failed to load tags', e); - } - }, []); - - // Load channel data when switching to channel view - useEffect(() => { - if (viewMode === 'channel') { - loadChannels(); - loadChannelPricings(); - loadTags(); - } - }, [viewMode, loadChannels, loadChannelPricings, loadTags]); - // 首先声明分页相关的工具函数 const getPagedData = (data, currentPage, pageSize) => { const start = (currentPage - 1) * pageSize; @@ -510,23 +455,9 @@ export default function ModelSettingsVisualEditor(props) { return ( <> - {/* View mode switch */} -
- setViewMode(e.target.value)} - type="button" - > - {t('全局定价')} - {t('渠道定价')} - -
- - {/* Global pricing view */} - {viewMode === 'global' && ( - - -