diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 99833e912a..2fcd69a9b2 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -81,8 +81,8 @@ func (f optionalLimitField) ToServiceInput() *float64 { if f.value != nil { return f.value } - zero := 0.0 - return &zero + unlimited := -1.0 + return &unlimited } // NewGroupHandler creates a new admin group handler diff --git a/backend/internal/handler/admin/group_handler_limit_test.go b/backend/internal/handler/admin/group_handler_limit_test.go new file mode 100644 index 0000000000..4142e72ade --- /dev/null +++ b/backend/internal/handler/admin/group_handler_limit_test.go @@ -0,0 +1,33 @@ +package admin + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUpdateGroupRequestLimitFieldsTriState(t *testing.T) { + t.Run("omitted means unchanged", func(t *testing.T) { + var req UpdateGroupRequest + require.NoError(t, json.Unmarshal([]byte(`{}`), &req)) + require.Nil(t, req.DailyLimitUSD.ToServiceInput()) + require.Nil(t, req.WeeklyLimitUSD.ToServiceInput()) + require.Nil(t, req.MonthlyLimitUSD.ToServiceInput()) + }) + + t.Run("null means unlimited", func(t *testing.T) { + var req UpdateGroupRequest + require.NoError(t, json.Unmarshal([]byte(`{"daily_limit_usd":null}`), &req)) + limit := req.DailyLimitUSD.ToServiceInput() + require.NotNil(t, limit) + require.Negative(t, *limit) + }) + + t.Run("number is preserved", func(t *testing.T) { + var req UpdateGroupRequest + require.NoError(t, json.Unmarshal([]byte(`{"weekly_limit_usd":0,"monthly_limit_usd":42.5}`), &req)) + require.Equal(t, 0.0, *req.WeeklyLimitUSD.ToServiceInput()) + require.Equal(t, 42.5, *req.MonthlyLimitUSD.ToServiceInput()) + }) +} diff --git a/backend/internal/service/admin_group.go b/backend/internal/service/admin_group.go index 7b64e4b60f..9e29c30eaa 100644 --- a/backend/internal/service/admin_group.go +++ b/backend/internal/service/admin_group.go @@ -680,11 +680,16 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd if input.SubscriptionType != "" { group.SubscriptionType = input.SubscriptionType } - // 限额字段:nil/负数 表示"无限制",0 表示"不允许用量",正数表示具体限额 - // 前端始终发送这三个字段,无需 nil 守卫 - group.DailyLimitUSD = normalizeLimit(input.DailyLimitUSD) - group.WeeklyLimitUSD = normalizeLimit(input.WeeklyLimitUSD) - group.MonthlyLimitUSD = normalizeLimit(input.MonthlyLimitUSD) + // 限额字段:nil 表示不修改,负数表示"无限制",0 表示"不允许用量",正数表示具体限额。 + if input.DailyLimitUSD != nil { + group.DailyLimitUSD = normalizeLimit(input.DailyLimitUSD) + } + if input.WeeklyLimitUSD != nil { + group.WeeklyLimitUSD = normalizeLimit(input.WeeklyLimitUSD) + } + if input.MonthlyLimitUSD != nil { + group.MonthlyLimitUSD = normalizeLimit(input.MonthlyLimitUSD) + } // 图片生成计费配置:负数表示清除(使用默认价格) if input.AllowImageGeneration != nil { group.AllowImageGeneration = *input.AllowImageGeneration diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index 8b0374e6be..72c5e9a0f7 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -603,6 +603,42 @@ func TestAdminService_UpdateGroup_PreservesImageGenerationControlsWhenOmitted(t require.InDelta(t, 0.5, repo.updated.ImageRateMultiplier, 1e-12) } +func TestAdminService_UpdateGroup_LimitFieldsPartialUpdate(t *testing.T) { + daily, weekly, monthly := 10.0, 20.0, 30.0 + existingGroup := &Group{ + ID: 1, + Name: "existing-group", + Platform: PlatformOpenAI, + Status: StatusActive, + DailyLimitUSD: &daily, + WeeklyLimitUSD: &weekly, + MonthlyLimitUSD: &monthly, + } + repo := &groupRepoStubForAdmin{getByID: existingGroup} + svc := &adminServiceImpl{groupRepo: repo} + + t.Run("non-quota update preserves all limits", func(t *testing.T) { + description := "updated" + group, err := svc.UpdateGroup(context.Background(), existingGroup.ID, &UpdateGroupInput{Description: &description}) + require.NoError(t, err) + require.Equal(t, 10.0, *group.DailyLimitUSD) + require.Equal(t, 20.0, *group.WeeklyLimitUSD) + require.Equal(t, 30.0, *group.MonthlyLimitUSD) + }) + + t.Run("explicit changes and unlimited clear only touched limits", func(t *testing.T) { + newDaily, unlimited := 15.0, -1.0 + group, err := svc.UpdateGroup(context.Background(), existingGroup.ID, &UpdateGroupInput{ + DailyLimitUSD: &newDaily, + WeeklyLimitUSD: &unlimited, + }) + require.NoError(t, err) + require.Equal(t, 15.0, *group.DailyLimitUSD) + require.Nil(t, group.WeeklyLimitUSD) + require.Equal(t, 30.0, *group.MonthlyLimitUSD) + }) +} + func TestAdminService_UpdateGroup_DisablesBatchImageWhenImageGenerationDisabled(t *testing.T) { existingGroup := &Group{ ID: 1,