package usage import ( "context" "github.com/wangjia/pangolin/server/internal/apierr" ) // defaultFreeDailyMinutes is the口径 fallback when the free plan row has a NULL // daily_minutes (should not happen given the seed, but we stay safe). const defaultFreeDailyMinutes = 10 // CheckFreeConnect enforces the free-plan connect gate and returns the user's // remaining minutes for today (UTC). The quota is account-wide (shared across // all devices). // // Rules (免费版累加式看广告加时): // - plan.ad_gate == false (pro/team): not minute-gated. Returns Unlimited // when daily_minutes is NULL, otherwise the remaining minutes for the day. // - plan.ad_gate == true (free): the day's allowance = daily_minutes (free=10) // + ad_bonus_minutes (accumulated by watching rewarded ads). No ad is // required to connect at all — the base 10 minutes are always available. // Returns allowance − minutes_used; QUOTA_EXHAUSTED when that reaches 0. func (svc *Service) CheckFreeConnect(ctx context.Context, userID int64) (remainingMinutes int, apiErr *apierr.Error) { plan, err := svc.store.EffectivePlan(ctx, userID) if err != nil { return 0, apierr.ErrInternal } limit := defaultFreeDailyMinutes if plan.DailyMinutes.Valid { limit = int(plan.DailyMinutes.Int64) } // Paid plans (no ad gate). if !plan.AdGate { if !plan.DailyMinutes.Valid { return Unlimited, nil } day, err := svc.store.GetDay(ctx, userID, utcToday()) if err != nil { return 0, apierr.ErrInternal } remaining := limit if day != nil { remaining = limit - day.MinutesUsed } if remaining < 0 { remaining = 0 } return remaining, nil } // Free plan: allowance = base daily + ad bonus; remaining = allowance − used. day, err := svc.store.GetDay(ctx, userID, utcToday()) if err != nil { return 0, apierr.ErrInternal } allowance := limit used := 0 if day != nil { allowance = limit + day.AdBonusMinutes used = day.MinutesUsed } if used >= allowance { return 0, apierr.ErrQuotaExhausted } return allowance - used, nil }