Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
dfbcc363
Unverified
Commit
dfbcc363
authored
Mar 14, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 14, 2026
Browse files
Merge pull request #969 from wucm667/feat/quota-fixed-reset-mode
feat: 账号配额支持固定时间重置模式
parents
e32977dd
b5f78ec1
Changes
13
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/dto/mappers.go
View file @
dfbcc363
...
@@ -281,6 +281,31 @@ func AccountFromServiceShallow(a *service.Account) *Account {
...
@@ -281,6 +281,31 @@ func AccountFromServiceShallow(a *service.Account) *Account {
used
:=
a
.
GetQuotaWeeklyUsed
()
used
:=
a
.
GetQuotaWeeklyUsed
()
out
.
QuotaWeeklyUsed
=
&
used
out
.
QuotaWeeklyUsed
=
&
used
}
}
// 固定时间重置配置
if
mode
:=
a
.
GetQuotaDailyResetMode
();
mode
==
"fixed"
{
out
.
QuotaDailyResetMode
=
&
mode
hour
:=
a
.
GetQuotaDailyResetHour
()
out
.
QuotaDailyResetHour
=
&
hour
}
if
mode
:=
a
.
GetQuotaWeeklyResetMode
();
mode
==
"fixed"
{
out
.
QuotaWeeklyResetMode
=
&
mode
day
:=
a
.
GetQuotaWeeklyResetDay
()
out
.
QuotaWeeklyResetDay
=
&
day
hour
:=
a
.
GetQuotaWeeklyResetHour
()
out
.
QuotaWeeklyResetHour
=
&
hour
}
if
a
.
GetQuotaDailyResetMode
()
==
"fixed"
||
a
.
GetQuotaWeeklyResetMode
()
==
"fixed"
{
tz
:=
a
.
GetQuotaResetTimezone
()
out
.
QuotaResetTimezone
=
&
tz
}
if
a
.
Extra
!=
nil
{
if
v
,
ok
:=
a
.
Extra
[
"quota_daily_reset_at"
]
.
(
string
);
ok
&&
v
!=
""
{
out
.
QuotaDailyResetAt
=
&
v
}
if
v
,
ok
:=
a
.
Extra
[
"quota_weekly_reset_at"
]
.
(
string
);
ok
&&
v
!=
""
{
out
.
QuotaWeeklyResetAt
=
&
v
}
}
}
}
return
out
return
out
...
...
backend/internal/handler/dto/types.go
View file @
dfbcc363
...
@@ -203,6 +203,16 @@ type Account struct {
...
@@ -203,6 +203,16 @@ type Account struct {
QuotaWeeklyLimit
*
float64
`json:"quota_weekly_limit,omitempty"`
QuotaWeeklyLimit
*
float64
`json:"quota_weekly_limit,omitempty"`
QuotaWeeklyUsed
*
float64
`json:"quota_weekly_used,omitempty"`
QuotaWeeklyUsed
*
float64
`json:"quota_weekly_used,omitempty"`
// 配额固定时间重置配置
QuotaDailyResetMode
*
string
`json:"quota_daily_reset_mode,omitempty"`
QuotaDailyResetHour
*
int
`json:"quota_daily_reset_hour,omitempty"`
QuotaWeeklyResetMode
*
string
`json:"quota_weekly_reset_mode,omitempty"`
QuotaWeeklyResetDay
*
int
`json:"quota_weekly_reset_day,omitempty"`
QuotaWeeklyResetHour
*
int
`json:"quota_weekly_reset_hour,omitempty"`
QuotaResetTimezone
*
string
`json:"quota_reset_timezone,omitempty"`
QuotaDailyResetAt
*
string
`json:"quota_daily_reset_at,omitempty"`
QuotaWeeklyResetAt
*
string
`json:"quota_weekly_reset_at,omitempty"`
Proxy
*
Proxy
`json:"proxy,omitempty"`
Proxy
*
Proxy
`json:"proxy,omitempty"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
...
...
backend/internal/repository/account_repo.go
View file @
dfbcc363
...
@@ -1727,8 +1727,96 @@ func (r *accountRepository) FindByExtraField(ctx context.Context, key string, va
...
@@ -1727,8 +1727,96 @@ func (r *accountRepository) FindByExtraField(ctx context.Context, key string, va
// nowUTC is a SQL expression to generate a UTC RFC3339 timestamp string.
// nowUTC is a SQL expression to generate a UTC RFC3339 timestamp string.
const
nowUTC
=
`to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
const
nowUTC
=
`to_char(NOW() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
// dailyExpiredExpr is a SQL expression that evaluates to TRUE when daily quota period has expired.
// Supports both rolling (24h from start) and fixed (pre-computed reset_at) modes.
const
dailyExpiredExpr
=
`(
CASE WHEN COALESCE(extra->>'quota_daily_reset_mode', 'rolling') = 'fixed'
THEN NOW() >= COALESCE((extra->>'quota_daily_reset_at')::timestamptz, '1970-01-01'::timestamptz)
ELSE COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz)
+ '24 hours'::interval <= NOW()
END
)`
// weeklyExpiredExpr is a SQL expression that evaluates to TRUE when weekly quota period has expired.
const
weeklyExpiredExpr
=
`(
CASE WHEN COALESCE(extra->>'quota_weekly_reset_mode', 'rolling') = 'fixed'
THEN NOW() >= COALESCE((extra->>'quota_weekly_reset_at')::timestamptz, '1970-01-01'::timestamptz)
ELSE COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz)
+ '168 hours'::interval <= NOW()
END
)`
// nextDailyResetAtExpr is a SQL expression to compute the next daily reset_at when a reset occurs.
// For fixed mode: computes the next future reset time based on NOW(), timezone, and configured hour.
// This correctly handles long-inactive accounts by jumping directly to the next valid reset point.
const
nextDailyResetAtExpr
=
`(
CASE WHEN COALESCE(extra->>'quota_daily_reset_mode', 'rolling') = 'fixed'
THEN to_char((
-- Compute today's reset point in the configured timezone, then pick next future one
CASE WHEN NOW() >= (
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
+ (COALESCE((extra->>'quota_daily_reset_hour')::int, 0) || ' hours')::interval
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
-- NOW() is at or past today's reset point → next reset is tomorrow
THEN (
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
+ (COALESCE((extra->>'quota_daily_reset_hour')::int, 0) || ' hours')::interval
+ '1 day'::interval
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
-- NOW() is before today's reset point → next reset is today
ELSE (
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
+ (COALESCE((extra->>'quota_daily_reset_hour')::int, 0) || ' hours')::interval
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
END
) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
ELSE NULL END
)`
// nextWeeklyResetAtExpr is a SQL expression to compute the next weekly reset_at when a reset occurs.
// For fixed mode: computes the next future reset time based on NOW(), timezone, configured day and hour.
// This correctly handles long-inactive accounts by jumping directly to the next valid reset point.
const
nextWeeklyResetAtExpr
=
`(
CASE WHEN COALESCE(extra->>'quota_weekly_reset_mode', 'rolling') = 'fixed'
THEN to_char((
-- Compute this week's reset point in the configured timezone
-- Step 1: get today's date at reset hour in configured tz
-- Step 2: compute days forward to target weekday
-- Step 3: if same day but past reset hour, advance 7 days
CASE
WHEN (
-- days_forward = (target_day - current_day + 7) % 7
(COALESCE((extra->>'quota_weekly_reset_day')::int, 1)
- EXTRACT(DOW FROM NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))::int
+ 7) % 7
) = 0 AND NOW() >= (
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
+ (COALESCE((extra->>'quota_weekly_reset_hour')::int, 0) || ' hours')::interval
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
-- Same weekday and past reset hour → next week
THEN (
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
+ (COALESCE((extra->>'quota_weekly_reset_hour')::int, 0) || ' hours')::interval
+ '7 days'::interval
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
ELSE (
-- Advance to target weekday this week (or next if days_forward > 0)
date_trunc('day', NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))
+ (COALESCE((extra->>'quota_weekly_reset_hour')::int, 0) || ' hours')::interval
+ ((
(COALESCE((extra->>'quota_weekly_reset_day')::int, 1)
- EXTRACT(DOW FROM NOW() AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC'))::int
+ 7) % 7
) || ' days')::interval
) AT TIME ZONE COALESCE(extra->>'quota_reset_timezone', 'UTC')
END
) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')
ELSE NULL END
)`
// IncrementQuotaUsed 原子递增账号的配额用量(总/日/周三个维度)
// IncrementQuotaUsed 原子递增账号的配额用量(总/日/周三个维度)
// 日/周额度在周期过期时自动重置为 0 再递增。
// 日/周额度在周期过期时自动重置为 0 再递增。
// 支持滚动窗口(rolling)和固定时间(fixed)两种重置模式。
func
(
r
*
accountRepository
)
IncrementQuotaUsed
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
error
{
func
(
r
*
accountRepository
)
IncrementQuotaUsed
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
error
{
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
`UPDATE accounts SET extra = (
`UPDATE accounts SET extra = (
...
@@ -1739,31 +1827,35 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
...
@@ -1739,31 +1827,35 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
|| CASE WHEN COALESCE((extra->>'quota_daily_limit')::numeric, 0) > 0 THEN
|| CASE WHEN COALESCE((extra->>'quota_daily_limit')::numeric, 0) > 0 THEN
jsonb_build_object(
jsonb_build_object(
'quota_daily_used',
'quota_daily_used',
CASE WHEN COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz)
CASE WHEN `
+
dailyExpiredExpr
+
`
+ '24 hours'::interval <= NOW()
THEN $1
THEN $1
ELSE COALESCE((extra->>'quota_daily_used')::numeric, 0) + $1 END,
ELSE COALESCE((extra->>'quota_daily_used')::numeric, 0) + $1 END,
'quota_daily_start',
'quota_daily_start',
CASE WHEN COALESCE((extra->>'quota_daily_start')::timestamptz, '1970-01-01'::timestamptz)
CASE WHEN `
+
dailyExpiredExpr
+
`
+ '24 hours'::interval <= NOW()
THEN `
+
nowUTC
+
`
THEN `
+
nowUTC
+
`
ELSE COALESCE(extra->>'quota_daily_start', `
+
nowUTC
+
`) END
ELSE COALESCE(extra->>'quota_daily_start', `
+
nowUTC
+
`) END
)
)
-- 固定模式重置时更新下次重置时间
|| CASE WHEN `
+
dailyExpiredExpr
+
` AND `
+
nextDailyResetAtExpr
+
` IS NOT NULL
THEN jsonb_build_object('quota_daily_reset_at', `
+
nextDailyResetAtExpr
+
`)
ELSE '{}'::jsonb END
ELSE '{}'::jsonb END
ELSE '{}'::jsonb END
-- 周额度:仅在 quota_weekly_limit > 0 时处理
-- 周额度:仅在 quota_weekly_limit > 0 时处理
|| CASE WHEN COALESCE((extra->>'quota_weekly_limit')::numeric, 0) > 0 THEN
|| CASE WHEN COALESCE((extra->>'quota_weekly_limit')::numeric, 0) > 0 THEN
jsonb_build_object(
jsonb_build_object(
'quota_weekly_used',
'quota_weekly_used',
CASE WHEN COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz)
CASE WHEN `
+
weeklyExpiredExpr
+
`
+ '168 hours'::interval <= NOW()
THEN $1
THEN $1
ELSE COALESCE((extra->>'quota_weekly_used')::numeric, 0) + $1 END,
ELSE COALESCE((extra->>'quota_weekly_used')::numeric, 0) + $1 END,
'quota_weekly_start',
'quota_weekly_start',
CASE WHEN COALESCE((extra->>'quota_weekly_start')::timestamptz, '1970-01-01'::timestamptz)
CASE WHEN `
+
weeklyExpiredExpr
+
`
+ '168 hours'::interval <= NOW()
THEN `
+
nowUTC
+
`
THEN `
+
nowUTC
+
`
ELSE COALESCE(extra->>'quota_weekly_start', `
+
nowUTC
+
`) END
ELSE COALESCE(extra->>'quota_weekly_start', `
+
nowUTC
+
`) END
)
)
-- 固定模式重置时更新下次重置时间
|| CASE WHEN `
+
weeklyExpiredExpr
+
` AND `
+
nextWeeklyResetAtExpr
+
` IS NOT NULL
THEN jsonb_build_object('quota_weekly_reset_at', `
+
nextWeeklyResetAtExpr
+
`)
ELSE '{}'::jsonb END
ELSE '{}'::jsonb END
ELSE '{}'::jsonb END
), updated_at = NOW()
), updated_at = NOW()
WHERE id = $2 AND deleted_at IS NULL
WHERE id = $2 AND deleted_at IS NULL
...
@@ -1796,12 +1888,13 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
...
@@ -1796,12 +1888,13 @@ func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, am
}
}
// ResetQuotaUsed 重置账号所有维度的配额用量为 0
// ResetQuotaUsed 重置账号所有维度的配额用量为 0
// 保留固定重置模式的配置字段(quota_daily_reset_mode 等),仅清零用量和窗口起始时间
func
(
r
*
accountRepository
)
ResetQuotaUsed
(
ctx
context
.
Context
,
id
int64
)
error
{
func
(
r
*
accountRepository
)
ResetQuotaUsed
(
ctx
context
.
Context
,
id
int64
)
error
{
_
,
err
:=
r
.
sql
.
ExecContext
(
ctx
,
_
,
err
:=
r
.
sql
.
ExecContext
(
ctx
,
`UPDATE accounts SET extra = (
`UPDATE accounts SET extra = (
COALESCE(extra, '{}'::jsonb)
COALESCE(extra, '{}'::jsonb)
|| '{"quota_used": 0, "quota_daily_used": 0, "quota_weekly_used": 0}'::jsonb
|| '{"quota_used": 0, "quota_daily_used": 0, "quota_weekly_used": 0}'::jsonb
) - 'quota_daily_start' - 'quota_weekly_start', updated_at = NOW()
) - 'quota_daily_start' - 'quota_weekly_start'
- 'quota_daily_reset_at' - 'quota_weekly_reset_at'
, updated_at = NOW()
WHERE id = $1 AND deleted_at IS NULL`
,
WHERE id = $1 AND deleted_at IS NULL`
,
id
)
id
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
backend/internal/service/account.go
View file @
dfbcc363
...
@@ -3,6 +3,7 @@ package service
...
@@ -3,6 +3,7 @@ package service
import
(
import
(
"encoding/json"
"encoding/json"
"errors"
"hash/fnv"
"hash/fnv"
"reflect"
"reflect"
"sort"
"sort"
...
@@ -1279,6 +1280,240 @@ func (a *Account) getExtraTime(key string) time.Time {
...
@@ -1279,6 +1280,240 @@ func (a *Account) getExtraTime(key string) time.Time {
return
time
.
Time
{}
return
time
.
Time
{}
}
}
// getExtraString 从 Extra 中读取指定 key 的字符串值
func
(
a
*
Account
)
getExtraString
(
key
string
)
string
{
if
a
.
Extra
==
nil
{
return
""
}
if
v
,
ok
:=
a
.
Extra
[
key
];
ok
{
if
s
,
ok
:=
v
.
(
string
);
ok
{
return
s
}
}
return
""
}
// getExtraInt 从 Extra 中读取指定 key 的 int 值
func
(
a
*
Account
)
getExtraInt
(
key
string
)
int
{
if
a
.
Extra
==
nil
{
return
0
}
if
v
,
ok
:=
a
.
Extra
[
key
];
ok
{
return
int
(
parseExtraFloat64
(
v
))
}
return
0
}
// GetQuotaDailyResetMode 获取日额度重置模式:"rolling"(默认)或 "fixed"
func
(
a
*
Account
)
GetQuotaDailyResetMode
()
string
{
if
m
:=
a
.
getExtraString
(
"quota_daily_reset_mode"
);
m
==
"fixed"
{
return
"fixed"
}
return
"rolling"
}
// GetQuotaDailyResetHour 获取固定重置的小时(0-23),默认 0
func
(
a
*
Account
)
GetQuotaDailyResetHour
()
int
{
return
a
.
getExtraInt
(
"quota_daily_reset_hour"
)
}
// GetQuotaWeeklyResetMode 获取周额度重置模式:"rolling"(默认)或 "fixed"
func
(
a
*
Account
)
GetQuotaWeeklyResetMode
()
string
{
if
m
:=
a
.
getExtraString
(
"quota_weekly_reset_mode"
);
m
==
"fixed"
{
return
"fixed"
}
return
"rolling"
}
// GetQuotaWeeklyResetDay 获取固定重置的星期几(0=周日, 1=周一, ..., 6=周六),默认 1(周一)
func
(
a
*
Account
)
GetQuotaWeeklyResetDay
()
int
{
if
a
.
Extra
==
nil
{
return
1
}
if
_
,
ok
:=
a
.
Extra
[
"quota_weekly_reset_day"
];
!
ok
{
return
1
}
return
a
.
getExtraInt
(
"quota_weekly_reset_day"
)
}
// GetQuotaWeeklyResetHour 获取周配额固定重置的小时(0-23),默认 0
func
(
a
*
Account
)
GetQuotaWeeklyResetHour
()
int
{
return
a
.
getExtraInt
(
"quota_weekly_reset_hour"
)
}
// GetQuotaResetTimezone 获取固定重置的时区名(IANA),默认 "UTC"
func
(
a
*
Account
)
GetQuotaResetTimezone
()
string
{
if
tz
:=
a
.
getExtraString
(
"quota_reset_timezone"
);
tz
!=
""
{
return
tz
}
return
"UTC"
}
// nextFixedDailyReset 计算在 after 之后的下一个每日固定重置时间点
func
nextFixedDailyReset
(
hour
int
,
tz
*
time
.
Location
,
after
time
.
Time
)
time
.
Time
{
t
:=
after
.
In
(
tz
)
today
:=
time
.
Date
(
t
.
Year
(),
t
.
Month
(),
t
.
Day
(),
hour
,
0
,
0
,
0
,
tz
)
if
!
after
.
Before
(
today
)
{
return
today
.
AddDate
(
0
,
0
,
1
)
}
return
today
}
// lastFixedDailyReset 计算 now 之前最近一次的每日固定重置时间点
func
lastFixedDailyReset
(
hour
int
,
tz
*
time
.
Location
,
now
time
.
Time
)
time
.
Time
{
t
:=
now
.
In
(
tz
)
today
:=
time
.
Date
(
t
.
Year
(),
t
.
Month
(),
t
.
Day
(),
hour
,
0
,
0
,
0
,
tz
)
if
now
.
Before
(
today
)
{
return
today
.
AddDate
(
0
,
0
,
-
1
)
}
return
today
}
// nextFixedWeeklyReset 计算在 after 之后的下一个每周固定重置时间点
// day: 0=Sunday, 1=Monday, ..., 6=Saturday
func
nextFixedWeeklyReset
(
day
,
hour
int
,
tz
*
time
.
Location
,
after
time
.
Time
)
time
.
Time
{
t
:=
after
.
In
(
tz
)
todayReset
:=
time
.
Date
(
t
.
Year
(),
t
.
Month
(),
t
.
Day
(),
hour
,
0
,
0
,
0
,
tz
)
currentDay
:=
int
(
todayReset
.
Weekday
())
daysForward
:=
(
day
-
currentDay
+
7
)
%
7
if
daysForward
==
0
&&
!
after
.
Before
(
todayReset
)
{
daysForward
=
7
}
return
todayReset
.
AddDate
(
0
,
0
,
daysForward
)
}
// lastFixedWeeklyReset 计算 now 之前最近一次的每周固定重置时间点
func
lastFixedWeeklyReset
(
day
,
hour
int
,
tz
*
time
.
Location
,
now
time
.
Time
)
time
.
Time
{
t
:=
now
.
In
(
tz
)
todayReset
:=
time
.
Date
(
t
.
Year
(),
t
.
Month
(),
t
.
Day
(),
hour
,
0
,
0
,
0
,
tz
)
currentDay
:=
int
(
todayReset
.
Weekday
())
daysBack
:=
(
currentDay
-
day
+
7
)
%
7
if
daysBack
==
0
&&
now
.
Before
(
todayReset
)
{
daysBack
=
7
}
return
todayReset
.
AddDate
(
0
,
0
,
-
daysBack
)
}
// isFixedDailyPeriodExpired 检查日配额是否在固定时间模式下已过期
func
(
a
*
Account
)
isFixedDailyPeriodExpired
(
periodStart
time
.
Time
)
bool
{
if
periodStart
.
IsZero
()
{
return
true
}
tz
,
err
:=
time
.
LoadLocation
(
a
.
GetQuotaResetTimezone
())
if
err
!=
nil
{
tz
=
time
.
UTC
}
lastReset
:=
lastFixedDailyReset
(
a
.
GetQuotaDailyResetHour
(),
tz
,
time
.
Now
())
return
periodStart
.
Before
(
lastReset
)
}
// isFixedWeeklyPeriodExpired 检查周配额是否在固定时间模式下已过期
func
(
a
*
Account
)
isFixedWeeklyPeriodExpired
(
periodStart
time
.
Time
)
bool
{
if
periodStart
.
IsZero
()
{
return
true
}
tz
,
err
:=
time
.
LoadLocation
(
a
.
GetQuotaResetTimezone
())
if
err
!=
nil
{
tz
=
time
.
UTC
}
lastReset
:=
lastFixedWeeklyReset
(
a
.
GetQuotaWeeklyResetDay
(),
a
.
GetQuotaWeeklyResetHour
(),
tz
,
time
.
Now
())
return
periodStart
.
Before
(
lastReset
)
}
// ComputeQuotaResetAt 根据当前配置计算并填充 extra 中的 quota_daily_reset_at / quota_weekly_reset_at
// 在保存账号配置时调用
func
ComputeQuotaResetAt
(
extra
map
[
string
]
any
)
{
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"
{
hour
:=
int
(
parseExtraFloat64
(
extra
[
"quota_daily_reset_hour"
]))
if
hour
<
0
||
hour
>
23
{
hour
=
0
}
resetAt
:=
nextFixedDailyReset
(
hour
,
tz
,
now
)
extra
[
"quota_daily_reset_at"
]
=
resetAt
.
UTC
()
.
Format
(
time
.
RFC3339
)
}
else
{
delete
(
extra
,
"quota_daily_reset_at"
)
}
// 周配额固定重置时间
if
mode
,
_
:=
extra
[
"quota_weekly_reset_mode"
]
.
(
string
);
mode
==
"fixed"
{
day
:=
1
// 默认周一
if
d
,
ok
:=
extra
[
"quota_weekly_reset_day"
];
ok
{
day
=
int
(
parseExtraFloat64
(
d
))
}
if
day
<
0
||
day
>
6
{
day
=
1
}
hour
:=
int
(
parseExtraFloat64
(
extra
[
"quota_weekly_reset_hour"
]))
if
hour
<
0
||
hour
>
23
{
hour
=
0
}
resetAt
:=
nextFixedWeeklyReset
(
day
,
hour
,
tz
,
now
)
extra
[
"quota_weekly_reset_at"
]
=
resetAt
.
UTC
()
.
Format
(
time
.
RFC3339
)
}
else
{
delete
(
extra
,
"quota_weekly_reset_at"
)
}
}
// ValidateQuotaResetConfig 校验配额固定重置时间配置的合法性
func
ValidateQuotaResetConfig
(
extra
map
[
string
]
any
)
error
{
if
extra
==
nil
{
return
nil
}
// 校验时区
if
tz
,
ok
:=
extra
[
"quota_reset_timezone"
]
.
(
string
);
ok
&&
tz
!=
""
{
if
_
,
err
:=
time
.
LoadLocation
(
tz
);
err
!=
nil
{
return
errors
.
New
(
"invalid quota_reset_timezone: must be a valid IANA timezone name"
)
}
}
// 日配额重置模式
if
mode
,
ok
:=
extra
[
"quota_daily_reset_mode"
]
.
(
string
);
ok
{
if
mode
!=
"rolling"
&&
mode
!=
"fixed"
{
return
errors
.
New
(
"quota_daily_reset_mode must be 'rolling' or 'fixed'"
)
}
}
// 日配额重置小时
if
v
,
ok
:=
extra
[
"quota_daily_reset_hour"
];
ok
{
hour
:=
int
(
parseExtraFloat64
(
v
))
if
hour
<
0
||
hour
>
23
{
return
errors
.
New
(
"quota_daily_reset_hour must be between 0 and 23"
)
}
}
// 周配额重置模式
if
mode
,
ok
:=
extra
[
"quota_weekly_reset_mode"
]
.
(
string
);
ok
{
if
mode
!=
"rolling"
&&
mode
!=
"fixed"
{
return
errors
.
New
(
"quota_weekly_reset_mode must be 'rolling' or 'fixed'"
)
}
}
// 周配额重置星期几
if
v
,
ok
:=
extra
[
"quota_weekly_reset_day"
];
ok
{
day
:=
int
(
parseExtraFloat64
(
v
))
if
day
<
0
||
day
>
6
{
return
errors
.
New
(
"quota_weekly_reset_day must be between 0 (Sunday) and 6 (Saturday)"
)
}
}
// 周配额重置小时
if
v
,
ok
:=
extra
[
"quota_weekly_reset_hour"
];
ok
{
hour
:=
int
(
parseExtraFloat64
(
v
))
if
hour
<
0
||
hour
>
23
{
return
errors
.
New
(
"quota_weekly_reset_hour must be between 0 and 23"
)
}
}
return
nil
}
// HasAnyQuotaLimit 检查是否配置了任一维度的配额限制
// HasAnyQuotaLimit 检查是否配置了任一维度的配额限制
func
(
a
*
Account
)
HasAnyQuotaLimit
()
bool
{
func
(
a
*
Account
)
HasAnyQuotaLimit
()
bool
{
return
a
.
GetQuotaLimit
()
>
0
||
a
.
GetQuotaDailyLimit
()
>
0
||
a
.
GetQuotaWeeklyLimit
()
>
0
return
a
.
GetQuotaLimit
()
>
0
||
a
.
GetQuotaDailyLimit
()
>
0
||
a
.
GetQuotaWeeklyLimit
()
>
0
...
@@ -1301,14 +1536,26 @@ func (a *Account) IsQuotaExceeded() bool {
...
@@ -1301,14 +1536,26 @@ func (a *Account) IsQuotaExceeded() bool {
// 日额度(周期过期视为未超限,下次 increment 会重置)
// 日额度(周期过期视为未超限,下次 increment 会重置)
if
limit
:=
a
.
GetQuotaDailyLimit
();
limit
>
0
{
if
limit
:=
a
.
GetQuotaDailyLimit
();
limit
>
0
{
start
:=
a
.
getExtraTime
(
"quota_daily_start"
)
start
:=
a
.
getExtraTime
(
"quota_daily_start"
)
if
!
isPeriodExpired
(
start
,
24
*
time
.
Hour
)
&&
a
.
GetQuotaDailyUsed
()
>=
limit
{
var
expired
bool
if
a
.
GetQuotaDailyResetMode
()
==
"fixed"
{
expired
=
a
.
isFixedDailyPeriodExpired
(
start
)
}
else
{
expired
=
isPeriodExpired
(
start
,
24
*
time
.
Hour
)
}
if
!
expired
&&
a
.
GetQuotaDailyUsed
()
>=
limit
{
return
true
return
true
}
}
}
}
// 周额度
// 周额度
if
limit
:=
a
.
GetQuotaWeeklyLimit
();
limit
>
0
{
if
limit
:=
a
.
GetQuotaWeeklyLimit
();
limit
>
0
{
start
:=
a
.
getExtraTime
(
"quota_weekly_start"
)
start
:=
a
.
getExtraTime
(
"quota_weekly_start"
)
if
!
isPeriodExpired
(
start
,
7
*
24
*
time
.
Hour
)
&&
a
.
GetQuotaWeeklyUsed
()
>=
limit
{
var
expired
bool
if
a
.
GetQuotaWeeklyResetMode
()
==
"fixed"
{
expired
=
a
.
isFixedWeeklyPeriodExpired
(
start
)
}
else
{
expired
=
isPeriodExpired
(
start
,
7
*
24
*
time
.
Hour
)
}
if
!
expired
&&
a
.
GetQuotaWeeklyUsed
()
>=
limit
{
return
true
return
true
}
}
}
}
...
...
backend/internal/service/account_quota_reset_test.go
0 → 100644
View file @
dfbcc363
//go:build unit
package
service
import
(
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ---------------------------------------------------------------------------
// nextFixedDailyReset
// ---------------------------------------------------------------------------
func
TestNextFixedDailyReset_BeforeResetHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-14 06:00 UTC, reset hour = 9
after
:=
time
.
Date
(
2026
,
3
,
14
,
6
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedDailyReset
(
9
,
tz
,
after
)
want
:=
time
.
Date
(
2026
,
3
,
14
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedDailyReset_AtResetHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// Exactly at reset hour → should return tomorrow
after
:=
time
.
Date
(
2026
,
3
,
14
,
9
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedDailyReset
(
9
,
tz
,
after
)
want
:=
time
.
Date
(
2026
,
3
,
15
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedDailyReset_AfterResetHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// After reset hour → should return tomorrow
after
:=
time
.
Date
(
2026
,
3
,
14
,
15
,
30
,
0
,
0
,
tz
)
got
:=
nextFixedDailyReset
(
9
,
tz
,
after
)
want
:=
time
.
Date
(
2026
,
3
,
15
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedDailyReset_MidnightReset
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// Reset at hour 0 (midnight), currently 23:59
after
:=
time
.
Date
(
2026
,
3
,
14
,
23
,
59
,
0
,
0
,
tz
)
got
:=
nextFixedDailyReset
(
0
,
tz
,
after
)
want
:=
time
.
Date
(
2026
,
3
,
15
,
0
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedDailyReset_NonUTCTimezone
(
t
*
testing
.
T
)
{
tz
,
err
:=
time
.
LoadLocation
(
"Asia/Shanghai"
)
require
.
NoError
(
t
,
err
)
// 2026-03-14 07:00 UTC = 2026-03-14 15:00 CST, reset hour = 9 (CST)
after
:=
time
.
Date
(
2026
,
3
,
14
,
7
,
0
,
0
,
0
,
time
.
UTC
)
got
:=
nextFixedDailyReset
(
9
,
tz
,
after
)
// Already past 9:00 CST today → tomorrow 9:00 CST = 2026-03-15 01:00 UTC
want
:=
time
.
Date
(
2026
,
3
,
15
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
// ---------------------------------------------------------------------------
// lastFixedDailyReset
// ---------------------------------------------------------------------------
func
TestLastFixedDailyReset_BeforeResetHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
now
:=
time
.
Date
(
2026
,
3
,
14
,
6
,
0
,
0
,
0
,
tz
)
got
:=
lastFixedDailyReset
(
9
,
tz
,
now
)
// Before today's 9:00 → yesterday 9:00
want
:=
time
.
Date
(
2026
,
3
,
13
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestLastFixedDailyReset_AtResetHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
now
:=
time
.
Date
(
2026
,
3
,
14
,
9
,
0
,
0
,
0
,
tz
)
got
:=
lastFixedDailyReset
(
9
,
tz
,
now
)
// At exactly 9:00 → today 9:00
want
:=
time
.
Date
(
2026
,
3
,
14
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestLastFixedDailyReset_AfterResetHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
now
:=
time
.
Date
(
2026
,
3
,
14
,
15
,
0
,
0
,
0
,
tz
)
got
:=
lastFixedDailyReset
(
9
,
tz
,
now
)
// After 9:00 → today 9:00
want
:=
time
.
Date
(
2026
,
3
,
14
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
// ---------------------------------------------------------------------------
// nextFixedWeeklyReset
// ---------------------------------------------------------------------------
func
TestNextFixedWeeklyReset_TargetDayAhead
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-14 is Saturday (day=6), target = Monday (day=1), hour = 9
after
:=
time
.
Date
(
2026
,
3
,
14
,
10
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedWeeklyReset
(
1
,
9
,
tz
,
after
)
// Next Monday = 2026-03-16
want
:=
time
.
Date
(
2026
,
3
,
16
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedWeeklyReset_TargetDayToday_BeforeHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-16 is Monday (day=1), target = Monday, hour = 9, before 9:00
after
:=
time
.
Date
(
2026
,
3
,
16
,
6
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedWeeklyReset
(
1
,
9
,
tz
,
after
)
// Today at 9:00
want
:=
time
.
Date
(
2026
,
3
,
16
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedWeeklyReset_TargetDayToday_AtHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-16 is Monday, target = Monday, hour = 9, exactly at 9:00
after
:=
time
.
Date
(
2026
,
3
,
16
,
9
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedWeeklyReset
(
1
,
9
,
tz
,
after
)
// Next Monday at 9:00
want
:=
time
.
Date
(
2026
,
3
,
23
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedWeeklyReset_TargetDayToday_AfterHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-16 is Monday, target = Monday, hour = 9, after 9:00
after
:=
time
.
Date
(
2026
,
3
,
16
,
15
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedWeeklyReset
(
1
,
9
,
tz
,
after
)
// Next Monday at 9:00
want
:=
time
.
Date
(
2026
,
3
,
23
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedWeeklyReset_TargetDayPast
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-18 is Wednesday (day=3), target = Monday (day=1)
after
:=
time
.
Date
(
2026
,
3
,
18
,
10
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedWeeklyReset
(
1
,
9
,
tz
,
after
)
// Next Monday = 2026-03-23
want
:=
time
.
Date
(
2026
,
3
,
23
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestNextFixedWeeklyReset_Sunday
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-14 is Saturday (day=6), target = Sunday (day=0)
after
:=
time
.
Date
(
2026
,
3
,
14
,
10
,
0
,
0
,
0
,
tz
)
got
:=
nextFixedWeeklyReset
(
0
,
0
,
tz
,
after
)
// Next Sunday = 2026-03-15
want
:=
time
.
Date
(
2026
,
3
,
15
,
0
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
// ---------------------------------------------------------------------------
// lastFixedWeeklyReset
// ---------------------------------------------------------------------------
func
TestLastFixedWeeklyReset_SameDay_AfterHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-16 is Monday (day=1), target = Monday, hour = 9, now = 15:00
now
:=
time
.
Date
(
2026
,
3
,
16
,
15
,
0
,
0
,
0
,
tz
)
got
:=
lastFixedWeeklyReset
(
1
,
9
,
tz
,
now
)
// Today at 9:00
want
:=
time
.
Date
(
2026
,
3
,
16
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestLastFixedWeeklyReset_SameDay_BeforeHour
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-16 is Monday, target = Monday, hour = 9, now = 06:00
now
:=
time
.
Date
(
2026
,
3
,
16
,
6
,
0
,
0
,
0
,
tz
)
got
:=
lastFixedWeeklyReset
(
1
,
9
,
tz
,
now
)
// Last Monday at 9:00 = 2026-03-09
want
:=
time
.
Date
(
2026
,
3
,
9
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
func
TestLastFixedWeeklyReset_DifferentDay
(
t
*
testing
.
T
)
{
tz
:=
time
.
UTC
// 2026-03-18 is Wednesday (day=3), target = Monday (day=1)
now
:=
time
.
Date
(
2026
,
3
,
18
,
10
,
0
,
0
,
0
,
tz
)
got
:=
lastFixedWeeklyReset
(
1
,
9
,
tz
,
now
)
// Last Monday = 2026-03-16
want
:=
time
.
Date
(
2026
,
3
,
16
,
9
,
0
,
0
,
0
,
tz
)
assert
.
Equal
(
t
,
want
,
got
)
}
// ---------------------------------------------------------------------------
// isFixedDailyPeriodExpired
// ---------------------------------------------------------------------------
func
TestIsFixedDailyPeriodExpired_ZeroPeriodStart
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}}
assert
.
True
(
t
,
a
.
isFixedDailyPeriodExpired
(
time
.
Time
{}))
}
func
TestIsFixedDailyPeriodExpired_NotExpired
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}}
// Period started after the most recent reset → not expired
// (This test uses a time very close to "now", which is after the last reset)
periodStart
:=
time
.
Now
()
.
Add
(
-
1
*
time
.
Minute
)
assert
.
False
(
t
,
a
.
isFixedDailyPeriodExpired
(
periodStart
))
}
func
TestIsFixedDailyPeriodExpired_Expired
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}}
// Period started 3 days ago → definitely expired
periodStart
:=
time
.
Now
()
.
Add
(
-
72
*
time
.
Hour
)
assert
.
True
(
t
,
a
.
isFixedDailyPeriodExpired
(
periodStart
))
}
func
TestIsFixedDailyPeriodExpired_InvalidTimezone
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"Invalid/Timezone"
,
}}
// Invalid timezone falls back to UTC
periodStart
:=
time
.
Now
()
.
Add
(
-
72
*
time
.
Hour
)
assert
.
True
(
t
,
a
.
isFixedDailyPeriodExpired
(
periodStart
))
}
// ---------------------------------------------------------------------------
// isFixedWeeklyPeriodExpired
// ---------------------------------------------------------------------------
func
TestIsFixedWeeklyPeriodExpired_ZeroPeriodStart
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_weekly_reset_mode"
:
"fixed"
,
"quota_weekly_reset_day"
:
float64
(
1
),
"quota_weekly_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}}
assert
.
True
(
t
,
a
.
isFixedWeeklyPeriodExpired
(
time
.
Time
{}))
}
func
TestIsFixedWeeklyPeriodExpired_NotExpired
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_weekly_reset_mode"
:
"fixed"
,
"quota_weekly_reset_day"
:
float64
(
1
),
"quota_weekly_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}}
// Period started 1 minute ago → not expired
periodStart
:=
time
.
Now
()
.
Add
(
-
1
*
time
.
Minute
)
assert
.
False
(
t
,
a
.
isFixedWeeklyPeriodExpired
(
periodStart
))
}
func
TestIsFixedWeeklyPeriodExpired_Expired
(
t
*
testing
.
T
)
{
a
:=
&
Account
{
Extra
:
map
[
string
]
any
{
"quota_weekly_reset_mode"
:
"fixed"
,
"quota_weekly_reset_day"
:
float64
(
1
),
"quota_weekly_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}}
// Period started 10 days ago → definitely expired
periodStart
:=
time
.
Now
()
.
Add
(
-
240
*
time
.
Hour
)
assert
.
True
(
t
,
a
.
isFixedWeeklyPeriodExpired
(
periodStart
))
}
// ---------------------------------------------------------------------------
// ValidateQuotaResetConfig
// ---------------------------------------------------------------------------
func
TestValidateQuotaResetConfig_NilExtra
(
t
*
testing
.
T
)
{
assert
.
NoError
(
t
,
ValidateQuotaResetConfig
(
nil
))
}
func
TestValidateQuotaResetConfig_EmptyExtra
(
t
*
testing
.
T
)
{
assert
.
NoError
(
t
,
ValidateQuotaResetConfig
(
map
[
string
]
any
{}))
}
func
TestValidateQuotaResetConfig_ValidFixed
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_weekly_reset_mode"
:
"fixed"
,
"quota_weekly_reset_day"
:
float64
(
1
),
"quota_weekly_reset_hour"
:
float64
(
0
),
"quota_reset_timezone"
:
"Asia/Shanghai"
,
}
assert
.
NoError
(
t
,
ValidateQuotaResetConfig
(
extra
))
}
func
TestValidateQuotaResetConfig_ValidRolling
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"rolling"
,
"quota_weekly_reset_mode"
:
"rolling"
,
}
assert
.
NoError
(
t
,
ValidateQuotaResetConfig
(
extra
))
}
func
TestValidateQuotaResetConfig_InvalidTimezone
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_reset_timezone"
:
"Not/A/Timezone"
,
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_reset_timezone"
)
}
func
TestValidateQuotaResetConfig_InvalidDailyMode
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"invalid"
,
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_daily_reset_mode"
)
}
func
TestValidateQuotaResetConfig_InvalidDailyHour_TooHigh
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_hour"
:
float64
(
24
),
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_daily_reset_hour"
)
}
func
TestValidateQuotaResetConfig_InvalidDailyHour_Negative
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_hour"
:
float64
(
-
1
),
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_daily_reset_hour"
)
}
func
TestValidateQuotaResetConfig_InvalidWeeklyMode
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_weekly_reset_mode"
:
"unknown"
,
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_weekly_reset_mode"
)
}
func
TestValidateQuotaResetConfig_InvalidWeeklyDay_TooHigh
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_weekly_reset_day"
:
float64
(
7
),
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_weekly_reset_day"
)
}
func
TestValidateQuotaResetConfig_InvalidWeeklyDay_Negative
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_weekly_reset_day"
:
float64
(
-
1
),
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_weekly_reset_day"
)
}
func
TestValidateQuotaResetConfig_InvalidWeeklyHour
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_weekly_reset_hour"
:
float64
(
25
),
}
err
:=
ValidateQuotaResetConfig
(
extra
)
require
.
Error
(
t
,
err
)
assert
.
Contains
(
t
,
err
.
Error
(),
"quota_weekly_reset_hour"
)
}
func
TestValidateQuotaResetConfig_BoundaryValues
(
t
*
testing
.
T
)
{
// All boundary values should be valid
extra
:=
map
[
string
]
any
{
"quota_daily_reset_hour"
:
float64
(
23
),
"quota_weekly_reset_day"
:
float64
(
0
),
// Sunday
"quota_weekly_reset_hour"
:
float64
(
0
),
"quota_reset_timezone"
:
"UTC"
,
}
assert
.
NoError
(
t
,
ValidateQuotaResetConfig
(
extra
))
extra2
:=
map
[
string
]
any
{
"quota_daily_reset_hour"
:
float64
(
0
),
"quota_weekly_reset_day"
:
float64
(
6
),
// Saturday
"quota_weekly_reset_hour"
:
float64
(
23
),
}
assert
.
NoError
(
t
,
ValidateQuotaResetConfig
(
extra2
))
}
// ---------------------------------------------------------------------------
// ComputeQuotaResetAt
// ---------------------------------------------------------------------------
func
TestComputeQuotaResetAt_RollingMode_NoResetAt
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"rolling"
,
"quota_weekly_reset_mode"
:
"rolling"
,
}
ComputeQuotaResetAt
(
extra
)
_
,
hasDailyResetAt
:=
extra
[
"quota_daily_reset_at"
]
_
,
hasWeeklyResetAt
:=
extra
[
"quota_weekly_reset_at"
]
assert
.
False
(
t
,
hasDailyResetAt
,
"rolling mode should not set quota_daily_reset_at"
)
assert
.
False
(
t
,
hasWeeklyResetAt
,
"rolling mode should not set quota_weekly_reset_at"
)
}
func
TestComputeQuotaResetAt_RollingMode_ClearsExistingResetAt
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"rolling"
,
"quota_weekly_reset_mode"
:
"rolling"
,
"quota_daily_reset_at"
:
"2026-03-14T09:00:00Z"
,
"quota_weekly_reset_at"
:
"2026-03-16T09:00:00Z"
,
}
ComputeQuotaResetAt
(
extra
)
_
,
hasDailyResetAt
:=
extra
[
"quota_daily_reset_at"
]
_
,
hasWeeklyResetAt
:=
extra
[
"quota_weekly_reset_at"
]
assert
.
False
(
t
,
hasDailyResetAt
,
"rolling mode should remove quota_daily_reset_at"
)
assert
.
False
(
t
,
hasWeeklyResetAt
,
"rolling mode should remove quota_weekly_reset_at"
)
}
func
TestComputeQuotaResetAt_FixedDaily_SetsResetAt
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"UTC"
,
}
ComputeQuotaResetAt
(
extra
)
resetAtStr
,
ok
:=
extra
[
"quota_daily_reset_at"
]
.
(
string
)
require
.
True
(
t
,
ok
,
"quota_daily_reset_at should be set"
)
resetAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
resetAtStr
)
require
.
NoError
(
t
,
err
)
// Reset time should be in the future
assert
.
True
(
t
,
resetAt
.
After
(
time
.
Now
()),
"reset_at should be in the future"
)
// Reset hour should be 9 UTC
assert
.
Equal
(
t
,
9
,
resetAt
.
UTC
()
.
Hour
())
}
func
TestComputeQuotaResetAt_FixedWeekly_SetsResetAt
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_weekly_reset_mode"
:
"fixed"
,
"quota_weekly_reset_day"
:
float64
(
1
),
// Monday
"quota_weekly_reset_hour"
:
float64
(
0
),
"quota_reset_timezone"
:
"UTC"
,
}
ComputeQuotaResetAt
(
extra
)
resetAtStr
,
ok
:=
extra
[
"quota_weekly_reset_at"
]
.
(
string
)
require
.
True
(
t
,
ok
,
"quota_weekly_reset_at should be set"
)
resetAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
resetAtStr
)
require
.
NoError
(
t
,
err
)
// Reset time should be in the future
assert
.
True
(
t
,
resetAt
.
After
(
time
.
Now
()),
"reset_at should be in the future"
)
// Reset day should be Monday
assert
.
Equal
(
t
,
time
.
Monday
,
resetAt
.
UTC
()
.
Weekday
())
}
func
TestComputeQuotaResetAt_FixedDaily_WithTimezone
(
t
*
testing
.
T
)
{
tz
,
err
:=
time
.
LoadLocation
(
"Asia/Shanghai"
)
require
.
NoError
(
t
,
err
)
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
9
),
"quota_reset_timezone"
:
"Asia/Shanghai"
,
}
ComputeQuotaResetAt
(
extra
)
resetAtStr
,
ok
:=
extra
[
"quota_daily_reset_at"
]
.
(
string
)
require
.
True
(
t
,
ok
)
resetAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
resetAtStr
)
require
.
NoError
(
t
,
err
)
// In Shanghai timezone, the hour should be 9
assert
.
Equal
(
t
,
9
,
resetAt
.
In
(
tz
)
.
Hour
())
}
func
TestComputeQuotaResetAt_DefaultTimezone
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
12
),
}
ComputeQuotaResetAt
(
extra
)
resetAtStr
,
ok
:=
extra
[
"quota_daily_reset_at"
]
.
(
string
)
require
.
True
(
t
,
ok
)
resetAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
resetAtStr
)
require
.
NoError
(
t
,
err
)
// Default timezone is UTC
assert
.
Equal
(
t
,
12
,
resetAt
.
UTC
()
.
Hour
())
}
func
TestComputeQuotaResetAt_InvalidHour_ClampedToZero
(
t
*
testing
.
T
)
{
extra
:=
map
[
string
]
any
{
"quota_daily_reset_mode"
:
"fixed"
,
"quota_daily_reset_hour"
:
float64
(
99
),
"quota_reset_timezone"
:
"UTC"
,
}
ComputeQuotaResetAt
(
extra
)
resetAtStr
,
ok
:=
extra
[
"quota_daily_reset_at"
]
.
(
string
)
require
.
True
(
t
,
ok
)
resetAt
,
err
:=
time
.
Parse
(
time
.
RFC3339
,
resetAtStr
)
require
.
NoError
(
t
,
err
)
// Invalid hour → clamped to 0
assert
.
Equal
(
t
,
0
,
resetAt
.
UTC
()
.
Hour
())
}
backend/internal/service/admin_service.go
View file @
dfbcc363
...
@@ -1462,6 +1462,13 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
...
@@ -1462,6 +1462,13 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
Status
:
StatusActive
,
Status
:
StatusActive
,
Schedulable
:
true
,
Schedulable
:
true
,
}
}
// 预计算固定时间重置的下次重置时间
if
account
.
Extra
!=
nil
{
if
err
:=
ValidateQuotaResetConfig
(
account
.
Extra
);
err
!=
nil
{
return
nil
,
err
}
ComputeQuotaResetAt
(
account
.
Extra
)
}
if
input
.
ExpiresAt
!=
nil
&&
*
input
.
ExpiresAt
>
0
{
if
input
.
ExpiresAt
!=
nil
&&
*
input
.
ExpiresAt
>
0
{
expiresAt
:=
time
.
Unix
(
*
input
.
ExpiresAt
,
0
)
expiresAt
:=
time
.
Unix
(
*
input
.
ExpiresAt
,
0
)
account
.
ExpiresAt
=
&
expiresAt
account
.
ExpiresAt
=
&
expiresAt
...
@@ -1535,6 +1542,11 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
...
@@ -1535,6 +1542,11 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
}
}
}
}
account
.
Extra
=
input
.
Extra
account
.
Extra
=
input
.
Extra
// 校验并预计算固定时间重置的下次重置时间
if
err
:=
ValidateQuotaResetConfig
(
account
.
Extra
);
err
!=
nil
{
return
nil
,
err
}
ComputeQuotaResetAt
(
account
.
Extra
)
}
}
if
input
.
ProxyID
!=
nil
{
if
input
.
ProxyID
!=
nil
{
// 0 表示清除代理(前端发送 0 而不是 null 来表达清除意图)
// 0 表示清除代理(前端发送 0 而不是 null 来表达清除意图)
...
...
frontend/src/components/account/AccountUsageCell.vue
View file @
dfbcc363
...
@@ -942,13 +942,25 @@ const makeQuotaBar = (
...
@@ -942,13 +942,25 @@ const makeQuotaBar = (
let
resetsAt
:
string
|
null
=
null
let
resetsAt
:
string
|
null
=
null
if
(
startKey
)
{
if
(
startKey
)
{
const
extra
=
props
.
account
.
extra
as
Record
<
string
,
unknown
>
|
undefined
const
extra
=
props
.
account
.
extra
as
Record
<
string
,
unknown
>
|
undefined
const
isDaily
=
startKey
.
includes
(
'
daily
'
)
const
mode
=
isDaily
?
(
extra
?.
quota_daily_reset_mode
as
string
)
||
'
rolling
'
:
(
extra
?.
quota_weekly_reset_mode
as
string
)
||
'
rolling
'
if
(
mode
===
'
fixed
'
)
{
// Use pre-computed next reset time for fixed mode
const
resetAtKey
=
isDaily
?
'
quota_daily_reset_at
'
:
'
quota_weekly_reset_at
'
resetsAt
=
(
extra
?.[
resetAtKey
]
as
string
)
||
null
}
else
{
// Rolling mode: compute from start + period
const
startStr
=
extra
?.[
startKey
]
as
string
|
undefined
const
startStr
=
extra
?.[
startKey
]
as
string
|
undefined
if
(
startStr
)
{
if
(
startStr
)
{
const
startDate
=
new
Date
(
startStr
)
const
startDate
=
new
Date
(
startStr
)
const
periodMs
=
startKey
.
includes
(
'
d
aily
'
)
?
24
*
60
*
60
*
1000
:
7
*
24
*
60
*
60
*
1000
const
periodMs
=
isD
aily
?
24
*
60
*
60
*
1000
:
7
*
24
*
60
*
60
*
1000
resetsAt
=
new
Date
(
startDate
.
getTime
()
+
periodMs
).
toISOString
()
resetsAt
=
new
Date
(
startDate
.
getTime
()
+
periodMs
).
toISOString
()
}
}
}
}
}
return
{
utilization
,
resetsAt
}
return
{
utilization
,
resetsAt
}
}
}
...
...
frontend/src/components/account/CreateAccountModal.vue
View file @
dfbcc363
...
@@ -1568,9 +1568,21 @@
...
@@ -1568,9 +1568,21 @@
:
totalLimit
=
"
editQuotaLimit
"
:
totalLimit
=
"
editQuotaLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
dailyResetMode
=
"
editDailyResetMode
"
:
dailyResetHour
=
"
editDailyResetHour
"
:
weeklyResetMode
=
"
editWeeklyResetMode
"
:
weeklyResetDay
=
"
editWeeklyResetDay
"
:
weeklyResetHour
=
"
editWeeklyResetHour
"
:
resetTimezone
=
"
editResetTimezone
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
dailyResetMode
=
"
editDailyResetMode = $event
"
@
update
:
dailyResetHour
=
"
editDailyResetHour = $event
"
@
update
:
weeklyResetMode
=
"
editWeeklyResetMode = $event
"
@
update
:
weeklyResetDay
=
"
editWeeklyResetDay = $event
"
@
update
:
weeklyResetHour
=
"
editWeeklyResetHour = $event
"
@
update
:
resetTimezone
=
"
editResetTimezone = $event
"
/>
/>
<
/div
>
<
/div
>
...
@@ -2955,6 +2967,12 @@ const apiKeyValue = ref('')
...
@@ -2955,6 +2967,12 @@ const apiKeyValue = ref('')
const
editQuotaLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaDailyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaDailyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaWeeklyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaWeeklyLimit
=
ref
<
number
|
null
>
(
null
)
const
editDailyResetMode
=
ref
<
'
rolling
'
|
'
fixed
'
|
null
>
(
null
)
const
editDailyResetHour
=
ref
<
number
|
null
>
(
null
)
const
editWeeklyResetMode
=
ref
<
'
rolling
'
|
'
fixed
'
|
null
>
(
null
)
const
editWeeklyResetDay
=
ref
<
number
|
null
>
(
null
)
const
editWeeklyResetHour
=
ref
<
number
|
null
>
(
null
)
const
editResetTimezone
=
ref
<
string
|
null
>
(
null
)
const
modelMappings
=
ref
<
ModelMapping
[]
>
([])
const
modelMappings
=
ref
<
ModelMapping
[]
>
([])
const
modelRestrictionMode
=
ref
<
'
whitelist
'
|
'
mapping
'
>
(
'
whitelist
'
)
const
modelRestrictionMode
=
ref
<
'
whitelist
'
|
'
mapping
'
>
(
'
whitelist
'
)
const
allowedModels
=
ref
<
string
[]
>
([])
const
allowedModels
=
ref
<
string
[]
>
([])
...
@@ -3651,6 +3669,12 @@ const resetForm = () => {
...
@@ -3651,6 +3669,12 @@ const resetForm = () => {
editQuotaLimit
.
value
=
null
editQuotaLimit
.
value
=
null
editQuotaDailyLimit
.
value
=
null
editQuotaDailyLimit
.
value
=
null
editQuotaWeeklyLimit
.
value
=
null
editQuotaWeeklyLimit
.
value
=
null
editDailyResetMode
.
value
=
null
editDailyResetHour
.
value
=
null
editWeeklyResetMode
.
value
=
null
editWeeklyResetDay
.
value
=
null
editWeeklyResetHour
.
value
=
null
editResetTimezone
.
value
=
null
modelMappings
.
value
=
[]
modelMappings
.
value
=
[]
modelRestrictionMode
.
value
=
'
whitelist
'
modelRestrictionMode
.
value
=
'
whitelist
'
allowedModels
.
value
=
[...
claudeModels
]
// Default fill related models
allowedModels
.
value
=
[...
claudeModels
]
// Default fill related models
...
@@ -4158,6 +4182,19 @@ const createAccountAndFinish = async (
...
@@ -4158,6 +4182,19 @@ const createAccountAndFinish = async (
if
(
editQuotaWeeklyLimit
.
value
!=
null
&&
editQuotaWeeklyLimit
.
value
>
0
)
{
if
(
editQuotaWeeklyLimit
.
value
!=
null
&&
editQuotaWeeklyLimit
.
value
>
0
)
{
quotaExtra
.
quota_weekly_limit
=
editQuotaWeeklyLimit
.
value
quotaExtra
.
quota_weekly_limit
=
editQuotaWeeklyLimit
.
value
}
}
// Quota reset mode config
if
(
editDailyResetMode
.
value
===
'
fixed
'
)
{
quotaExtra
.
quota_daily_reset_mode
=
'
fixed
'
quotaExtra
.
quota_daily_reset_hour
=
editDailyResetHour
.
value
??
0
}
if
(
editWeeklyResetMode
.
value
===
'
fixed
'
)
{
quotaExtra
.
quota_weekly_reset_mode
=
'
fixed
'
quotaExtra
.
quota_weekly_reset_day
=
editWeeklyResetDay
.
value
??
1
quotaExtra
.
quota_weekly_reset_hour
=
editWeeklyResetHour
.
value
??
0
}
if
(
editDailyResetMode
.
value
===
'
fixed
'
||
editWeeklyResetMode
.
value
===
'
fixed
'
)
{
quotaExtra
.
quota_reset_timezone
=
editResetTimezone
.
value
||
'
UTC
'
}
if
(
Object
.
keys
(
quotaExtra
).
length
>
0
)
{
if
(
Object
.
keys
(
quotaExtra
).
length
>
0
)
{
finalExtra
=
quotaExtra
finalExtra
=
quotaExtra
}
}
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
dfbcc363
...
@@ -1161,9 +1161,21 @@
...
@@ -1161,9 +1161,21 @@
:
totalLimit
=
"
editQuotaLimit
"
:
totalLimit
=
"
editQuotaLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
dailyLimit
=
"
editQuotaDailyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
weeklyLimit
=
"
editQuotaWeeklyLimit
"
:
dailyResetMode
=
"
editDailyResetMode
"
:
dailyResetHour
=
"
editDailyResetHour
"
:
weeklyResetMode
=
"
editWeeklyResetMode
"
:
weeklyResetDay
=
"
editWeeklyResetDay
"
:
weeklyResetHour
=
"
editWeeklyResetHour
"
:
resetTimezone
=
"
editResetTimezone
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
totalLimit
=
"
editQuotaLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
dailyLimit
=
"
editQuotaDailyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
weeklyLimit
=
"
editQuotaWeeklyLimit = $event
"
@
update
:
dailyResetMode
=
"
editDailyResetMode = $event
"
@
update
:
dailyResetHour
=
"
editDailyResetHour = $event
"
@
update
:
weeklyResetMode
=
"
editWeeklyResetMode = $event
"
@
update
:
weeklyResetDay
=
"
editWeeklyResetDay = $event
"
@
update
:
weeklyResetHour
=
"
editWeeklyResetHour = $event
"
@
update
:
resetTimezone
=
"
editResetTimezone = $event
"
/>
/>
<
/div
>
<
/div
>
...
@@ -1814,6 +1826,12 @@ const anthropicPassthroughEnabled = ref(false)
...
@@ -1814,6 +1826,12 @@ const anthropicPassthroughEnabled = ref(false)
const
editQuotaLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaDailyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaDailyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaWeeklyLimit
=
ref
<
number
|
null
>
(
null
)
const
editQuotaWeeklyLimit
=
ref
<
number
|
null
>
(
null
)
const
editDailyResetMode
=
ref
<
'
rolling
'
|
'
fixed
'
|
null
>
(
null
)
const
editDailyResetHour
=
ref
<
number
|
null
>
(
null
)
const
editWeeklyResetMode
=
ref
<
'
rolling
'
|
'
fixed
'
|
null
>
(
null
)
const
editWeeklyResetDay
=
ref
<
number
|
null
>
(
null
)
const
editWeeklyResetHour
=
ref
<
number
|
null
>
(
null
)
const
editResetTimezone
=
ref
<
string
|
null
>
(
null
)
const
openAIWSModeOptions
=
computed
(()
=>
[
const
openAIWSModeOptions
=
computed
(()
=>
[
{
value
:
OPENAI_WS_MODE_OFF
,
label
:
t
(
'
admin.accounts.openai.wsModeOff
'
)
}
,
{
value
:
OPENAI_WS_MODE_OFF
,
label
:
t
(
'
admin.accounts.openai.wsModeOff
'
)
}
,
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
...
@@ -2001,10 +2019,23 @@ watch(
...
@@ -2001,10 +2019,23 @@ watch(
editQuotaDailyLimit
.
value
=
(
dailyVal
&&
dailyVal
>
0
)
?
dailyVal
:
null
editQuotaDailyLimit
.
value
=
(
dailyVal
&&
dailyVal
>
0
)
?
dailyVal
:
null
const
weeklyVal
=
extra
?.
quota_weekly_limit
as
number
|
undefined
const
weeklyVal
=
extra
?.
quota_weekly_limit
as
number
|
undefined
editQuotaWeeklyLimit
.
value
=
(
weeklyVal
&&
weeklyVal
>
0
)
?
weeklyVal
:
null
editQuotaWeeklyLimit
.
value
=
(
weeklyVal
&&
weeklyVal
>
0
)
?
weeklyVal
:
null
// Load quota reset mode config
editDailyResetMode
.
value
=
(
extra
?.
quota_daily_reset_mode
as
'
rolling
'
|
'
fixed
'
)
||
null
editDailyResetHour
.
value
=
(
extra
?.
quota_daily_reset_hour
as
number
)
??
null
editWeeklyResetMode
.
value
=
(
extra
?.
quota_weekly_reset_mode
as
'
rolling
'
|
'
fixed
'
)
||
null
editWeeklyResetDay
.
value
=
(
extra
?.
quota_weekly_reset_day
as
number
)
??
null
editWeeklyResetHour
.
value
=
(
extra
?.
quota_weekly_reset_hour
as
number
)
??
null
editResetTimezone
.
value
=
(
extra
?.
quota_reset_timezone
as
string
)
||
null
}
else
{
}
else
{
editQuotaLimit
.
value
=
null
editQuotaLimit
.
value
=
null
editQuotaDailyLimit
.
value
=
null
editQuotaDailyLimit
.
value
=
null
editQuotaWeeklyLimit
.
value
=
null
editQuotaWeeklyLimit
.
value
=
null
editDailyResetMode
.
value
=
null
editDailyResetHour
.
value
=
null
editWeeklyResetMode
.
value
=
null
editWeeklyResetDay
.
value
=
null
editWeeklyResetHour
.
value
=
null
editResetTimezone
.
value
=
null
}
}
// Load antigravity model mapping (Antigravity 只支持映射模式)
// Load antigravity model mapping (Antigravity 只支持映射模式)
...
@@ -2945,6 +2976,28 @@ const handleSubmit = async () => {
...
@@ -2945,6 +2976,28 @@ const handleSubmit = async () => {
}
else
{
}
else
{
delete
newExtra
.
quota_weekly_limit
delete
newExtra
.
quota_weekly_limit
}
}
// Quota reset mode config
if
(
editDailyResetMode
.
value
===
'
fixed
'
)
{
newExtra
.
quota_daily_reset_mode
=
'
fixed
'
newExtra
.
quota_daily_reset_hour
=
editDailyResetHour
.
value
??
0
}
else
{
delete
newExtra
.
quota_daily_reset_mode
delete
newExtra
.
quota_daily_reset_hour
}
if
(
editWeeklyResetMode
.
value
===
'
fixed
'
)
{
newExtra
.
quota_weekly_reset_mode
=
'
fixed
'
newExtra
.
quota_weekly_reset_day
=
editWeeklyResetDay
.
value
??
1
newExtra
.
quota_weekly_reset_hour
=
editWeeklyResetHour
.
value
??
0
}
else
{
delete
newExtra
.
quota_weekly_reset_mode
delete
newExtra
.
quota_weekly_reset_day
delete
newExtra
.
quota_weekly_reset_hour
}
if
(
editDailyResetMode
.
value
===
'
fixed
'
||
editWeeklyResetMode
.
value
===
'
fixed
'
)
{
newExtra
.
quota_reset_timezone
=
editResetTimezone
.
value
||
'
UTC
'
}
else
{
delete
newExtra
.
quota_reset_timezone
}
updatePayload
.
extra
=
newExtra
updatePayload
.
extra
=
newExtra
}
}
...
...
frontend/src/components/account/QuotaLimitCard.vue
View file @
dfbcc363
...
@@ -8,12 +8,24 @@ const props = defineProps<{
...
@@ -8,12 +8,24 @@ const props = defineProps<{
totalLimit
:
number
|
null
totalLimit
:
number
|
null
dailyLimit
:
number
|
null
dailyLimit
:
number
|
null
weeklyLimit
:
number
|
null
weeklyLimit
:
number
|
null
dailyResetMode
:
'
rolling
'
|
'
fixed
'
|
null
dailyResetHour
:
number
|
null
weeklyResetMode
:
'
rolling
'
|
'
fixed
'
|
null
weeklyResetDay
:
number
|
null
weeklyResetHour
:
number
|
null
resetTimezone
:
string
|
null
}
>
()
}
>
()
const
emit
=
defineEmits
<
{
const
emit
=
defineEmits
<
{
'
update:totalLimit
'
:
[
value
:
number
|
null
]
'
update:totalLimit
'
:
[
value
:
number
|
null
]
'
update:dailyLimit
'
:
[
value
:
number
|
null
]
'
update:dailyLimit
'
:
[
value
:
number
|
null
]
'
update:weeklyLimit
'
:
[
value
:
number
|
null
]
'
update:weeklyLimit
'
:
[
value
:
number
|
null
]
'
update:dailyResetMode
'
:
[
value
:
'
rolling
'
|
'
fixed
'
|
null
]
'
update:dailyResetHour
'
:
[
value
:
number
|
null
]
'
update:weeklyResetMode
'
:
[
value
:
'
rolling
'
|
'
fixed
'
|
null
]
'
update:weeklyResetDay
'
:
[
value
:
number
|
null
]
'
update:weeklyResetHour
'
:
[
value
:
number
|
null
]
'
update:resetTimezone
'
:
[
value
:
string
|
null
]
}
>
()
}
>
()
const
enabled
=
computed
(()
=>
const
enabled
=
computed
(()
=>
...
@@ -35,9 +47,56 @@ watch(localEnabled, (val) => {
...
@@ -35,9 +47,56 @@ watch(localEnabled, (val) => {
emit
(
'
update:totalLimit
'
,
null
)
emit
(
'
update:totalLimit
'
,
null
)
emit
(
'
update:dailyLimit
'
,
null
)
emit
(
'
update:dailyLimit
'
,
null
)
emit
(
'
update:weeklyLimit
'
,
null
)
emit
(
'
update:weeklyLimit
'
,
null
)
emit
(
'
update:dailyResetMode
'
,
null
)
emit
(
'
update:dailyResetHour
'
,
null
)
emit
(
'
update:weeklyResetMode
'
,
null
)
emit
(
'
update:weeklyResetDay
'
,
null
)
emit
(
'
update:weeklyResetHour
'
,
null
)
emit
(
'
update:resetTimezone
'
,
null
)
}
}
})
})
// Whether any fixed mode is active (to show timezone selector)
const
hasFixedMode
=
computed
(()
=>
props
.
dailyResetMode
===
'
fixed
'
||
props
.
weeklyResetMode
===
'
fixed
'
)
// Common timezone options
const
timezoneOptions
=
[
'
UTC
'
,
'
Asia/Shanghai
'
,
'
Asia/Tokyo
'
,
'
Asia/Seoul
'
,
'
Asia/Singapore
'
,
'
Asia/Kolkata
'
,
'
Asia/Dubai
'
,
'
Europe/London
'
,
'
Europe/Paris
'
,
'
Europe/Berlin
'
,
'
Europe/Moscow
'
,
'
America/New_York
'
,
'
America/Chicago
'
,
'
America/Denver
'
,
'
America/Los_Angeles
'
,
'
America/Sao_Paulo
'
,
'
Australia/Sydney
'
,
'
Pacific/Auckland
'
,
]
// Hours for dropdown (0-23)
const
hourOptions
=
Array
.
from
({
length
:
24
},
(
_
,
i
)
=>
i
)
// Day of week options
const
dayOptions
=
[
{
value
:
1
,
key
:
'
monday
'
},
{
value
:
2
,
key
:
'
tuesday
'
},
{
value
:
3
,
key
:
'
wednesday
'
},
{
value
:
4
,
key
:
'
thursday
'
},
{
value
:
5
,
key
:
'
friday
'
},
{
value
:
6
,
key
:
'
saturday
'
},
{
value
:
0
,
key
:
'
sunday
'
},
]
const
onTotalInput
=
(
e
:
Event
)
=>
{
const
onTotalInput
=
(
e
:
Event
)
=>
{
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
emit
(
'
update:totalLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
emit
(
'
update:totalLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
...
@@ -50,6 +109,25 @@ const onWeeklyInput = (e: Event) => {
...
@@ -50,6 +109,25 @@ const onWeeklyInput = (e: Event) => {
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
const
raw
=
(
e
.
target
as
HTMLInputElement
).
valueAsNumber
emit
(
'
update:weeklyLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
emit
(
'
update:weeklyLimit
'
,
Number
.
isNaN
(
raw
)
?
null
:
raw
)
}
}
const
onDailyModeChange
=
(
e
:
Event
)
=>
{
const
val
=
(
e
.
target
as
HTMLSelectElement
).
value
as
'
rolling
'
|
'
fixed
'
emit
(
'
update:dailyResetMode
'
,
val
)
if
(
val
===
'
fixed
'
)
{
if
(
props
.
dailyResetHour
==
null
)
emit
(
'
update:dailyResetHour
'
,
0
)
if
(
!
props
.
resetTimezone
)
emit
(
'
update:resetTimezone
'
,
'
UTC
'
)
}
}
const
onWeeklyModeChange
=
(
e
:
Event
)
=>
{
const
val
=
(
e
.
target
as
HTMLSelectElement
).
value
as
'
rolling
'
|
'
fixed
'
emit
(
'
update:weeklyResetMode
'
,
val
)
if
(
val
===
'
fixed
'
)
{
if
(
props
.
weeklyResetDay
==
null
)
emit
(
'
update:weeklyResetDay
'
,
1
)
if
(
props
.
weeklyResetHour
==
null
)
emit
(
'
update:weeklyResetHour
'
,
0
)
if
(
!
props
.
resetTimezone
)
emit
(
'
update:resetTimezone
'
,
'
UTC
'
)
}
}
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -94,7 +172,37 @@ const onWeeklyInput = (e: Event) => {
...
@@ -94,7 +172,37 @@ const onWeeklyInput = (e: Event) => {
:placeholder=
"t('admin.accounts.quotaLimitPlaceholder')"
:placeholder=
"t('admin.accounts.quotaLimitPlaceholder')"
/>
/>
</div>
</div>
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.quotaDailyLimitHint
'
)
}}
</p>
<!-- 日配额重置模式 -->
<div
class=
"mt-2 flex items-center gap-2"
>
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{
t
(
'
admin.accounts.quotaResetMode
'
)
}}
</label>
<select
:value=
"dailyResetMode || 'rolling'"
@
change=
"onDailyModeChange"
class=
"input py-1 text-xs"
>
<option
value=
"rolling"
>
{{
t
(
'
admin.accounts.quotaResetModeRolling
'
)
}}
</option>
<option
value=
"fixed"
>
{{
t
(
'
admin.accounts.quotaResetModeFixed
'
)
}}
</option>
</select>
</div>
<!-- 固定模式:小时选择 -->
<div
v-if=
"dailyResetMode === 'fixed'"
class=
"mt-2 flex items-center gap-2"
>
<label
class=
"text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"
>
{{
t
(
'
admin.accounts.quotaResetHour
'
)
}}
</label>
<select
:value=
"dailyResetHour ?? 0"
@
change=
"emit('update:dailyResetHour', Number(($event.target as HTMLSelectElement).value))"
class=
"input py-1 text-xs w-24"
>
<option
v-for=
"h in hourOptions"
:key=
"h"
:value=
"h"
>
{{
String
(
h
).
padStart
(
2
,
'
0
'
)
}}
:00
</option>
</select>
</div>
<p
class=
"input-hint"
>
<template
v-if=
"dailyResetMode === 'fixed'"
>
{{
t
(
'
admin.accounts.quotaDailyLimitHintFixed
'
,
{
hour
:
String
(
dailyResetHour
??
0
).
padStart
(
2
,
'
0
'
),
timezone
:
resetTimezone
||
'
UTC
'
}
)
}}
<
/template
>
<
template
v
-
else
>
{{
t
(
'
admin.accounts.quotaDailyLimitHint
'
)
}}
<
/template
>
<
/p
>
<
/div
>
<
/div
>
<!--
周配额
-->
<!--
周配额
-->
...
@@ -112,7 +220,57 @@ const onWeeklyInput = (e: Event) => {
...
@@ -112,7 +220,57 @@ const onWeeklyInput = (e: Event) => {
:
placeholder
=
"
t('admin.accounts.quotaLimitPlaceholder')
"
:
placeholder
=
"
t('admin.accounts.quotaLimitPlaceholder')
"
/>
/>
<
/div
>
<
/div
>
<p
class=
"input-hint"
>
{{
t
(
'
admin.accounts.quotaWeeklyLimitHint
'
)
}}
</p>
<!--
周配额重置模式
-->
<
div
class
=
"
mt-2 flex items-center gap-2
"
>
<
label
class
=
"
text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap
"
>
{{
t
(
'
admin.accounts.quotaResetMode
'
)
}}
<
/label
>
<
select
:
value
=
"
weeklyResetMode || 'rolling'
"
@
change
=
"
onWeeklyModeChange
"
class
=
"
input py-1 text-xs
"
>
<
option
value
=
"
rolling
"
>
{{
t
(
'
admin.accounts.quotaResetModeRolling
'
)
}}
<
/option
>
<
option
value
=
"
fixed
"
>
{{
t
(
'
admin.accounts.quotaResetModeFixed
'
)
}}
<
/option
>
<
/select
>
<
/div
>
<!--
固定模式
:
星期几
+
小时
-->
<
div
v
-
if
=
"
weeklyResetMode === 'fixed'
"
class
=
"
mt-2 flex items-center gap-2 flex-wrap
"
>
<
label
class
=
"
text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap
"
>
{{
t
(
'
admin.accounts.quotaWeeklyResetDay
'
)
}}
<
/label
>
<
select
:
value
=
"
weeklyResetDay ?? 1
"
@
change
=
"
emit('update:weeklyResetDay', Number(($event.target as HTMLSelectElement).value))
"
class
=
"
input py-1 text-xs w-28
"
>
<
option
v
-
for
=
"
d in dayOptions
"
:
key
=
"
d.value
"
:
value
=
"
d.value
"
>
{{
t
(
'
admin.accounts.dayOfWeek.
'
+
d
.
key
)
}}
<
/option
>
<
/select
>
<
label
class
=
"
text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap
"
>
{{
t
(
'
admin.accounts.quotaResetHour
'
)
}}
<
/label
>
<
select
:
value
=
"
weeklyResetHour ?? 0
"
@
change
=
"
emit('update:weeklyResetHour', Number(($event.target as HTMLSelectElement).value))
"
class
=
"
input py-1 text-xs w-24
"
>
<
option
v
-
for
=
"
h in hourOptions
"
:
key
=
"
h
"
:
value
=
"
h
"
>
{{
String
(
h
).
padStart
(
2
,
'
0
'
)
}}
:
00
<
/option
>
<
/select
>
<
/div
>
<
p
class
=
"
input-hint
"
>
<
template
v
-
if
=
"
weeklyResetMode === 'fixed'
"
>
{{
t
(
'
admin.accounts.quotaWeeklyLimitHintFixed
'
,
{
day
:
t
(
'
admin.accounts.dayOfWeek.
'
+
(
dayOptions
.
find
(
d
=>
d
.
value
===
(
weeklyResetDay
??
1
))?.
key
||
'
monday
'
)),
hour
:
String
(
weeklyResetHour
??
0
).
padStart
(
2
,
'
0
'
),
timezone
:
resetTimezone
||
'
UTC
'
}
)
}}
<
/template
>
<
template
v
-
else
>
{{
t
(
'
admin.accounts.quotaWeeklyLimitHint
'
)
}}
<
/template
>
<
/p
>
<
/div
>
<!--
时区选择
(
当任一维度使用固定模式时显示
)
-->
<
div
v
-
if
=
"
hasFixedMode
"
>
<
label
class
=
"
input-label
"
>
{{
t
(
'
admin.accounts.quotaResetTimezone
'
)
}}
<
/label
>
<
select
:
value
=
"
resetTimezone || 'UTC'
"
@
change
=
"
emit('update:resetTimezone', ($event.target as HTMLSelectElement).value)
"
class
=
"
input text-sm
"
>
<
option
v
-
for
=
"
tz in timezoneOptions
"
:
key
=
"
tz
"
:
value
=
"
tz
"
>
{{
tz
}}
<
/option
>
<
/select
>
<
/div
>
<
/div
>
<!--
总配额
-->
<!--
总配额
-->
...
...
frontend/src/i18n/locales/en.ts
View file @
dfbcc363
...
@@ -1866,6 +1866,23 @@ export default {
...
@@ -1866,6 +1866,23 @@ export default {
quotaWeeklyLimitHint
:
'
Automatically resets every 7 days from first usage.
'
,
quotaWeeklyLimitHint
:
'
Automatically resets every 7 days from first usage.
'
,
quotaTotalLimit
:
'
Total Limit
'
,
quotaTotalLimit
:
'
Total Limit
'
,
quotaTotalLimitHint
:
'
Cumulative spending limit. Does not auto-reset — use "Reset Quota" to clear.
'
,
quotaTotalLimitHint
:
'
Cumulative spending limit. Does not auto-reset — use "Reset Quota" to clear.
'
,
quotaResetMode
:
'
Reset Mode
'
,
quotaResetModeRolling
:
'
Rolling Window
'
,
quotaResetModeFixed
:
'
Fixed Time
'
,
quotaResetHour
:
'
Reset Hour
'
,
quotaWeeklyResetDay
:
'
Reset Day
'
,
quotaResetTimezone
:
'
Reset Timezone
'
,
quotaDailyLimitHintFixed
:
'
Resets daily at {hour}:00 ({timezone}).
'
,
quotaWeeklyLimitHintFixed
:
'
Resets every {day} at {hour}:00 ({timezone}).
'
,
dayOfWeek
:
{
monday
:
'
Monday
'
,
tuesday
:
'
Tuesday
'
,
wednesday
:
'
Wednesday
'
,
thursday
:
'
Thursday
'
,
friday
:
'
Friday
'
,
saturday
:
'
Saturday
'
,
sunday
:
'
Sunday
'
,
},
quotaLimitAmount
:
'
Total Limit
'
,
quotaLimitAmount
:
'
Total Limit
'
,
quotaLimitAmountHint
:
'
Cumulative spending limit. Does not auto-reset.
'
,
quotaLimitAmountHint
:
'
Cumulative spending limit. Does not auto-reset.
'
,
testConnection
:
'
Test Connection
'
,
testConnection
:
'
Test Connection
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
dfbcc363
...
@@ -1872,6 +1872,23 @@ export default {
...
@@ -1872,6 +1872,23 @@ export default {
quotaWeeklyLimitHint
:
'
从首次使用起每 7 天自动重置。
'
,
quotaWeeklyLimitHint
:
'
从首次使用起每 7 天自动重置。
'
,
quotaTotalLimit
:
'
总限额
'
,
quotaTotalLimit
:
'
总限额
'
,
quotaTotalLimitHint
:
'
累计消费上限,不会自动重置 — 使用「重置配额」手动清零。
'
,
quotaTotalLimitHint
:
'
累计消费上限,不会自动重置 — 使用「重置配额」手动清零。
'
,
quotaResetMode
:
'
重置方式
'
,
quotaResetModeRolling
:
'
滚动窗口
'
,
quotaResetModeFixed
:
'
固定时间
'
,
quotaResetHour
:
'
重置时间
'
,
quotaWeeklyResetDay
:
'
重置日
'
,
quotaResetTimezone
:
'
重置时区
'
,
quotaDailyLimitHintFixed
:
'
每天 {hour}:00({timezone})重置。
'
,
quotaWeeklyLimitHintFixed
:
'
每{day} {hour}:00({timezone})重置。
'
,
dayOfWeek
:
{
monday
:
'
周一
'
,
tuesday
:
'
周二
'
,
wednesday
:
'
周三
'
,
thursday
:
'
周四
'
,
friday
:
'
周五
'
,
saturday
:
'
周六
'
,
sunday
:
'
周日
'
,
},
quotaLimitAmount
:
'
总限额
'
,
quotaLimitAmount
:
'
总限额
'
,
quotaLimitAmountHint
:
'
累计消费上限,不会自动重置。
'
,
quotaLimitAmountHint
:
'
累计消费上限,不会自动重置。
'
,
testConnection
:
'
测试连接
'
,
testConnection
:
'
测试连接
'
,
...
...
frontend/src/types/index.ts
View file @
dfbcc363
...
@@ -727,6 +727,16 @@ export interface Account {
...
@@ -727,6 +727,16 @@ export interface Account {
quota_weekly_limit
?:
number
|
null
quota_weekly_limit
?:
number
|
null
quota_weekly_used
?:
number
|
null
quota_weekly_used
?:
number
|
null
// 配额固定时间重置配置
quota_daily_reset_mode
?:
'
rolling
'
|
'
fixed
'
|
null
quota_daily_reset_hour
?:
number
|
null
quota_weekly_reset_mode
?:
'
rolling
'
|
'
fixed
'
|
null
quota_weekly_reset_day
?:
number
|
null
quota_weekly_reset_hour
?:
number
|
null
quota_reset_timezone
?:
string
|
null
quota_daily_reset_at
?:
string
|
null
quota_weekly_reset_at
?:
string
|
null
// 运行时状态(仅当启用对应限制时返回)
// 运行时状态(仅当启用对应限制时返回)
current_window_cost
?:
number
|
null
// 当前窗口费用
current_window_cost
?:
number
|
null
// 当前窗口费用
active_sessions
?:
number
|
null
// 当前活跃会话数
active_sessions
?:
number
|
null
// 当前活跃会话数
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment