fix(account): normalize fixed quota windows on edit

This commit is contained in:
Zbl1007
2026-06-04 01:06:41 +08:00
parent aa69e3947d
commit fb0195f3dc
4 changed files with 152 additions and 0 deletions
+66
View File
@@ -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())
}