Skip to content

Commit d4cc987

Browse files
QTomclaude
andcommitted
feat(admin): 分组管理新增容量列(并发/会话/RPM 实时聚合)
复用 GroupCapacityService,在 admin 分组列表中添加容量列, 显示每个分组的实时并发/会话/RPM 使用量和上限。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 961c30e commit d4cc987

16 files changed

Lines changed: 310 additions & 24 deletions

File tree

backend/cmd/server/wire_gen.go

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/internal/handler/admin/admin_basic_handlers_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
1717
adminSvc := newStubAdminService()
1818

1919
userHandler := NewUserHandler(adminSvc, nil)
20-
groupHandler := NewGroupHandler(adminSvc, nil)
21-
proxyHandler := NewProxyHandler(adminSvc, nil, nil)
20+
groupHandler := NewGroupHandler(adminSvc, nil, nil)
21+
proxyHandler := NewProxyHandler(adminSvc)
2222
redeemHandler := NewRedeemHandler(adminSvc, nil)
2323

2424
router.GET("/api/v1/admin/users", userHandler.List)

backend/internal/handler/admin/group_handler.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import (
1717

1818
// GroupHandler handles admin group management
1919
type GroupHandler struct {
20-
adminService service.AdminService
21-
dashboardService *service.DashboardService
20+
adminService service.AdminService
21+
dashboardService *service.DashboardService
22+
groupCapacityService *service.GroupCapacityService
2223
}
2324

2425
type optionalLimitField struct {
@@ -71,10 +72,11 @@ func (f optionalLimitField) ToServiceInput() *float64 {
7172
}
7273

7374
// NewGroupHandler creates a new admin group handler
74-
func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService) *GroupHandler {
75+
func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService) *GroupHandler {
7576
return &GroupHandler{
76-
adminService: adminService,
77-
dashboardService: dashboardService,
77+
adminService: adminService,
78+
dashboardService: dashboardService,
79+
groupCapacityService: groupCapacityService,
7880
}
7981
}
8082

@@ -382,6 +384,17 @@ func (h *GroupHandler) GetUsageSummary(c *gin.Context) {
382384
response.Success(c, results)
383385
}
384386

387+
// GetCapacitySummary returns aggregated capacity (concurrency/sessions/RPM) for all active groups.
388+
// GET /api/v1/admin/groups/capacity-summary
389+
func (h *GroupHandler) GetCapacitySummary(c *gin.Context) {
390+
results, err := h.groupCapacityService.GetAllGroupCapacity(c.Request.Context())
391+
if err != nil {
392+
response.Error(c, 500, "Failed to get group capacity summary")
393+
return
394+
}
395+
response.Success(c, results)
396+
}
397+
385398
// GetGroupAPIKeys handles getting API keys in a group
386399
// GET /api/v1/admin/groups/:id/api-keys
387400
func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) {

backend/internal/handler/dto/mappers.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,16 @@ func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
135135
return nil
136136
}
137137
out := &AdminGroup{
138-
Group: groupFromServiceBase(g),
139-
ModelRouting: g.ModelRouting,
140-
ModelRoutingEnabled: g.ModelRoutingEnabled,
141-
MCPXMLInject: g.MCPXMLInject,
142-
DefaultMappedModel: g.DefaultMappedModel,
143-
SupportedModelScopes: g.SupportedModelScopes,
138+
Group: groupFromServiceBase(g),
139+
ModelRouting: g.ModelRouting,
140+
ModelRoutingEnabled: g.ModelRoutingEnabled,
141+
MCPXMLInject: g.MCPXMLInject,
142+
DefaultMappedModel: g.DefaultMappedModel,
143+
SupportedModelScopes: g.SupportedModelScopes,
144144
AccountCount: g.AccountCount,
145145
ActiveAccountCount: g.ActiveAccountCount,
146146
RateLimitedAccountCount: g.RateLimitedAccountCount,
147-
SortOrder: g.SortOrder,
147+
SortOrder: g.SortOrder,
148148
}
149149
if len(g.AccountGroups) > 0 {
150150
out.AccountGroups = make([]AccountGroup, 0, len(g.AccountGroups))

backend/internal/handler/dto/types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ type AdminGroup struct {
122122
DefaultMappedModel string `json:"default_mapped_model"`
123123

124124
// 支持的模型系列(仅 antigravity 平台使用)
125-
SupportedModelScopes []string `json:"supported_model_scopes"`
126-
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
125+
SupportedModelScopes []string `json:"supported_model_scopes"`
126+
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
127127
AccountCount int64 `json:"account_count,omitempty"`
128128
ActiveAccountCount int64 `json:"active_account_count,omitempty"`
129129
RateLimitedAccountCount int64 `json:"rate_limited_account_count,omitempty"`

backend/internal/handler/sora_gateway_handler_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ func (s *stubUsageLogRepo) GetGroupStatsWithFilters(ctx context.Context, startTi
348348
func (s *stubUsageLogRepo) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) ([]usagestats.UserBreakdownItem, error) {
349349
return nil, nil
350350
}
351+
func (s *stubUsageLogRepo) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
352+
return nil, nil
353+
}
351354
func (s *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) {
352355
return nil, nil
353356
}

backend/internal/repository/usage_log_repo.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3000,7 +3000,6 @@ func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, start
30003000
return results, nil
30013001
}
30023002

3003-
<<<<<<< HEAD
30043003
// GetUserBreakdownStats returns per-user usage breakdown within a specific dimension.
30053004
func (r *usageLogRepository) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) (results []usagestats.UserBreakdownItem, err error) {
30063005
query := `
@@ -3088,13 +3087,11 @@ func (r *usageLogRepository) GetAllGroupUsageSummary(ctx context.Context, todayS
30883087
if err != nil {
30893088
return nil, err
30903089
}
3091-
defer rows.Close()
3092-
3090+
defer func() { _ = rows.Close() }()
30933091
var results []usagestats.GroupUsageSummary
30943092
for rows.Next() {
30953093
var row usagestats.GroupUsageSummary
30963094
if err := rows.Scan(&row.GroupID, &row.TotalCost, &row.TodayCost); err != nil {
3097-
>>>>>>> c8c1b4d4 (feat(admin): 分组管理列表新增用量列与账号数分类)
30983095
return nil, err
30993096
}
31003097
results = append(results, row)

backend/internal/server/api_contract_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -924,8 +924,8 @@ func (stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error
924924
return false, errors.New("not implemented")
925925
}
926926

927-
func (stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, error) {
928-
return 0, errors.New("not implemented")
927+
func (stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, int64, error) {
928+
return 0, 0, errors.New("not implemented")
929929
}
930930

931931
func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
@@ -1786,6 +1786,9 @@ func (r *stubUsageLogRepo) GetAccountUsageStats(ctx context.Context, accountID i
17861786
func (r *stubUsageLogRepo) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
17871787
return nil, errors.New("not implemented")
17881788
}
1789+
func (r *stubUsageLogRepo) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
1790+
return nil, errors.New("not implemented")
1791+
}
17891792

17901793
type stubSettingRepo struct {
17911794
all map[string]string

backend/internal/server/routes/admin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ func registerGroupRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
228228
groups.GET("", h.Admin.Group.List)
229229
groups.GET("/all", h.Admin.Group.GetAll)
230230
groups.GET("/usage-summary", h.Admin.Group.GetUsageSummary)
231+
groups.GET("/capacity-summary", h.Admin.Group.GetCapacitySummary)
231232
groups.PUT("/sort-order", h.Admin.Group.UpdateSortOrder)
232233
groups.GET("/:id", h.Admin.Group.GetByID)
233234
groups.POST("", h.Admin.Group.Create)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"time"
6+
)
7+
8+
// GroupCapacitySummary holds aggregated capacity for a single group.
9+
type GroupCapacitySummary struct {
10+
GroupID int64 `json:"group_id"`
11+
ConcurrencyUsed int `json:"concurrency_used"`
12+
ConcurrencyMax int `json:"concurrency_max"`
13+
SessionsUsed int `json:"sessions_used"`
14+
SessionsMax int `json:"sessions_max"`
15+
RPMUsed int `json:"rpm_used"`
16+
RPMMax int `json:"rpm_max"`
17+
}
18+
19+
// GroupCapacityService aggregates per-group capacity from runtime data.
20+
type GroupCapacityService struct {
21+
accountRepo AccountRepository
22+
groupRepo GroupRepository
23+
concurrencyService *ConcurrencyService
24+
sessionLimitCache SessionLimitCache
25+
rpmCache RPMCache
26+
}
27+
28+
// NewGroupCapacityService creates a new GroupCapacityService.
29+
func NewGroupCapacityService(
30+
accountRepo AccountRepository,
31+
groupRepo GroupRepository,
32+
concurrencyService *ConcurrencyService,
33+
sessionLimitCache SessionLimitCache,
34+
rpmCache RPMCache,
35+
) *GroupCapacityService {
36+
return &GroupCapacityService{
37+
accountRepo: accountRepo,
38+
groupRepo: groupRepo,
39+
concurrencyService: concurrencyService,
40+
sessionLimitCache: sessionLimitCache,
41+
rpmCache: rpmCache,
42+
}
43+
}
44+
45+
// GetAllGroupCapacity returns capacity summary for all active groups.
46+
func (s *GroupCapacityService) GetAllGroupCapacity(ctx context.Context) ([]GroupCapacitySummary, error) {
47+
groups, err := s.groupRepo.ListActive(ctx)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
results := make([]GroupCapacitySummary, 0, len(groups))
53+
for i := range groups {
54+
cap, err := s.getGroupCapacity(ctx, groups[i].ID)
55+
if err != nil {
56+
// Skip groups with errors, return partial results
57+
continue
58+
}
59+
cap.GroupID = groups[i].ID
60+
results = append(results, cap)
61+
}
62+
return results, nil
63+
}
64+
65+
func (s *GroupCapacityService) getGroupCapacity(ctx context.Context, groupID int64) (GroupCapacitySummary, error) {
66+
accounts, err := s.accountRepo.ListSchedulableByGroupID(ctx, groupID)
67+
if err != nil {
68+
return GroupCapacitySummary{}, err
69+
}
70+
if len(accounts) == 0 {
71+
return GroupCapacitySummary{}, nil
72+
}
73+
74+
// Collect account IDs and config values
75+
accountIDs := make([]int64, 0, len(accounts))
76+
sessionTimeouts := make(map[int64]time.Duration)
77+
var concurrencyMax, sessionsMax, rpmMax int
78+
79+
for i := range accounts {
80+
acc := &accounts[i]
81+
accountIDs = append(accountIDs, acc.ID)
82+
concurrencyMax += acc.Concurrency
83+
84+
if ms := acc.GetMaxSessions(); ms > 0 {
85+
sessionsMax += ms
86+
timeout := time.Duration(acc.GetSessionIdleTimeoutMinutes()) * time.Minute
87+
if timeout <= 0 {
88+
timeout = 5 * time.Minute
89+
}
90+
sessionTimeouts[acc.ID] = timeout
91+
}
92+
93+
if rpm := acc.GetBaseRPM(); rpm > 0 {
94+
rpmMax += rpm
95+
}
96+
}
97+
98+
// Batch query runtime data from Redis
99+
concurrencyMap, _ := s.concurrencyService.GetAccountConcurrencyBatch(ctx, accountIDs)
100+
101+
var sessionsMap map[int64]int
102+
if sessionsMax > 0 && s.sessionLimitCache != nil {
103+
sessionsMap, _ = s.sessionLimitCache.GetActiveSessionCountBatch(ctx, accountIDs, sessionTimeouts)
104+
}
105+
106+
var rpmMap map[int64]int
107+
if rpmMax > 0 && s.rpmCache != nil {
108+
rpmMap, _ = s.rpmCache.GetRPMBatch(ctx, accountIDs)
109+
}
110+
111+
// Aggregate
112+
var concurrencyUsed, sessionsUsed, rpmUsed int
113+
for _, id := range accountIDs {
114+
concurrencyUsed += concurrencyMap[id]
115+
if sessionsMap != nil {
116+
sessionsUsed += sessionsMap[id]
117+
}
118+
if rpmMap != nil {
119+
rpmUsed += rpmMap[id]
120+
}
121+
}
122+
123+
return GroupCapacitySummary{
124+
ConcurrencyUsed: concurrencyUsed,
125+
ConcurrencyMax: concurrencyMax,
126+
SessionsUsed: sessionsUsed,
127+
SessionsMax: sessionsMax,
128+
RPMUsed: rpmUsed,
129+
RPMMax: rpmMax,
130+
}, nil
131+
}

0 commit comments

Comments
 (0)