From de18bce9aa795a295dbc6f9c86ddd82ece2f9cb8 Mon Sep 17 00:00:00 2001 From: haruka <1628615876@qq.com> Date: Tue, 10 Mar 2026 11:21:11 +0800 Subject: [PATCH 01/24] feat: add admin reset subscription quota endpoint and UI - Add AdminResetQuota service method to reset daily/weekly usage windows - Add POST /api/v1/admin/subscriptions/:id/reset-quota handler and route - Add resetQuota API function in frontend subscriptions client - Add reset quota button, confirmation dialog, and handlers in SubscriptionsView - Add i18n keys for reset quota feature in zh and en locales Co-Authored-By: Claude Sonnet 4.6 --- .../handler/admin/subscription_handler.go | 31 +++++++++++++ backend/internal/server/routes/admin.go | 1 + .../internal/service/subscription_service.go | 31 +++++++++++++ frontend/src/api/admin/subscriptions.ts | 18 ++++++++ frontend/src/i18n/locales/en.ts | 5 +++ frontend/src/i18n/locales/zh.ts | 5 +++ .../src/views/admin/SubscriptionsView.vue | 44 +++++++++++++++++++ 7 files changed, 135 insertions(+) diff --git a/backend/internal/handler/admin/subscription_handler.go b/backend/internal/handler/admin/subscription_handler.go index e5b6db13..d6073551 100644 --- a/backend/internal/handler/admin/subscription_handler.go +++ b/backend/internal/handler/admin/subscription_handler.go @@ -216,6 +216,37 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) { }) } +// ResetSubscriptionQuotaRequest represents the reset quota request +type ResetSubscriptionQuotaRequest struct { + Daily bool `json:"daily"` + Weekly bool `json:"weekly"` +} + +// ResetQuota resets daily and/or weekly usage for a subscription. +// POST /api/v1/admin/subscriptions/:id/reset-quota +func (h *SubscriptionHandler) ResetQuota(c *gin.Context) { + subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid subscription ID") + return + } + var req ResetSubscriptionQuotaRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + if !req.Daily && !req.Weekly { + response.BadRequest(c, "At least one of 'daily' or 'weekly' must be true") + return + } + sub, err := h.subscriptionService.AdminResetQuota(c.Request.Context(), subscriptionID, req.Daily, req.Weekly) + if err != nil { + response.ErrorFrom(c, err) + return + } + response.Success(c, dto.UserSubscriptionFromServiceAdmin(sub)) +} + // Revoke handles revoking a subscription // DELETE /api/v1/admin/subscriptions/:id func (h *SubscriptionHandler) Revoke(c *gin.Context) { diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 5f4a0784..642bae5e 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -453,6 +453,7 @@ func registerSubscriptionRoutes(admin *gin.RouterGroup, h *handler.Handlers) { subscriptions.POST("/assign", h.Admin.Subscription.Assign) subscriptions.POST("/bulk-assign", h.Admin.Subscription.BulkAssign) subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend) + subscriptions.POST("/:id/reset-quota", h.Admin.Subscription.ResetQuota) subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke) } diff --git a/backend/internal/service/subscription_service.go b/backend/internal/service/subscription_service.go index 57e04266..ec0c878e 100644 --- a/backend/internal/service/subscription_service.go +++ b/backend/internal/service/subscription_service.go @@ -695,6 +695,37 @@ func (s *SubscriptionService) CheckAndActivateWindow(ctx context.Context, sub *U return s.userSubRepo.ActivateWindows(ctx, sub.ID, windowStart) } +// 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) { + sub, err := s.userSubRepo.GetByID(ctx, subscriptionID) + if err != nil { + return nil, err + } + windowStart := startOfDay(time.Now()) + if resetDaily { + if err := s.userSubRepo.ResetDailyUsage(ctx, sub.ID, windowStart); err != nil { + return nil, err + } + sub.DailyWindowStart = &windowStart + sub.DailyUsageUSD = 0 + } + if resetWeekly { + if err := s.userSubRepo.ResetWeeklyUsage(ctx, sub.ID, windowStart); err != nil { + return nil, err + } + sub.WeeklyWindowStart = &windowStart + sub.WeeklyUsageUSD = 0 + } + // Invalidate caches, same as CheckAndResetWindows + s.InvalidateSubCache(sub.UserID, sub.GroupID) + if s.billingCacheService != nil { + _ = s.billingCacheService.InvalidateSubscription(ctx, sub.UserID, sub.GroupID) + } + // Return the refreshed subscription from DB + return s.userSubRepo.GetByID(ctx, subscriptionID) +} + // CheckAndResetWindows 检查并重置过期的窗口 func (s *SubscriptionService) CheckAndResetWindows(ctx context.Context, sub *UserSubscription) error { // 使用当天零点作为新窗口起始时间 diff --git a/frontend/src/api/admin/subscriptions.ts b/frontend/src/api/admin/subscriptions.ts index 9f21056f..d06e0774 100644 --- a/frontend/src/api/admin/subscriptions.ts +++ b/frontend/src/api/admin/subscriptions.ts @@ -120,6 +120,23 @@ export async function revoke(id: number): Promise<{ message: string }> { return data } +/** + * Reset daily and/or weekly usage quota for a subscription + * @param id - Subscription ID + * @param options - Which windows to reset + * @returns Updated subscription + */ +export async function resetQuota( + id: number, + options: { daily: boolean; weekly: boolean } +): Promise { + const { data } = await apiClient.post( + `/admin/subscriptions/${id}/reset-quota`, + options + ) + return data +} + /** * List subscriptions by group * @param groupId - Group ID @@ -170,6 +187,7 @@ export const subscriptionsAPI = { bulkAssign, extend, revoke, + resetQuota, listByGroup, listByUser } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 9832ed85..1d83db23 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1570,6 +1570,11 @@ export default { adjust: 'Adjust', adjusting: 'Adjusting...', revoke: 'Revoke', + resetQuota: 'Reset Quota', + resetQuotaTitle: 'Reset Usage Quota', + resetQuotaConfirm: "Reset the daily and weekly usage quota for '{user}'? Usage will be zeroed and windows restarted from today.", + quotaResetSuccess: 'Quota reset successfully', + failedToResetQuota: 'Failed to reset quota', noSubscriptionsYet: 'No subscriptions yet', assignFirstSubscription: 'Assign a subscription to get started.', subscriptionAssigned: 'Subscription assigned successfully', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 7ad89848..aefe4799 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1658,6 +1658,11 @@ export default { adjust: '调整', adjusting: '调整中...', revoke: '撤销', + resetQuota: '重置配额', + resetQuotaTitle: '重置用量配额', + resetQuotaConfirm: "确定要重置 '{user}' 的每日和每周用量配额吗?用量将归零并从今天开始重新计算。", + quotaResetSuccess: '配额重置成功', + failedToResetQuota: '重置配额失败', noSubscriptionsYet: '暂无订阅', assignFirstSubscription: '分配一个订阅以开始使用。', subscriptionAssigned: '订阅分配成功', diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index eb2b40d5..9e5a7bae 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -370,6 +370,14 @@ {{ t('admin.subscriptions.adjust') }} +