feat(accounts): 账户列表显示实时并发数
- 在账户列表 API 返回中添加 current_concurrency 字段 - 合并平台和类型列为 PlatformTypeBadge 组件,节省表格空间 - 新增并发状态列,显示 当前/最大 并发数,支持颜色编码
This commit is contained in:
@@ -84,7 +84,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher)
|
||||
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
||||
accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, httpUpstream)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService)
|
||||
concurrencyCache := repository.NewConcurrencyCache(client)
|
||||
concurrencyService := service.NewConcurrencyService(concurrencyCache)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService)
|
||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
|
||||
proxyHandler := admin.NewProxyHandler(adminService)
|
||||
@@ -108,8 +110,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
identityCache := repository.NewIdentityCache(client)
|
||||
identityService := service.NewIdentityService(identityCache)
|
||||
gatewayService := service.NewGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, billingService, rateLimitService, billingCacheService, identityService, httpUpstream)
|
||||
concurrencyCache := repository.NewConcurrencyCache(client)
|
||||
concurrencyService := service.NewConcurrencyService(concurrencyCache)
|
||||
gatewayHandler := handler.NewGatewayHandler(gatewayService, userService, concurrencyService, billingCacheService)
|
||||
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, billingService, rateLimitService, billingCacheService, httpUpstream)
|
||||
openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService)
|
||||
|
||||
@@ -3,6 +3,7 @@ package admin
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"sub2api/internal/model"
|
||||
"sub2api/internal/pkg/claude"
|
||||
"sub2api/internal/pkg/openai"
|
||||
"sub2api/internal/pkg/response"
|
||||
@@ -32,10 +33,11 @@ type AccountHandler struct {
|
||||
rateLimitService *service.RateLimitService
|
||||
accountUsageService *service.AccountUsageService
|
||||
accountTestService *service.AccountTestService
|
||||
concurrencyService *service.ConcurrencyService
|
||||
}
|
||||
|
||||
// NewAccountHandler creates a new admin account handler
|
||||
func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService) *AccountHandler {
|
||||
func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, openaiOAuthService *service.OpenAIOAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService, concurrencyService *service.ConcurrencyService) *AccountHandler {
|
||||
return &AccountHandler{
|
||||
adminService: adminService,
|
||||
oauthService: oauthService,
|
||||
@@ -43,6 +45,7 @@ func NewAccountHandler(adminService service.AdminService, oauthService *service.
|
||||
rateLimitService: rateLimitService,
|
||||
accountUsageService: accountUsageService,
|
||||
accountTestService: accountTestService,
|
||||
concurrencyService: concurrencyService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +76,12 @@ type UpdateAccountRequest struct {
|
||||
GroupIDs *[]int64 `json:"group_ids"`
|
||||
}
|
||||
|
||||
// AccountWithConcurrency extends Account with real-time concurrency info
|
||||
type AccountWithConcurrency struct {
|
||||
*model.Account
|
||||
CurrentConcurrency int `json:"current_concurrency"`
|
||||
}
|
||||
|
||||
// List handles listing all accounts with pagination
|
||||
// GET /api/v1/admin/accounts
|
||||
func (h *AccountHandler) List(c *gin.Context) {
|
||||
@@ -88,7 +97,28 @@ func (h *AccountHandler) List(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response.Paginated(c, accounts, total, page, pageSize)
|
||||
// Get current concurrency counts for all accounts
|
||||
accountIDs := make([]int64, len(accounts))
|
||||
for i, acc := range accounts {
|
||||
accountIDs[i] = acc.ID
|
||||
}
|
||||
|
||||
concurrencyCounts, err := h.concurrencyService.GetAccountConcurrencyBatch(c.Request.Context(), accountIDs)
|
||||
if err != nil {
|
||||
// Log error but don't fail the request, just use 0 for all
|
||||
concurrencyCounts = make(map[int64]int)
|
||||
}
|
||||
|
||||
// Build response with concurrency info
|
||||
result := make([]AccountWithConcurrency, len(accounts))
|
||||
for i := range accounts {
|
||||
result[i] = AccountWithConcurrency{
|
||||
Account: &accounts[i],
|
||||
CurrentConcurrency: concurrencyCounts[accounts[i].ID],
|
||||
}
|
||||
}
|
||||
|
||||
response.Paginated(c, result, total, page, pageSize)
|
||||
}
|
||||
|
||||
// GetByID handles getting an account by ID
|
||||
|
||||
@@ -147,3 +147,20 @@ func CalculateMaxWait(userConcurrency int) int {
|
||||
}
|
||||
return userConcurrency + defaultExtraWaitSlots
|
||||
}
|
||||
|
||||
// GetAccountConcurrencyBatch gets current concurrency counts for multiple accounts
|
||||
// Returns a map of accountID -> current concurrency count
|
||||
func (s *ConcurrencyService) GetAccountConcurrencyBatch(ctx context.Context, accountIDs []int64) (map[int64]int, error) {
|
||||
result := make(map[int64]int)
|
||||
|
||||
for _, accountID := range accountIDs {
|
||||
count, err := s.cache.GetAccountConcurrency(ctx, accountID)
|
||||
if err != nil {
|
||||
// If key doesn't exist in Redis, count is 0
|
||||
count = 0
|
||||
}
|
||||
result[accountID] = count
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
75
frontend/src/components/common/PlatformTypeBadge.vue
Normal file
75
frontend/src/components/common/PlatformTypeBadge.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="inline-flex items-center rounded-md overflow-hidden text-xs font-medium">
|
||||
<!-- Platform part -->
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1 px-2 py-1',
|
||||
platformClass
|
||||
]"
|
||||
>
|
||||
<PlatformIcon :platform="platform" size="xs" />
|
||||
<span>{{ platformLabel }}</span>
|
||||
</span>
|
||||
<!-- Type part -->
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1 px-1.5 py-1',
|
||||
typeClass
|
||||
]"
|
||||
>
|
||||
<!-- OAuth icon -->
|
||||
<svg v-if="type === 'oauth'" class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<!-- Setup Token icon -->
|
||||
<svg v-else-if="type === 'setup-token'" class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</svg>
|
||||
<!-- API Key icon -->
|
||||
<svg v-else class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
<span>{{ typeLabel }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { AccountPlatform, AccountType } from '@/types'
|
||||
import PlatformIcon from './PlatformIcon.vue'
|
||||
|
||||
interface Props {
|
||||
platform: AccountPlatform
|
||||
type: AccountType
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const platformLabel = computed(() => {
|
||||
return props.platform === 'anthropic' ? 'Anthropic' : 'OpenAI'
|
||||
})
|
||||
|
||||
const typeLabel = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'oauth': return 'OAuth'
|
||||
case 'setup-token': return 'Token'
|
||||
case 'apikey': return 'Key'
|
||||
default: return props.type
|
||||
}
|
||||
})
|
||||
|
||||
const platformClass = computed(() => {
|
||||
if (props.platform === 'anthropic') {
|
||||
return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
}
|
||||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||
})
|
||||
|
||||
const typeClass = computed(() => {
|
||||
if (props.platform === 'anthropic') {
|
||||
return 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
}
|
||||
return 'bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||
})
|
||||
</script>
|
||||
@@ -702,8 +702,10 @@ export default {
|
||||
},
|
||||
columns: {
|
||||
name: 'Name',
|
||||
platformType: 'Platform/Type',
|
||||
platform: 'Platform',
|
||||
type: 'Type',
|
||||
concurrencyStatus: 'Concurrency',
|
||||
status: 'Status',
|
||||
schedulable: 'Schedule',
|
||||
todayStats: "Today's Stats",
|
||||
|
||||
@@ -796,8 +796,10 @@ export default {
|
||||
failedToToggleSchedulable: '切换调度状态失败',
|
||||
columns: {
|
||||
name: '名称',
|
||||
platformType: '平台/类型',
|
||||
platform: '平台',
|
||||
type: '类型',
|
||||
concurrencyStatus: '并发',
|
||||
priority: '优先级',
|
||||
weight: '权重',
|
||||
status: '状态',
|
||||
|
||||
@@ -319,6 +319,7 @@ export interface Account {
|
||||
extra?: CodexUsageSnapshot & Record<string, unknown>; // Extra fields including Codex usage
|
||||
proxy_id: number | null;
|
||||
concurrency: number;
|
||||
current_concurrency?: number; // Real-time concurrency count from Redis
|
||||
priority: number;
|
||||
status: 'active' | 'inactive' | 'error';
|
||||
error_message: string | null;
|
||||
|
||||
@@ -73,29 +73,30 @@
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-platform="{ value }">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||
value === 'anthropic'
|
||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||
]"
|
||||
>
|
||||
<PlatformIcon :platform="value" size="xs" />
|
||||
{{ value === 'anthropic' ? 'Anthropic' : 'OpenAI' }}
|
||||
</span>
|
||||
<template #cell-platform_type="{ row }">
|
||||
<PlatformTypeBadge :platform="row.platform" :type="row.type" />
|
||||
</template>
|
||||
|
||||
<template #cell-type="{ value }">
|
||||
<span
|
||||
:class="[
|
||||
'badge',
|
||||
value === 'oauth' ? 'badge-primary' : value === 'setup-token' ? 'badge-info' : 'badge-purple'
|
||||
]"
|
||||
>
|
||||
{{ value === 'oauth' ? 'Oauth' : value === 'setup-token' ? t('admin.accounts.setupToken') : t('admin.accounts.apiKey') }}
|
||||
</span>
|
||||
<template #cell-concurrency="{ row }">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium',
|
||||
(row.current_concurrency || 0) >= row.concurrency
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: (row.current_concurrency || 0) > 0
|
||||
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
||||
</svg>
|
||||
<span class="font-mono">{{ row.current_concurrency || 0 }}</span>
|
||||
<span class="text-gray-400 dark:text-gray-500">/</span>
|
||||
<span class="font-mono">{{ row.concurrency }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
@@ -336,7 +337,7 @@ import AccountUsageCell from '@/components/account/AccountUsageCell.vue'
|
||||
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
|
||||
import AccountTestModal from '@/components/account/AccountTestModal.vue'
|
||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||
import PlatformIcon from '@/components/common/PlatformIcon.vue'
|
||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||
import { formatRelativeTime } from '@/utils/format'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -345,8 +346,8 @@ const appStore = useAppStore()
|
||||
// Table columns
|
||||
const columns = computed<Column[]>(() => [
|
||||
{ key: 'name', label: t('admin.accounts.columns.name'), sortable: true },
|
||||
{ key: 'platform', label: t('admin.accounts.columns.platform'), sortable: true },
|
||||
{ key: 'type', label: t('admin.accounts.columns.type'), sortable: true },
|
||||
{ key: 'platform_type', label: t('admin.accounts.columns.platformType'), sortable: false },
|
||||
{ key: 'concurrency', label: t('admin.accounts.columns.concurrencyStatus'), sortable: false },
|
||||
{ key: 'status', label: t('admin.accounts.columns.status'), sortable: true },
|
||||
{ key: 'schedulable', label: t('admin.accounts.columns.schedulable'), sortable: true },
|
||||
{ key: 'today_stats', label: t('admin.accounts.columns.todayStats'), sortable: false },
|
||||
|
||||
Reference in New Issue
Block a user