mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-03-30 02:46:51 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
// 使用当天零点作为新窗口起始时间
|
||||
|
||||
@@ -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<UserSubscription> {
|
||||
const { data } = await apiClient.post<UserSubscription>(
|
||||
`/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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1658,6 +1658,11 @@ export default {
|
||||
adjust: '调整',
|
||||
adjusting: '调整中...',
|
||||
revoke: '撤销',
|
||||
resetQuota: '重置配额',
|
||||
resetQuotaTitle: '重置用量配额',
|
||||
resetQuotaConfirm: "确定要重置 '{user}' 的每日和每周用量配额吗?用量将归零并从今天开始重新计算。",
|
||||
quotaResetSuccess: '配额重置成功',
|
||||
failedToResetQuota: '重置配额失败',
|
||||
noSubscriptionsYet: '暂无订阅',
|
||||
assignFirstSubscription: '分配一个订阅以开始使用。',
|
||||
subscriptionAssigned: '订阅分配成功',
|
||||
|
||||
@@ -370,6 +370,14 @@
|
||||
<Icon name="calendar" size="sm" />
|
||||
<span class="text-xs">{{ t('admin.subscriptions.adjust') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="row.status === 'active'"
|
||||
@click="handleResetQuota(row)"
|
||||
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-orange-50 hover:text-orange-600 dark:hover:bg-orange-900/20 dark:hover:text-orange-400"
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
<span class="text-xs">{{ t('admin.subscriptions.resetQuota') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="row.status === 'active'"
|
||||
@click="handleRevoke(row)"
|
||||
@@ -618,6 +626,17 @@
|
||||
@confirm="confirmRevoke"
|
||||
@cancel="showRevokeDialog = false"
|
||||
/>
|
||||
|
||||
<!-- Reset Quota Confirmation Dialog -->
|
||||
<ConfirmDialog
|
||||
:show="showResetQuotaConfirm"
|
||||
:title="t('admin.subscriptions.resetQuotaTitle')"
|
||||
:message="t('admin.subscriptions.resetQuotaConfirm', { user: resettingSubscription?.user?.email })"
|
||||
:confirm-text="t('admin.subscriptions.resetQuota')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
@confirm="confirmResetQuota"
|
||||
@cancel="showResetQuotaConfirm = false"
|
||||
/>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
@@ -812,7 +831,10 @@ const pagination = reactive({
|
||||
const showAssignModal = ref(false)
|
||||
const showExtendModal = ref(false)
|
||||
const showRevokeDialog = ref(false)
|
||||
const showResetQuotaConfirm = ref(false)
|
||||
const submitting = ref(false)
|
||||
const resettingSubscription = ref<UserSubscription | null>(null)
|
||||
const resettingQuota = ref(false)
|
||||
const extendingSubscription = ref<UserSubscription | null>(null)
|
||||
const revokingSubscription = ref<UserSubscription | null>(null)
|
||||
|
||||
@@ -1121,6 +1143,28 @@ const confirmRevoke = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetQuota = (subscription: UserSubscription) => {
|
||||
resettingSubscription.value = subscription
|
||||
showResetQuotaConfirm.value = true
|
||||
}
|
||||
|
||||
const confirmResetQuota = async () => {
|
||||
if (!resettingSubscription.value) return
|
||||
resettingQuota.value = true
|
||||
try {
|
||||
await adminAPI.subscriptions.resetQuota(resettingSubscription.value.id, { daily: true, weekly: true })
|
||||
appStore.showSuccess(t('admin.subscriptions.quotaResetSuccess'))
|
||||
showResetQuotaConfirm.value = false
|
||||
resettingSubscription.value = null
|
||||
await loadSubscriptions()
|
||||
} catch (error: any) {
|
||||
appStore.showError(error.response?.data?.detail || t('admin.subscriptions.failedToResetQuota'))
|
||||
console.error('Error resetting quota:', error)
|
||||
} finally {
|
||||
resettingQuota.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
const getDaysRemaining = (expiresAt: string): number | null => {
|
||||
const now = new Date()
|
||||
|
||||
Reference in New Issue
Block a user