Skip to content

Commit 773fbda

Browse files
committed
feat: improve account admin filters and bulk model inputs
1 parent 531b895 commit 773fbda

21 files changed

Lines changed: 1776 additions & 150 deletions

backend/internal/handler/admin/account_data.go

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import (
88
"strings"
99
"time"
1010

11-
"log/slog"
12-
13-
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
1411
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
1512
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
1613
"github.com/Wei-Shaw/sub2api/internal/service"
1714
"github.com/gin-gonic/gin"
15+
"log/slog"
1816
)
1917

2018
const (
@@ -373,12 +371,12 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e
373371
return out, nil
374372
}
375373

376-
func (h *AccountHandler) listAccountsFiltered(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode, sortBy, sortOrder string) ([]service.Account, error) {
374+
func (h *AccountHandler) listAccountsFiltered(ctx context.Context, platform, accountType, status, schedulable, search string, groupID int64, privacyMode, displayGroup, namePrefix, searchRegex, sortBy, sortOrder string) ([]service.Account, error) {
377375
page := 1
378376
pageSize := dataPageCap
379377
var out []service.Account
380378
for {
381-
items, total, err := h.adminService.ListAccounts(ctx, page, pageSize, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder)
379+
items, total, err := h.adminService.ListAccounts(ctx, page, pageSize, platform, accountType, status, schedulable, search, groupID, privacyMode, displayGroup, namePrefix, searchRegex, sortBy, sortOrder)
382380
if err != nil {
383381
return nil, err
384382
}
@@ -407,31 +405,12 @@ func (h *AccountHandler) resolveExportAccounts(ctx context.Context, ids []int64,
407405
return out, nil
408406
}
409407

410-
platform := c.Query("platform")
411-
accountType := c.Query("type")
412-
status := c.Query("status")
413-
privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
414-
search := strings.TrimSpace(c.Query("search"))
415-
sortBy := c.DefaultQuery("sort_by", "name")
416-
sortOrder := c.DefaultQuery("sort_order", "asc")
417-
if len(search) > 100 {
418-
search = search[:100]
419-
}
420-
421-
groupID := int64(0)
422-
if groupIDStr := c.Query("group"); groupIDStr != "" {
423-
if groupIDStr == accountListGroupUngroupedQueryValue {
424-
groupID = service.AccountListGroupUngrouped
425-
} else {
426-
parsedGroupID, parseErr := strconv.ParseInt(groupIDStr, 10, 64)
427-
if parseErr != nil || parsedGroupID <= 0 {
428-
return nil, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter")
429-
}
430-
groupID = parsedGroupID
431-
}
408+
filters, err := parseAccountListQueryFilters(c)
409+
if err != nil {
410+
return nil, err
432411
}
433412

434-
return h.listAccountsFiltered(ctx, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder)
413+
return h.listAccountsFiltered(ctx, filters.platform, filters.accountType, filters.status, filters.schedulable, filters.search, filters.groupID, filters.privacyMode, filters.displayGroup, filters.namePrefix, filters.searchRegex, filters.sortBy, filters.sortOrder)
435414
}
436415

437416
func (h *AccountHandler) resolveExportProxies(ctx context.Context, accounts []service.Account) ([]service.Proxy, error) {

backend/internal/handler/admin/account_data_handler_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func TestExportDataPassesAccountFiltersAndSort(t *testing.T) {
181181
rec := httptest.NewRecorder()
182182
req := httptest.NewRequest(
183183
http.MethodGet,
184-
"/api/v1/admin/accounts/data?platform=openai&type=oauth&status=active&group=12&privacy_mode=blocked&search=keyword&sort_by=priority&sort_order=desc",
184+
"/api/v1/admin/accounts/data?platform=openai&type=oauth&status=active&schedulable=false&group=12&privacy_mode=blocked&display_group=pool-a&name_prefix=prod&search=keyword&search_regex=claude&sort_by=priority&sort_order=desc",
185185
nil,
186186
)
187187
router.ServeHTTP(rec, req)
@@ -191,9 +191,13 @@ func TestExportDataPassesAccountFiltersAndSort(t *testing.T) {
191191
require.Equal(t, "openai", adminSvc.lastListAccounts.platform)
192192
require.Equal(t, "oauth", adminSvc.lastListAccounts.accountType)
193193
require.Equal(t, "active", adminSvc.lastListAccounts.status)
194+
require.Equal(t, "false", adminSvc.lastListAccounts.schedulable)
194195
require.Equal(t, int64(12), adminSvc.lastListAccounts.groupID)
195196
require.Equal(t, "blocked", adminSvc.lastListAccounts.privacyMode)
197+
require.Equal(t, "pool-a", adminSvc.lastListAccounts.displayGroup)
198+
require.Equal(t, "prod", adminSvc.lastListAccounts.namePrefix)
196199
require.Equal(t, "keyword", adminSvc.lastListAccounts.search)
200+
require.Equal(t, "claude", adminSvc.lastListAccounts.searchRegex)
197201
require.Equal(t, "priority", adminSvc.lastListAccounts.sortBy)
198202
require.Equal(t, "desc", adminSvc.lastListAccounts.sortOrder)
199203
}

backend/internal/handler/admin/account_handler.go

Lines changed: 115 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"log"
1212
"log/slog"
1313
"net/http"
14+
"regexp"
1415
"strconv"
1516
"strings"
1617
"sync"
@@ -151,12 +152,16 @@ type BulkUpdateAccountsRequest struct {
151152
}
152153

153154
type BulkUpdateAccountFilters struct {
154-
Platform string `json:"platform"`
155-
Type string `json:"type"`
156-
Status string `json:"status"`
157-
Group string `json:"group"`
158-
Search string `json:"search"`
159-
PrivacyMode string `json:"privacy_mode"`
155+
Platform string `json:"platform"`
156+
Type string `json:"type"`
157+
Status string `json:"status"`
158+
Schedulable string `json:"schedulable"`
159+
Group string `json:"group"`
160+
Search string `json:"search"`
161+
PrivacyMode string `json:"privacy_mode"`
162+
DisplayGroup string `json:"display_group"`
163+
NamePrefix string `json:"name_prefix"`
164+
SearchRegex string `json:"search_regex"`
160165
}
161166

162167
// CheckMixedChannelRequest represents check mixed channel risk request
@@ -178,6 +183,90 @@ type AccountWithConcurrency struct {
178183

179184
const accountListGroupUngroupedQueryValue = "ungrouped"
180185

186+
type accountListQueryFilters struct {
187+
platform string
188+
accountType string
189+
status string
190+
schedulable string
191+
search string
192+
groupID int64
193+
privacyMode string
194+
displayGroup string
195+
namePrefix string
196+
searchRegex string
197+
sortBy string
198+
sortOrder string
199+
}
200+
201+
func normalizeSchedulableFilter(raw string) (string, error) {
202+
switch strings.ToLower(strings.TrimSpace(raw)) {
203+
case "", "all":
204+
return "", nil
205+
case "true", "1", "enabled", "on":
206+
return "true", nil
207+
case "false", "0", "disabled", "off":
208+
return "false", nil
209+
default:
210+
return "", infraerrors.BadRequest("INVALID_SCHEDULABLE_FILTER", "invalid schedulable filter")
211+
}
212+
}
213+
214+
func parseAccountListQueryFilters(c *gin.Context) (*accountListQueryFilters, error) {
215+
search := strings.TrimSpace(c.Query("search"))
216+
if len(search) > 100 {
217+
search = search[:100]
218+
}
219+
displayGroup := strings.TrimSpace(c.Query("display_group"))
220+
if len(displayGroup) > 100 {
221+
displayGroup = displayGroup[:100]
222+
}
223+
namePrefix := strings.TrimSpace(c.Query("name_prefix"))
224+
if len(namePrefix) > 100 {
225+
namePrefix = namePrefix[:100]
226+
}
227+
searchRegex := strings.TrimSpace(c.Query("search_regex"))
228+
if len(searchRegex) > 512 {
229+
return nil, infraerrors.BadRequest("INVALID_SEARCH_REGEX", "search regex is too long")
230+
}
231+
if searchRegex != "" {
232+
if _, err := regexp.Compile(searchRegex); err != nil {
233+
return nil, infraerrors.BadRequest("INVALID_SEARCH_REGEX", "invalid search regex")
234+
}
235+
}
236+
schedulable, err := normalizeSchedulableFilter(c.Query("schedulable"))
237+
if err != nil {
238+
return nil, err
239+
}
240+
241+
groupID := int64(0)
242+
if groupIDStr := c.Query("group"); groupIDStr != "" {
243+
if groupIDStr == accountListGroupUngroupedQueryValue {
244+
groupID = service.AccountListGroupUngrouped
245+
} else {
246+
parsedGroupID, parseErr := strconv.ParseInt(groupIDStr, 10, 64)
247+
if parseErr != nil || parsedGroupID < 0 {
248+
return nil, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter")
249+
}
250+
groupID = parsedGroupID
251+
}
252+
}
253+
254+
return &accountListQueryFilters{
255+
platform: c.Query("platform"),
256+
accountType: c.Query("type"),
257+
status: c.Query("status"),
258+
schedulable: schedulable,
259+
search: search,
260+
groupID: groupID,
261+
privacyMode: strings.TrimSpace(c.Query("privacy_mode")),
262+
displayGroup: displayGroup,
263+
namePrefix: namePrefix,
264+
searchRegex: searchRegex,
265+
sortBy: c.DefaultQuery("sort_by", "name"),
266+
sortOrder: c.DefaultQuery("sort_order", "asc"),
267+
}, nil
268+
}
269+
181270
func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
182271
item := AccountWithConcurrency{
183272
Account: dto.AccountFromService(account),
@@ -226,39 +315,13 @@ func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, ac
226315
// GET /api/v1/admin/accounts
227316
func (h *AccountHandler) List(c *gin.Context) {
228317
page, pageSize := response.ParsePagination(c)
229-
platform := c.Query("platform")
230-
accountType := c.Query("type")
231-
status := c.Query("status")
232-
search := c.Query("search")
233-
privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
234-
sortBy := c.DefaultQuery("sort_by", "name")
235-
sortOrder := c.DefaultQuery("sort_order", "asc")
236-
// 标准化和验证 search 参数
237-
search = strings.TrimSpace(search)
238-
if len(search) > 100 {
239-
search = search[:100]
318+
filters, err := parseAccountListQueryFilters(c)
319+
if err != nil {
320+
response.ErrorFrom(c, err)
321+
return
240322
}
241323
lite := parseBoolQueryWithDefault(c.Query("lite"), false)
242-
243-
var groupID int64
244-
if groupIDStr := c.Query("group"); groupIDStr != "" {
245-
if groupIDStr == accountListGroupUngroupedQueryValue {
246-
groupID = service.AccountListGroupUngrouped
247-
} else {
248-
parsedGroupID, parseErr := strconv.ParseInt(groupIDStr, 10, 64)
249-
if parseErr != nil {
250-
response.ErrorFrom(c, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter"))
251-
return
252-
}
253-
if parsedGroupID < 0 {
254-
response.ErrorFrom(c, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter"))
255-
return
256-
}
257-
groupID = parsedGroupID
258-
}
259-
}
260-
261-
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder)
324+
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, filters.platform, filters.accountType, filters.status, filters.schedulable, filters.search, filters.groupID, filters.privacyMode, filters.displayGroup, filters.namePrefix, filters.searchRegex, filters.sortBy, filters.sortOrder)
262325
if err != nil {
263326
response.ErrorFrom(c, err)
264327
return
@@ -380,7 +443,7 @@ func (h *AccountHandler) List(c *gin.Context) {
380443
result[i] = item
381444
}
382445

383-
etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search, lite)
446+
etag := buildAccountsListETag(result, total, page, pageSize, filters.platform, filters.accountType, filters.status, filters.schedulable, filters.search, lite)
384447
if etag != "" {
385448
c.Header("ETag", etag)
386449
c.Header("Vary", "If-None-Match")
@@ -397,7 +460,7 @@ func buildAccountsListETag(
397460
items []AccountWithConcurrency,
398461
total int64,
399462
page, pageSize int,
400-
platform, accountType, status, search string,
463+
platform, accountType, status, schedulable, search string,
401464
lite bool,
402465
) string {
403466
payload := struct {
@@ -407,6 +470,7 @@ func buildAccountsListETag(
407470
Platform string `json:"platform"`
408471
AccountType string `json:"type"`
409472
Status string `json:"status"`
473+
Schedulable string `json:"schedulable"`
410474
Search string `json:"search"`
411475
Lite bool `json:"lite"`
412476
Items []AccountWithConcurrency `json:"items"`
@@ -417,6 +481,7 @@ func buildAccountsListETag(
417481
Platform: platform,
418482
AccountType: accountType,
419483
Status: status,
484+
Schedulable: schedulable,
420485
Search: search,
421486
Lite: lite,
422487
Items: items,
@@ -1489,12 +1554,16 @@ func toServiceBulkUpdateAccountFilters(filters *BulkUpdateAccountFilters) *servi
14891554
return nil
14901555
}
14911556
return &service.BulkUpdateAccountFilters{
1492-
Platform: filters.Platform,
1493-
Type: filters.Type,
1494-
Status: filters.Status,
1495-
Group: filters.Group,
1496-
Search: filters.Search,
1497-
PrivacyMode: filters.PrivacyMode,
1557+
Platform: filters.Platform,
1558+
Type: filters.Type,
1559+
Status: filters.Status,
1560+
Schedulable: filters.Schedulable,
1561+
Group: filters.Group,
1562+
Search: filters.Search,
1563+
PrivacyMode: filters.PrivacyMode,
1564+
DisplayGroup: filters.DisplayGroup,
1565+
NamePrefix: filters.NamePrefix,
1566+
SearchRegex: filters.SearchRegex,
14981567
}
14991568
}
15001569

@@ -2107,7 +2176,7 @@ func (h *AccountHandler) BatchRefreshTier(c *gin.Context) {
21072176
accounts := make([]*service.Account, 0)
21082177

21092178
if len(req.AccountIDs) == 0 {
2110-
allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "", 0, "", "name", "asc")
2179+
allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "", "", 0, "", "", "", "", "name", "asc")
21112180
if err != nil {
21122181
response.ErrorFrom(c, err)
21132182
return

backend/internal/handler/admin/admin_service_stub_test.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,19 @@ type stubAdminService struct {
3434
groupIDs []int64
3535
}
3636
lastListAccounts struct {
37-
platform string
38-
accountType string
39-
status string
40-
search string
41-
groupID int64
42-
privacyMode string
43-
sortBy string
44-
sortOrder string
45-
calls int
37+
platform string
38+
accountType string
39+
status string
40+
schedulable string
41+
search string
42+
groupID int64
43+
privacyMode string
44+
displayGroup string
45+
namePrefix string
46+
searchRegex string
47+
sortBy string
48+
sortOrder string
49+
calls int
4650
}
4751
lastListUsers struct {
4852
page int
@@ -299,13 +303,17 @@ func (s *stubAdminService) BatchSetGroupRPMOverrides(_ context.Context, _ int64,
299303
return nil
300304
}
301305

302-
func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]service.Account, int64, error) {
306+
func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, schedulable, search string, groupID int64, privacyMode, displayGroup, namePrefix, searchRegex, sortBy, sortOrder string) ([]service.Account, int64, error) {
303307
s.lastListAccounts.platform = platform
304308
s.lastListAccounts.accountType = accountType
305309
s.lastListAccounts.status = status
310+
s.lastListAccounts.schedulable = schedulable
306311
s.lastListAccounts.search = search
307312
s.lastListAccounts.groupID = groupID
308313
s.lastListAccounts.privacyMode = privacyMode
314+
s.lastListAccounts.displayGroup = displayGroup
315+
s.lastListAccounts.namePrefix = namePrefix
316+
s.lastListAccounts.searchRegex = searchRegex
309317
s.lastListAccounts.sortBy = sortBy
310318
s.lastListAccounts.sortOrder = sortOrder
311319
s.lastListAccounts.calls++

0 commit comments

Comments
 (0)