fix: 修正分组账号可用计数口径

This commit is contained in:
shaw 2026-05-20 16:53:23 +08:00
parent dd4d482a70
commit df2b02e61c
2 changed files with 86 additions and 27 deletions

View File

@ -94,9 +94,13 @@ func (r *groupRepository) GetByID(ctx context.Context, id int64) (*service.Group
if err != nil {
return nil, err
}
total, active, _ := r.GetAccountCount(ctx, out.ID)
out.AccountCount = total
out.ActiveAccountCount = active
counts, err := r.loadAccountCounts(ctx, []int64{out.ID})
if err == nil {
c := counts[out.ID]
out.AccountCount = c.Total
out.ActiveAccountCount = c.Active
out.RateLimitedAccountCount = c.RateLimited
}
return out, nil
}
@ -538,15 +542,12 @@ func (r *groupRepository) ExistsByIDs(ctx context.Context, ids []int64) (map[int
func (r *groupRepository) GetAccountCount(ctx context.Context, groupID int64) (total int64, active int64, err error) {
var rateLimited int64
err = scanSingleRow(ctx, r.sql,
`SELECT COUNT(*),
COUNT(*) FILTER (WHERE a.status = 'active' AND a.schedulable = true),
COUNT(*) FILTER (WHERE a.status = 'active' AND (
a.rate_limit_reset_at > NOW() OR
a.overload_until > NOW() OR
a.temp_unschedulable_until > NOW()
))
fmt.Sprintf(`SELECT
COUNT(*) FILTER (WHERE a.deleted_at IS NULL),
COUNT(*) FILTER (WHERE %s),
COUNT(*) FILTER (WHERE %s)
FROM account_groups ag JOIN accounts a ON a.id = ag.account_id
WHERE ag.group_id = $1`,
WHERE ag.group_id = $1`, groupAccountAvailableSQL, groupAccountTemporarilyLimitedSQL),
[]any{groupID}, &total, &active, &rateLimited)
return
}
@ -680,6 +681,28 @@ type groupAccountCounts struct {
RateLimited int64
}
const (
// 分组页的"可用"账号数必须与账号仓储的 ListSchedulableByGroupID 过滤口径一致。
groupAccountAvailableSQL = `a.deleted_at IS NULL
AND a.status = 'active'
AND a.schedulable = true
AND (a.expires_at IS NULL OR a.expires_at > NOW() OR a.auto_pause_on_expired = FALSE)
AND (a.rate_limit_reset_at IS NULL OR a.rate_limit_reset_at <= NOW())
AND (a.overload_until IS NULL OR a.overload_until <= NOW())
AND (a.temp_unschedulable_until IS NULL OR a.temp_unschedulable_until <= NOW())`
// 这里沿用历史字段名 RateLimitedAccountCount但统计的是会让账号暂时退出调度的时间窗口。
groupAccountTemporarilyLimitedSQL = `a.deleted_at IS NULL
AND a.status = 'active'
AND a.schedulable = true
AND (a.expires_at IS NULL OR a.expires_at > NOW() OR a.auto_pause_on_expired = FALSE)
AND (
a.rate_limit_reset_at > NOW() OR
a.overload_until > NOW() OR
a.temp_unschedulable_until > NOW()
)`
)
func (r *groupRepository) loadAccountCounts(ctx context.Context, groupIDs []int64) (counts map[int64]groupAccountCounts, err error) {
counts = make(map[int64]groupAccountCounts, len(groupIDs))
if len(groupIDs) == 0 {
@ -688,18 +711,14 @@ func (r *groupRepository) loadAccountCounts(ctx context.Context, groupIDs []int6
rows, err := r.sql.QueryContext(
ctx,
`SELECT ag.group_id,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE a.status = 'active' AND a.schedulable = true) AS active,
COUNT(*) FILTER (WHERE a.status = 'active' AND (
a.rate_limit_reset_at > NOW() OR
a.overload_until > NOW() OR
a.temp_unschedulable_until > NOW()
)) AS rate_limited
fmt.Sprintf(`SELECT ag.group_id,
COUNT(*) FILTER (WHERE a.deleted_at IS NULL) AS total,
COUNT(*) FILTER (WHERE %s) AS active,
COUNT(*) FILTER (WHERE %s) AS rate_limited
FROM account_groups ag
JOIN accounts a ON a.id = ag.account_id
WHERE ag.group_id = ANY($1)
GROUP BY ag.group_id`,
GROUP BY ag.group_id`, groupAccountAvailableSQL, groupAccountTemporarilyLimitedSQL),
pq.Array(groupIDs),
)
if err != nil {

View File

@ -714,9 +714,9 @@ func (s *GroupRepoSuite) TestListWithFilters_ActiveAccountCount_LessThanTotal()
s.Assert().Equal(found.ActiveAccountCount, active, "GetAccountCount active must match ListWithFilters ActiveAccountCount")
}
// TestListWithFilters_RateLimitedAccountCount 验证 RateLimitedAccountCount 正确统计临时受限账号。
// 受限账号rate_limit_reset_at 尚未过期)仍然计入 ActiveAccountCount
// 同时额外出现在 RateLimitedAccountCount 中
// TestListWithFilters_RateLimitedAccountCount 验证临时受限账号不会计入可用账号数
// rate_limit / overload / temp_unschedulable 都会让账号退出当前调度池
// 因此 ActiveAccountCount 必须与真实调度查询口径一致
func (s *GroupRepoSuite) TestListWithFilters_RateLimitedAccountCount() {
g := &service.Group{
Name: "g-rate-limited",
@ -740,6 +740,24 @@ func (s *GroupRepoSuite) TestListWithFilters_RateLimitedAccountCount() {
[]any{"acc-rate-limited", service.PlatformAnthropic, service.AccountTypeOAuth},
&rateLimitedID))
var overloadedID int64
s.Require().NoError(scanSingleRow(s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type, overload_until) VALUES ($1, $2, $3, NOW() + INTERVAL '1 hour') RETURNING id",
[]any{"acc-overloaded", service.PlatformAnthropic, service.AccountTypeOAuth},
&overloadedID))
var tempUnschedulableID int64
s.Require().NoError(scanSingleRow(s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type, temp_unschedulable_until) VALUES ($1, $2, $3, NOW() + INTERVAL '1 hour') RETURNING id",
[]any{"acc-temp-unschedulable", service.PlatformAnthropic, service.AccountTypeOAuth},
&tempUnschedulableID))
var expiredID int64
s.Require().NoError(scanSingleRow(s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type, expires_at, auto_pause_on_expired) VALUES ($1, $2, $3, NOW() - INTERVAL '1 hour', TRUE) RETURNING id",
[]any{"acc-expired", service.PlatformAnthropic, service.AccountTypeOAuth},
&expiredID))
_, err := s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
normalID, g.ID, 1)
@ -748,6 +766,18 @@ func (s *GroupRepoSuite) TestListWithFilters_RateLimitedAccountCount() {
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
rateLimitedID, g.ID, 2)
s.Require().NoError(err)
_, err = s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
overloadedID, g.ID, 3)
s.Require().NoError(err)
_, err = s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
tempUnschedulableID, g.ID, 4)
s.Require().NoError(err)
_, err = s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
expiredID, g.ID, 5)
s.Require().NoError(err)
isExclusive := false
groups, _, err := s.repo.ListWithFilters(s.ctx,
@ -763,10 +793,20 @@ func (s *GroupRepoSuite) TestListWithFilters_RateLimitedAccountCount() {
}
}
s.Require().NotNil(found, "created group must appear in ListWithFilters result")
s.Assert().Equal(int64(2), found.AccountCount, "AccountCount must be 2")
// rate-limited account is still active+schedulable, so it counts toward active
s.Assert().Equal(int64(2), found.ActiveAccountCount, "rate-limited account still counts as active")
s.Assert().Equal(int64(1), found.RateLimitedAccountCount, "RateLimitedAccountCount must be 1")
s.Assert().Equal(int64(5), found.AccountCount, "AccountCount must include all linked accounts")
s.Assert().Equal(int64(1), found.ActiveAccountCount, "ActiveAccountCount must include only currently schedulable accounts")
s.Assert().Equal(int64(3), found.RateLimitedAccountCount, "RateLimitedAccountCount must include temporarily limited accounts")
total, active, err := s.repo.GetAccountCount(s.ctx, g.ID)
s.Require().NoError(err)
s.Assert().Equal(found.AccountCount, total, "GetAccountCount total must match ListWithFilters AccountCount")
s.Assert().Equal(found.ActiveAccountCount, active, "GetAccountCount active must match ListWithFilters ActiveAccountCount")
detail, err := s.repo.GetByID(s.ctx, g.ID)
s.Require().NoError(err)
s.Assert().Equal(found.AccountCount, detail.AccountCount, "GetByID AccountCount must match ListWithFilters")
s.Assert().Equal(found.ActiveAccountCount, detail.ActiveAccountCount, "GetByID ActiveAccountCount must match ListWithFilters")
s.Assert().Equal(found.RateLimitedAccountCount, detail.RateLimitedAccountCount, "GetByID RateLimitedAccountCount must match ListWithFilters")
}
// --- DeleteAccountGroupsByGroupID ---