--- base/backend/internal/service/user_subscription.go	2026-05-03 12:25:18
+++ new/backend/internal/service/user_subscription.go	2026-05-03 12:25:56
@@ -6,6 +6,38 @@
 	"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
 )
 
+func anchoredMonthlyWindowStart(startsAt, expiresAt, now time.Time) time.Time {
+	if startsAt.IsZero() {
+		return time.Time{}
+	}
+	if now.Before(startsAt) || now.Equal(startsAt) {
+		return startsAt
+	}
+	current := startsAt
+	for {
+		next := current.AddDate(0, 1, 0)
+		if !expiresAt.IsZero() && !next.Before(expiresAt) {
+			break
+		}
+		if next.After(now) {
+			break
+		}
+		current = next
+	}
+	return current
+}
+
+func anchoredMonthlyWindowEnd(windowStart, expiresAt time.Time) time.Time {
+	if windowStart.IsZero() {
+		return expiresAt
+	}
+	windowEnd := windowStart.AddDate(0, 1, 0)
+	if !expiresAt.IsZero() && windowEnd.After(expiresAt) {
+		return expiresAt
+	}
+	return windowEnd
+}
+
 type UserSubscription struct {
 	ID      int64
 	UserID  int64
@@ -74,8 +106,14 @@
 	if s.MonthlyWindowStart == nil {
 		return false
 	}
-	monthStart := timezone.StartOfMonth(timezone.Now())
-	return s.MonthlyWindowStart.Before(monthStart)
+	if s.StartsAt.IsZero() {
+		return false
+	}
+	currentWindowStart := anchoredMonthlyWindowStart(s.StartsAt, s.ExpiresAt, timezone.Now())
+	if currentWindowStart.IsZero() {
+		return false
+	}
+	return !s.MonthlyWindowStart.Equal(currentWindowStart)
 }
 
 func (s *UserSubscription) DailyResetTime() *time.Time {
@@ -100,8 +138,7 @@
 	if s.MonthlyWindowStart == nil {
 		return nil
 	}
-	base := timezone.StartOfMonth(*s.MonthlyWindowStart)
-	t := base.AddDate(0, 1, 0)
+	t := anchoredMonthlyWindowEnd(*s.MonthlyWindowStart, s.ExpiresAt)
 	return &t
 }
 
--- base/backend/internal/service/subscription_service.go	2026-05-03 12:25:30
+++ new/backend/internal/service/subscription_service.go	2026-05-03 12:25:56
@@ -999,11 +999,36 @@
 	}
 }
 
-func currentSubscriptionWindowStarts() (dailyStart, weeklyStart, monthlyStart time.Time) {
+func subscriptionMonthlyCycleStart(sub *UserSubscription) time.Time {
+	start, _ := subscriptionMonthlyCycleBounds(sub)
+	return start
+}
+
+func subscriptionMonthlyCycleEnd(sub *UserSubscription) time.Time {
+	_, end := subscriptionMonthlyCycleBounds(sub)
+	return end
+}
+
+func subscriptionMonthlyCycleBounds(sub *UserSubscription) (time.Time, time.Time) {
+	if sub != nil && !sub.StartsAt.IsZero() {
+		windowStart := anchoredMonthlyWindowStart(sub.StartsAt, sub.ExpiresAt, timezone.Now())
+		if !windowStart.IsZero() {
+			return windowStart, anchoredMonthlyWindowEnd(windowStart, sub.ExpiresAt)
+		}
+	}
+	if sub != nil && sub.MonthlyWindowStart != nil {
+		windowStart := *sub.MonthlyWindowStart
+		return windowStart, anchoredMonthlyWindowEnd(windowStart, sub.ExpiresAt)
+	}
 	now := timezone.Now()
-	return timezone.StartOfDay(now), timezone.StartOfWeek(now), timezone.StartOfMonth(now)
+	return now, now
 }
 
+func subscriptionWindowStarts(sub *UserSubscription) (dailyStart, weeklyStart, monthlyStart time.Time) {
+	now := timezone.Now()
+	return timezone.StartOfDay(now), timezone.StartOfWeek(now), subscriptionMonthlyCycleStart(sub)
+}
+
 // CheckAndActivateWindow 检查并激活窗口（首次使用时）
 func (s *SubscriptionService) CheckAndActivateWindow(ctx context.Context, sub *UserSubscription) error {
 	if sub.IsWindowActivated() {
@@ -1016,7 +1041,7 @@
 		return nil
 	}
 
-	dailyStart, weeklyStart, monthlyStart := currentSubscriptionWindowStarts()
+	dailyStart, weeklyStart, monthlyStart := subscriptionWindowStarts(sub)
 	before := captureSubscriptionWindowSnapshot(sub)
 	subscriptionInfo(ctx, "订阅窗口激活前",
 		mergeSubscriptionLogFields(
@@ -1081,7 +1106,7 @@
 	if err != nil {
 		return nil, err
 	}
-	dailyStart, weeklyStart, monthlyStart := currentSubscriptionWindowStarts()
+	dailyStart, weeklyStart, monthlyStart := subscriptionWindowStarts(sub)
 	subscriptionInfo(ctx, "手动重置订阅额度前",
 		mergeSubscriptionLogFields(
 			subscriptionIdentityFields(sub),
@@ -1168,7 +1193,7 @@
 
 // CheckAndResetWindows 检查并重置过期的窗口
 func (s *SubscriptionService) CheckAndResetWindows(ctx context.Context, sub *UserSubscription) error {
-	dailyStart, weeklyStart, monthlyStart := currentSubscriptionWindowStarts()
+	dailyStart, weeklyStart, monthlyStart := subscriptionWindowStarts(sub)
 	needsInvalidateCache := false
 	needDailyReset := sub.NeedsDailyReset()
 	needWeeklyReset := sub.NeedsWeeklyReset()
@@ -1489,13 +1514,11 @@
 	if sub == nil {
 		return time.Time{}, false
 	}
-	if !sub.StartsAt.IsZero() {
-		return sub.StartsAt, true
+	windowStart, _ := subscriptionMonthlyCycleBounds(sub)
+	if windowStart.IsZero() {
+		return time.Time{}, false
 	}
-	if sub.MonthlyWindowStart != nil {
-		return *sub.MonthlyWindowStart, true
-	}
-	return time.Time{}, false
+	return windowStart, true
 }
 
 func (s *SubscriptionService) buildSubscriptionCycleMonthlyProgress(ctx context.Context, sub *UserSubscription, group *Group, fallback *UsageWindowProgress) (*UsageWindowProgress, error) {
@@ -1503,18 +1526,15 @@
 		return fallback, nil
 	}
 	windowStart, ok := subscriptionMonthlyProgressWindowStart(sub)
-	if !ok || sub.ExpiresAt.IsZero() {
+	if !ok {
 		return fallback, nil
 	}
+	_, windowEnd := subscriptionMonthlyCycleBounds(sub)
+	if windowEnd.IsZero() {
+		return fallback, nil
+	}
 
 	usedUSD := sub.MonthlyUsageUSD
-	if s.entClient != nil {
-		sum, err := s.sumSubscriptionActualCostInRange(ctx, sub.ID, windowStart, sub.ExpiresAt)
-		if err != nil {
-			return nil, err
-		}
-		usedUSD = sum
-	}
 
 	limit := *group.MonthlyLimitUSD
 	remainingUSD := limit - usedUSD
@@ -1528,7 +1548,7 @@
 			percentage = 100
 		}
 	}
-	resetsInSeconds := int64(time.Until(sub.ExpiresAt).Seconds())
+	resetsInSeconds := int64(time.Until(windowEnd).Seconds())
 	if resetsInSeconds < 0 {
 		resetsInSeconds = 0
 	}
@@ -1542,7 +1562,7 @@
 	progress.RemainingUSD = remainingUSD
 	progress.Percentage = percentage
 	progress.WindowStart = windowStart
-	progress.ResetsAt = sub.ExpiresAt
+	progress.ResetsAt = windowEnd
 	progress.ResetsInSeconds = resetsInSeconds
 	return progress, nil
 }
@@ -1627,14 +1647,15 @@
 
 	// 月进度
 	if group.HasMonthlyLimit() && sub.MonthlyWindowStart != nil {
+		windowStart, resetsAt := subscriptionMonthlyCycleBounds(sub)
+		usedUSD := sub.MonthlyUsageUSD
 		limit := *group.MonthlyLimitUSD
-		resetsAt := *sub.MonthlyResetTime()
 		progress.Monthly = &UsageWindowProgress{
 			LimitUSD:        limit,
-			UsedUSD:         sub.MonthlyUsageUSD,
-			RemainingUSD:    limit - sub.MonthlyUsageUSD,
-			Percentage:      (sub.MonthlyUsageUSD / limit) * 100,
-			WindowStart:     *sub.MonthlyWindowStart,
+			UsedUSD:         usedUSD,
+			RemainingUSD:    limit - usedUSD,
+			Percentage:      (usedUSD / limit) * 100,
+			WindowStart:     windowStart,
 			ResetsAt:        resetsAt,
 			ResetsInSeconds: int64(time.Until(resetsAt).Seconds()),
 		}
