diff --git a/backend/internal/service/subscription_reset_quota_test.go b/backend/internal/service/subscription_reset_quota_test.go index 70eba686..36aa177f 100644 --- a/backend/internal/service/subscription_reset_quota_test.go +++ b/backend/internal/service/subscription_reset_quota_test.go @@ -32,8 +32,12 @@ func (r *resetQuotaUserSubRepoStub) GetByID(_ context.Context, id int64) (*UserS return &cp, nil } -func (r *resetQuotaUserSubRepoStub) ResetDailyUsage(_ context.Context, _ int64, _ time.Time) error { +func (r *resetQuotaUserSubRepoStub) ResetDailyUsage(_ context.Context, _ int64, windowStart time.Time) error { r.resetDailyCalled = true + if r.resetDailyErr == nil && r.sub != nil { + r.sub.DailyUsageUSD = 0 + r.sub.DailyWindowStart = &windowStart + } return r.resetDailyErr } @@ -88,6 +92,19 @@ func TestAdminResetQuota_ResetWeeklyOnly(t *testing.T) { require.True(t, stub.resetWeeklyCalled, "应调用 ResetWeeklyUsage") } +func TestAdminResetQuota_BothFalseReturnsError(t *testing.T) { + stub := &resetQuotaUserSubRepoStub{ + sub: &UserSubscription{ID: 7, UserID: 10, GroupID: 20}, + } + svc := newResetQuotaSvc(stub) + + _, err := svc.AdminResetQuota(context.Background(), 7, false, false) + + require.ErrorIs(t, err, ErrInvalidInput) + require.False(t, stub.resetDailyCalled) + require.False(t, stub.resetWeeklyCalled) +} + func TestAdminResetQuota_SubscriptionNotFound(t *testing.T) { stub := &resetQuotaUserSubRepoStub{sub: nil} svc := newResetQuotaSvc(stub) @@ -129,26 +146,21 @@ func TestAdminResetQuota_ResetWeeklyUsageError(t *testing.T) { } func TestAdminResetQuota_ReturnsRefreshedSub(t *testing.T) { - now := time.Now() - windowStart := startOfDay(now) - sub := &UserSubscription{ - ID: 6, - UserID: 10, - GroupID: 20, - DailyUsageUSD: 99.9, + stub := &resetQuotaUserSubRepoStub{ + sub: &UserSubscription{ + ID: 6, + UserID: 10, + GroupID: 20, + DailyUsageUSD: 99.9, + }, } - stub := &resetQuotaUserSubRepoStub{sub: sub} - // 模拟 ResetDailyUsage 将 DB 中的数据归零 - stub.resetDailyErr = nil - stub.ResetDailyUsage(context.Background(), sub.ID, windowStart) //nolint:errcheck - // 手动更新 stub 中的 sub,模拟 DB 写入效果 - stub.resetDailyCalled = false - stub.sub.DailyUsageUSD = 0 - stub.sub.DailyWindowStart = &windowStart svc := newResetQuotaSvc(stub) result, err := svc.AdminResetQuota(context.Background(), 6, true, false) require.NoError(t, err) + // ResetDailyUsage stub 会将 sub.DailyUsageUSD 归零, + // 服务应返回第二次 GetByID 的刷新值而非初始的 99.9 require.Equal(t, float64(0), result.DailyUsageUSD, "返回的订阅应反映已归零的用量") + require.True(t, stub.resetDailyCalled) } diff --git a/backend/internal/service/subscription_service.go b/backend/internal/service/subscription_service.go index c8836542..55f029fa 100644 --- a/backend/internal/service/subscription_service.go +++ b/backend/internal/service/subscription_service.go @@ -31,6 +31,7 @@ var ( ErrSubscriptionAlreadyExists = infraerrors.Conflict("SUBSCRIPTION_ALREADY_EXISTS", "subscription already exists for this user and group") ErrSubscriptionAssignConflict = infraerrors.Conflict("SUBSCRIPTION_ASSIGN_CONFLICT", "subscription exists but request conflicts with existing assignment semantics") ErrGroupNotSubscriptionType = infraerrors.BadRequest("GROUP_NOT_SUBSCRIPTION_TYPE", "group is not a subscription type") + ErrInvalidInput = infraerrors.BadRequest("INVALID_INPUT", "at least one of resetDaily or resetWeekly must be true") ErrDailyLimitExceeded = infraerrors.TooManyRequests("DAILY_LIMIT_EXCEEDED", "daily usage limit exceeded") ErrWeeklyLimitExceeded = infraerrors.TooManyRequests("WEEKLY_LIMIT_EXCEEDED", "weekly usage limit exceeded") ErrMonthlyLimitExceeded = infraerrors.TooManyRequests("MONTHLY_LIMIT_EXCEEDED", "monthly usage limit exceeded") @@ -698,6 +699,9 @@ func (s *SubscriptionService) CheckAndActivateWindow(ctx context.Context, sub *U // AdminResetQuota manually resets the daily and/or weekly usage windows. // Uses startOfDay(now) as the new window start, matching automatic resets. func (s *SubscriptionService) AdminResetQuota(ctx context.Context, subscriptionID int64, resetDaily, resetWeekly bool) (*UserSubscription, error) { + if !resetDaily && !resetWeekly { + return nil, ErrInvalidInput + } sub, err := s.userSubRepo.GetByID(ctx, subscriptionID) if err != nil { return nil, err diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index 6779b22c..bb711b01 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -1151,6 +1151,7 @@ const handleResetQuota = (subscription: UserSubscription) => { const confirmResetQuota = async () => { if (!resettingSubscription.value) return + if (resettingQuota.value) return resettingQuota.value = true try { await adminAPI.subscriptions.resetQuota(resettingSubscription.value.id, { daily: true, weekly: true })