Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||
)
|
||||
|
||||
const subscriptionDayDuration = 24 * time.Hour
|
||||
|
||||
type UserSubscription struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
GroupID int64
|
||||
|
||||
StartsAt time.Time
|
||||
ExpiresAt time.Time
|
||||
Status string
|
||||
|
||||
DailyWindowStart *time.Time
|
||||
WeeklyWindowStart *time.Time
|
||||
MonthlyWindowStart *time.Time
|
||||
|
||||
DailyUsageUSD float64
|
||||
WeeklyUsageUSD float64
|
||||
MonthlyUsageUSD float64
|
||||
|
||||
AssignedBy *int64
|
||||
AssignedAt time.Time
|
||||
Notes string
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
|
||||
User *User
|
||||
Group *Group
|
||||
AssignedByUser *User
|
||||
}
|
||||
|
||||
func (s *UserSubscription) IsActive() bool {
|
||||
return s.Status == SubscriptionStatusActive && time.Now().Before(s.ExpiresAt)
|
||||
}
|
||||
|
||||
func (s *UserSubscription) IsExpired() bool {
|
||||
return time.Now().After(s.ExpiresAt)
|
||||
}
|
||||
|
||||
func (s *UserSubscription) DaysRemaining() int {
|
||||
return s.daysRemainingAt(time.Now())
|
||||
}
|
||||
|
||||
func (s *UserSubscription) daysRemainingAt(now time.Time) int {
|
||||
remaining := s.ExpiresAt.Sub(now)
|
||||
if remaining <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
days := int(remaining / subscriptionDayDuration)
|
||||
if remaining%subscriptionDayDuration != 0 {
|
||||
days++
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
func (s *UserSubscription) IsWindowActivated() bool {
|
||||
return s.DailyWindowStart != nil || s.WeeklyWindowStart != nil || s.MonthlyWindowStart != nil
|
||||
}
|
||||
|
||||
func (s *UserSubscription) HasOneTimeDailyQuota() bool {
|
||||
if s == nil || s.StartsAt.IsZero() || s.ExpiresAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
return !s.ExpiresAt.After(s.StartsAt.AddDate(0, 0, 1))
|
||||
}
|
||||
|
||||
func (s *UserSubscription) NeedsDailyReset() bool {
|
||||
return s.NeedsDailyResetAt(time.Now())
|
||||
}
|
||||
|
||||
func (s *UserSubscription) NeedsDailyResetAt(now time.Time) bool {
|
||||
_, ok := s.automaticDailyWindowStartAt(now)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *UserSubscription) NeedsWeeklyReset() bool {
|
||||
return s.NeedsWeeklyResetAt(time.Now())
|
||||
}
|
||||
|
||||
func (s *UserSubscription) NeedsWeeklyResetAt(now time.Time) bool {
|
||||
if s.WeeklyWindowStart == nil {
|
||||
return false
|
||||
}
|
||||
return !now.Before(s.WeeklyWindowStart.Add(7 * 24 * time.Hour))
|
||||
}
|
||||
|
||||
func (s *UserSubscription) NeedsMonthlyReset() bool {
|
||||
return s.NeedsMonthlyResetAt(time.Now())
|
||||
}
|
||||
|
||||
func (s *UserSubscription) NeedsMonthlyResetAt(now time.Time) bool {
|
||||
if s.MonthlyWindowStart == nil {
|
||||
return false
|
||||
}
|
||||
return !now.Before(s.MonthlyWindowStart.Add(30 * 24 * time.Hour))
|
||||
}
|
||||
|
||||
func (s *UserSubscription) canAutomaticallyResetDailyAt(now time.Time) bool {
|
||||
_, ok := s.automaticDailyWindowStartAt(now)
|
||||
return ok
|
||||
}
|
||||
|
||||
// automaticDailyWindowStartAt 计算日窗口按“配置时区日历日”对齐后的当前窗口起点。
|
||||
// 日额度固定在每天 0 点刷新(与周/月的期限对齐滚动窗口语义不同),因此只要持久化
|
||||
// 的窗口起点落在更早的日历日,就允许推进到今天 0 点。手动重置、激活等写入的任何
|
||||
// 非 0 点锚点都会在下一个 0 点被拉回日历日边界,不会永久漂移刷新时刻。
|
||||
func (s *UserSubscription) automaticDailyWindowStartAt(now time.Time) (time.Time, bool) {
|
||||
if s.DailyWindowStart == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
if s.HasOneTimeDailyQuota() {
|
||||
return time.Time{}, false
|
||||
}
|
||||
today := timezone.StartOfDay(now)
|
||||
if !today.After(timezone.StartOfDay(*s.DailyWindowStart)) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return today, true
|
||||
}
|
||||
|
||||
func (s *UserSubscription) canAutomaticallyResetWeeklyAt(now time.Time) bool {
|
||||
_, ok := s.automaticWindowStartAt(s.WeeklyWindowStart, 7*24*time.Hour, now)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *UserSubscription) canAutomaticallyResetMonthlyAt(now time.Time) bool {
|
||||
_, ok := s.automaticWindowStartAt(s.MonthlyWindowStart, 30*24*time.Hour, now)
|
||||
return ok
|
||||
}
|
||||
|
||||
// automaticWindowStartAt 计算周/月窗口(期限对齐滚动窗口)的当前窗口起点。
|
||||
// 窗口从锚点按整数个 period 步进,且不越过订阅到期时间,避免最后一个不完整
|
||||
// 周期重复发放额度(issue #5051)。日窗口不走此函数,见 automaticDailyWindowStartAt。
|
||||
func (s *UserSubscription) automaticWindowStartAt(previous *time.Time, period time.Duration, now time.Time) (time.Time, bool) {
|
||||
if previous == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
anchor := *previous
|
||||
// Older subscriptions initialized their first windows at midnight on their
|
||||
// start date. Only that initial value is unambiguous; later midnight anchors
|
||||
// may be manual resets and must remain authoritative.
|
||||
legacyAnchor := startOfDay(s.StartsAt)
|
||||
if legacyAnchor.Before(s.StartsAt) && anchor.Equal(legacyAnchor) {
|
||||
anchor = s.StartsAt
|
||||
}
|
||||
next := anchor.Add(period)
|
||||
if now.Before(next) || !next.Before(s.ExpiresAt) {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
periods := now.Sub(anchor) / period
|
||||
lastPeriodBeforeExpiry := (s.ExpiresAt.Sub(anchor) - 1) / period
|
||||
if periods > lastPeriodBeforeExpiry {
|
||||
periods = lastPeriodBeforeExpiry
|
||||
}
|
||||
return anchor.Add(periods * period), true
|
||||
}
|
||||
|
||||
func (s *UserSubscription) DailyResetTime() *time.Time {
|
||||
if s.DailyWindowStart == nil {
|
||||
return nil
|
||||
}
|
||||
if s.HasOneTimeDailyQuota() {
|
||||
t := s.ExpiresAt
|
||||
return &t
|
||||
}
|
||||
// 日窗口按日历日对齐:下次刷新固定在窗口起点所在日的次日 0 点。
|
||||
t := timezone.StartOfDay(*s.DailyWindowStart).AddDate(0, 0, 1)
|
||||
return &t
|
||||
}
|
||||
|
||||
func (s *UserSubscription) WeeklyResetTime() *time.Time {
|
||||
if s.WeeklyWindowStart == nil {
|
||||
return nil
|
||||
}
|
||||
t := s.WeeklyWindowStart.Add(7 * 24 * time.Hour)
|
||||
return &t
|
||||
}
|
||||
|
||||
func (s *UserSubscription) MonthlyResetTime() *time.Time {
|
||||
if s.MonthlyWindowStart == nil {
|
||||
return nil
|
||||
}
|
||||
t := s.MonthlyWindowStart.Add(30 * 24 * time.Hour)
|
||||
return &t
|
||||
}
|
||||
|
||||
func (s *UserSubscription) CheckDailyLimit(group *Group, additionalCost float64) bool {
|
||||
if !group.HasDailyLimit() {
|
||||
return true
|
||||
}
|
||||
return s.DailyUsageUSD+additionalCost <= *group.DailyLimitUSD
|
||||
}
|
||||
|
||||
func (s *UserSubscription) CheckWeeklyLimit(group *Group, additionalCost float64) bool {
|
||||
if !group.HasWeeklyLimit() {
|
||||
return true
|
||||
}
|
||||
return s.WeeklyUsageUSD+additionalCost <= *group.WeeklyLimitUSD
|
||||
}
|
||||
|
||||
func (s *UserSubscription) CheckMonthlyLimit(group *Group, additionalCost float64) bool {
|
||||
if !group.HasMonthlyLimit() {
|
||||
return true
|
||||
}
|
||||
return s.MonthlyUsageUSD+additionalCost <= *group.MonthlyLimitUSD
|
||||
}
|
||||
|
||||
func (s *UserSubscription) CheckAllLimits(group *Group, additionalCost float64) (daily, weekly, monthly bool) {
|
||||
daily = s.CheckDailyLimit(group, additionalCost)
|
||||
weekly = s.CheckWeeklyLimit(group, additionalCost)
|
||||
monthly = s.CheckMonthlyLimit(group, additionalCost)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user