fix: 修复订阅窗口过期后进度条显示不正确的问题

问题:滑动窗口过期后(如昨天用满额度),前端仍显示历史数据(红色进度条100%、"即将重置")

解决:
- 后端返回数据前检查窗口是否过期,过期则清零展示数据
- 前端处理 window_start 为 null 的情况,显示"窗口未激活"
- 不影响实际的窗口激活逻辑,窗口仍从当天零点开始
This commit is contained in:
shaw
2025-12-23 10:38:15 +08:00
parent eb55947ec4
commit 5bbfbcdae9
5 changed files with 56 additions and 7 deletions

View File

@@ -335,24 +335,67 @@ func (s *SubscriptionService) GetActiveSubscription(ctx context.Context, userID,
// ListUserSubscriptions 获取用户的所有订阅 // ListUserSubscriptions 获取用户的所有订阅
func (s *SubscriptionService) ListUserSubscriptions(ctx context.Context, userID int64) ([]model.UserSubscription, error) { func (s *SubscriptionService) ListUserSubscriptions(ctx context.Context, userID int64) ([]model.UserSubscription, error) {
return s.userSubRepo.ListByUserID(ctx, userID) subs, err := s.userSubRepo.ListByUserID(ctx, userID)
if err != nil {
return nil, err
}
normalizeExpiredWindows(subs)
return subs, nil
} }
// ListActiveUserSubscriptions 获取用户的所有有效订阅 // ListActiveUserSubscriptions 获取用户的所有有效订阅
func (s *SubscriptionService) ListActiveUserSubscriptions(ctx context.Context, userID int64) ([]model.UserSubscription, error) { func (s *SubscriptionService) ListActiveUserSubscriptions(ctx context.Context, userID int64) ([]model.UserSubscription, error) {
return s.userSubRepo.ListActiveByUserID(ctx, userID) subs, err := s.userSubRepo.ListActiveByUserID(ctx, userID)
if err != nil {
return nil, err
}
normalizeExpiredWindows(subs)
return subs, nil
} }
// ListGroupSubscriptions 获取分组的所有订阅 // ListGroupSubscriptions 获取分组的所有订阅
func (s *SubscriptionService) ListGroupSubscriptions(ctx context.Context, groupID int64, page, pageSize int) ([]model.UserSubscription, *pagination.PaginationResult, error) { func (s *SubscriptionService) ListGroupSubscriptions(ctx context.Context, groupID int64, page, pageSize int) ([]model.UserSubscription, *pagination.PaginationResult, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize}
return s.userSubRepo.ListByGroupID(ctx, groupID, params) subs, pag, err := s.userSubRepo.ListByGroupID(ctx, groupID, params)
if err != nil {
return nil, nil, err
}
normalizeExpiredWindows(subs)
return subs, pag, nil
} }
// List 获取所有订阅(分页,支持筛选) // List 获取所有订阅(分页,支持筛选)
func (s *SubscriptionService) List(ctx context.Context, page, pageSize int, userID, groupID *int64, status string) ([]model.UserSubscription, *pagination.PaginationResult, error) { func (s *SubscriptionService) List(ctx context.Context, page, pageSize int, userID, groupID *int64, status string) ([]model.UserSubscription, *pagination.PaginationResult, error) {
params := pagination.PaginationParams{Page: page, PageSize: pageSize} params := pagination.PaginationParams{Page: page, PageSize: pageSize}
return s.userSubRepo.List(ctx, params, userID, groupID, status) subs, pag, err := s.userSubRepo.List(ctx, params, userID, groupID, status)
if err != nil {
return nil, nil, err
}
normalizeExpiredWindows(subs)
return subs, pag, nil
}
// normalizeExpiredWindows 将已过期窗口的数据清零(仅影响返回数据,不影响数据库)
// 这确保前端显示正确的当前窗口状态,而不是过期窗口的历史数据
func normalizeExpiredWindows(subs []model.UserSubscription) {
for i := range subs {
sub := &subs[i]
// 日窗口过期:清零展示数据
if sub.NeedsDailyReset() {
sub.DailyWindowStart = nil
sub.DailyUsageUSD = 0
}
// 周窗口过期:清零展示数据
if sub.NeedsWeeklyReset() {
sub.WeeklyWindowStart = nil
sub.WeeklyUsageUSD = 0
}
// 月窗口过期:清零展示数据
if sub.NeedsMonthlyReset() {
sub.MonthlyWindowStart = nil
sub.MonthlyUsageUSD = 0
}
}
} }
// startOfDay 返回给定时间所在日期的零点(保持原时区) // startOfDay 返回给定时间所在日期的零点(保持原时区)

View File

@@ -582,6 +582,7 @@ export default {
monthly: 'Monthly', monthly: 'Monthly',
noLimits: 'No limits configured', noLimits: 'No limits configured',
resetNow: 'Resetting soon', resetNow: 'Resetting soon',
windowNotActive: 'Window not active',
resetInMinutes: 'Resets in {minutes}m', resetInMinutes: 'Resets in {minutes}m',
resetInHoursMinutes: 'Resets in {hours}h {minutes}m', resetInHoursMinutes: 'Resets in {hours}h {minutes}m',
resetInDaysHours: 'Resets in {days}d {hours}h', resetInDaysHours: 'Resets in {days}d {hours}h',
@@ -1121,6 +1122,7 @@ export default {
daysRemaining: '{days} days remaining', daysRemaining: '{days} days remaining',
expiresOn: 'Expires on {date}', expiresOn: 'Expires on {date}',
resetIn: 'Resets in {time}', resetIn: 'Resets in {time}',
windowNotActive: 'Awaiting first use',
usageOf: '{used} of {limit}', usageOf: '{used} of {limit}',
}, },
} }

View File

@@ -639,6 +639,7 @@ export default {
monthly: '每月', monthly: '每月',
noLimits: '未配置限额', noLimits: '未配置限额',
resetNow: '即将重置', resetNow: '即将重置',
windowNotActive: '窗口未激活',
resetInMinutes: '{minutes} 分钟后重置', resetInMinutes: '{minutes} 分钟后重置',
resetInHoursMinutes: '{hours} 小时 {minutes} 分钟后重置', resetInHoursMinutes: '{hours} 小时 {minutes} 分钟后重置',
resetInDaysHours: '{days} 天 {hours} 小时后重置', resetInDaysHours: '{days} 天 {hours} 小时后重置',
@@ -1302,6 +1303,7 @@ export default {
daysRemaining: '剩余 {days} 天', daysRemaining: '剩余 {days} 天',
expiresOn: '{date} 到期', expiresOn: '{date} 到期',
resetIn: '{time} 后重置', resetIn: '{time} 后重置',
windowNotActive: '等待首次使用',
usageOf: '已用 {used} / {limit}', usageOf: '已用 {used} / {limit}',
}, },
} }

View File

@@ -592,6 +592,8 @@ const getProgressClass = (used: number, limit: number | null): string => {
// Format reset time based on window start and period type // Format reset time based on window start and period type
const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'monthly'): string => { const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'monthly'): string => {
if (!windowStart) return t('admin.subscriptions.windowNotActive')
const start = new Date(windowStart) const start = new Date(windowStart)
const now = new Date() const now = new Date()
@@ -610,7 +612,7 @@ const formatResetTime = (windowStart: string, period: 'daily' | 'weekly' | 'mont
} }
const diffMs = resetTime.getTime() - now.getTime() const diffMs = resetTime.getTime() - now.getTime()
if (diffMs <= 0) return t('admin.subscriptions.resetNow') if (diffMs <= 0) return t('admin.subscriptions.windowNotActive')
const diffSeconds = Math.floor(diffMs / 1000) const diffSeconds = Math.floor(diffMs / 1000)
const days = Math.floor(diffSeconds / 86400) const days = Math.floor(diffSeconds / 86400)

View File

@@ -229,14 +229,14 @@ function getExpirationClass(expiresAt: string): string {
} }
function formatResetTime(windowStart: string | null, windowHours: number): string { function formatResetTime(windowStart: string | null, windowHours: number): string {
if (!windowStart) return '--'; if (!windowStart) return t('userSubscriptions.windowNotActive');
const start = new Date(windowStart); const start = new Date(windowStart);
const end = new Date(start.getTime() + windowHours * 60 * 60 * 1000); const end = new Date(start.getTime() + windowHours * 60 * 60 * 1000);
const now = new Date(); const now = new Date();
const diff = end.getTime() - now.getTime(); const diff = end.getTime() - now.getTime();
if (diff <= 0) return 'Now'; if (diff <= 0) return t('userSubscriptions.windowNotActive');
const hours = Math.floor(diff / (1000 * 60 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));