mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(account): normalize fixed quota windows on edit
This commit is contained in:
@@ -2036,6 +2036,60 @@ func ComputeQuotaResetAt(extra map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeFixedQuotaWindows aligns preserved quota usage with the active fixed reset window.
|
||||
//
|
||||
// Editing an existing account can switch a daily/weekly quota from rolling to fixed reset
|
||||
// while preserving quota_*_used and quota_*_start. If the preserved start belongs to the
|
||||
// old rolling window, response mapping treats the usage as expired and the dashboard shows
|
||||
// 0 until the next reset. Normalize those stale starts before persisting the edited account.
|
||||
func NormalizeFixedQuotaWindows(extra map[string]any) {
|
||||
if extra == nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
tzName, _ := extra["quota_reset_timezone"].(string)
|
||||
if tzName == "" {
|
||||
tzName = "UTC"
|
||||
}
|
||||
tz, err := time.LoadLocation(tzName)
|
||||
if err != nil {
|
||||
tz = time.UTC
|
||||
}
|
||||
|
||||
if mode, _ := extra["quota_daily_reset_mode"].(string); mode == "fixed" && parseExtraFloat64(extra["quota_daily_limit"]) > 0 {
|
||||
hour := int(parseExtraFloat64(extra["quota_daily_reset_hour"]))
|
||||
if hour < 0 || hour > 23 {
|
||||
hour = 0
|
||||
}
|
||||
lastReset := lastFixedDailyReset(hour, tz, now)
|
||||
start := parseExtraTime(extra["quota_daily_start"])
|
||||
if start.IsZero() || start.Before(lastReset) {
|
||||
extra["quota_daily_used"] = 0.0
|
||||
extra["quota_daily_start"] = lastReset.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
if mode, _ := extra["quota_weekly_reset_mode"].(string); mode == "fixed" && parseExtraFloat64(extra["quota_weekly_limit"]) > 0 {
|
||||
day := 1
|
||||
if rawDay, ok := extra["quota_weekly_reset_day"]; ok {
|
||||
day = int(parseExtraFloat64(rawDay))
|
||||
}
|
||||
if day < 0 || day > 6 {
|
||||
day = 1
|
||||
}
|
||||
hour := int(parseExtraFloat64(extra["quota_weekly_reset_hour"]))
|
||||
if hour < 0 || hour > 23 {
|
||||
hour = 0
|
||||
}
|
||||
lastReset := lastFixedWeeklyReset(day, hour, tz, now)
|
||||
start := parseExtraTime(extra["quota_weekly_start"])
|
||||
if start.IsZero() || start.Before(lastReset) {
|
||||
extra["quota_weekly_used"] = 0.0
|
||||
extra["quota_weekly_start"] = lastReset.UTC().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateQuotaResetConfig 校验配额固定重置时间配置的合法性
|
||||
func ValidateQuotaResetConfig(extra map[string]any) error {
|
||||
if extra == nil {
|
||||
@@ -2364,6 +2418,18 @@ func parseExtraFloat64(value any) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseExtraTime(value any) time.Time {
|
||||
if s, ok := value.(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
|
||||
return t
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// parseExtraInt 从 extra 字段解析 int 值
|
||||
// ParseExtraInt 从 extra 字段的 any 值解析为 int。
|
||||
// 支持 int, int64, float64, json.Number, string 类型,无法解析时返回 0。
|
||||
|
||||
@@ -398,6 +398,51 @@ func TestValidateQuotaResetConfig_BoundaryValues(t *testing.T) {
|
||||
assert.NoError(t, ValidateQuotaResetConfig(extra2))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NormalizeFixedQuotaWindows
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNormalizeFixedQuotaWindows_ClearsExpiredWeeklyWindow(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
daysSinceMonday := (int(now.Weekday()) + 6) % 7
|
||||
currentWeekStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -daysSinceMonday)
|
||||
staleStart := currentWeekStart.Add(-24 * time.Hour)
|
||||
extra := map[string]any{
|
||||
"quota_weekly_limit": 500.0,
|
||||
"quota_weekly_used": 76.0,
|
||||
"quota_weekly_start": staleStart.Format(time.RFC3339),
|
||||
"quota_weekly_reset_mode": "fixed",
|
||||
"quota_weekly_reset_day": float64(1),
|
||||
"quota_weekly_reset_hour": float64(0),
|
||||
"quota_reset_timezone": "UTC",
|
||||
}
|
||||
|
||||
NormalizeFixedQuotaWindows(extra)
|
||||
|
||||
assert.Equal(t, 0.0, extra["quota_weekly_used"])
|
||||
assert.Equal(t, currentWeekStart.Format(time.RFC3339), extra["quota_weekly_start"])
|
||||
}
|
||||
|
||||
func TestNormalizeFixedQuotaWindows_KeepsActiveWeeklyWindow(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
daysSinceMonday := (int(now.Weekday()) + 6) % 7
|
||||
currentWeekStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -daysSinceMonday)
|
||||
extra := map[string]any{
|
||||
"quota_weekly_limit": 500.0,
|
||||
"quota_weekly_used": 76.0,
|
||||
"quota_weekly_start": currentWeekStart.Format(time.RFC3339),
|
||||
"quota_weekly_reset_mode": "fixed",
|
||||
"quota_weekly_reset_day": float64(1),
|
||||
"quota_weekly_reset_hour": float64(0),
|
||||
"quota_reset_timezone": "UTC",
|
||||
}
|
||||
|
||||
NormalizeFixedQuotaWindows(extra)
|
||||
|
||||
assert.Equal(t, 76.0, extra["quota_weekly_used"])
|
||||
assert.Equal(t, currentWeekStart.Format(time.RFC3339), extra["quota_weekly_start"])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ComputeQuotaResetAt
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2498,6 +2498,7 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
|
||||
return nil, err
|
||||
}
|
||||
ComputeQuotaResetAt(account.Extra)
|
||||
NormalizeFixedQuotaWindows(account.Extra)
|
||||
}
|
||||
if input.ExpiresAt != nil && *input.ExpiresAt > 0 {
|
||||
expiresAt := time.Unix(*input.ExpiresAt, 0)
|
||||
@@ -2606,6 +2607,7 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
||||
return nil, err
|
||||
}
|
||||
ComputeQuotaResetAt(account.Extra)
|
||||
NormalizeFixedQuotaWindows(account.Extra)
|
||||
}
|
||||
if input.ProxyID != nil {
|
||||
// 0 表示清除代理(前端发送 0 而不是 null 来表达清除意图)
|
||||
|
||||
@@ -153,3 +153,42 @@ func TestUpdateAccount_EmptyExtraPayloadCanClearQuotaLimits(t *testing.T) {
|
||||
require.NotContains(t, repo.account.Extra, "quota_weekly_limit")
|
||||
require.Len(t, repo.account.Extra, 0)
|
||||
}
|
||||
|
||||
func TestUpdateAccount_FixedWeeklyResetClearsLegacyRollingUsage(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
daysSinceMonday := (int(now.Weekday()) + 6) % 7
|
||||
currentWeekStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -daysSinceMonday)
|
||||
legacyRollingStart := currentWeekStart.Add(-24 * time.Hour)
|
||||
accountID := int64(104)
|
||||
repo := &updateAccountOveragesRepoStub{
|
||||
account: &Account{
|
||||
ID: accountID,
|
||||
Platform: PlatformAnthropic,
|
||||
Type: AccountTypeAPIKey,
|
||||
Status: StatusActive,
|
||||
Extra: map[string]any{
|
||||
"quota_weekly_limit": 40.0,
|
||||
"quota_weekly_used": 12.5,
|
||||
"quota_weekly_start": legacyRollingStart.Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
svc := &adminServiceImpl{accountRepo: repo}
|
||||
updated, err := svc.UpdateAccount(context.Background(), accountID, &UpdateAccountInput{
|
||||
Extra: map[string]any{
|
||||
"quota_weekly_limit": 40.0,
|
||||
"quota_weekly_reset_mode": "fixed",
|
||||
"quota_weekly_reset_day": float64(1),
|
||||
"quota_weekly_reset_hour": float64(0),
|
||||
"quota_reset_timezone": "UTC",
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, updated)
|
||||
require.Equal(t, 1, repo.updateCalls)
|
||||
require.InDelta(t, 0.0, updated.GetQuotaWeeklyUsed(), 1e-9)
|
||||
require.Equal(t, currentWeekStart.Format(time.RFC3339), updated.Extra["quota_weekly_start"])
|
||||
require.False(t, updated.IsWeeklyQuotaPeriodExpired())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user