feat(ui): 用户列表页显示当前并发数
优化 /admin/users 页面的并发数列,显示「当前/最大」格式, 参考 AccountCapacityCell 的设计风格。 - 后端 UserHandler 注入 ConcurrencyService,批量查询用户当前并发数 - 新增 UserConcurrencyCell 组件,支持颜色状态(空闲灰/使用中黄/满载红) - 前端 AdminUser 类型添加 current_concurrency 字段
This commit is contained in:
@@ -102,7 +102,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
||||||
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
||||||
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator)
|
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator)
|
||||||
adminUserHandler := admin.NewUserHandler(adminService)
|
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
||||||
|
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
||||||
|
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService)
|
||||||
groupHandler := admin.NewGroupHandler(adminService)
|
groupHandler := admin.NewGroupHandler(adminService)
|
||||||
claudeOAuthClient := repository.NewClaudeOAuthClient()
|
claudeOAuthClient := repository.NewClaudeOAuthClient()
|
||||||
oAuthService := service.NewOAuthService(proxyRepository, claudeOAuthClient)
|
oAuthService := service.NewOAuthService(proxyRepository, claudeOAuthClient)
|
||||||
@@ -126,13 +128,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache)
|
accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache)
|
||||||
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
||||||
gatewayCache := repository.NewGatewayCache(redisClient)
|
gatewayCache := repository.NewGatewayCache(redisClient)
|
||||||
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
|
||||||
schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
|
schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
|
||||||
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
|
schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
|
||||||
|
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
||||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
||||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
||||||
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
|
||||||
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
|
||||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||||
sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
|
sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
|
||||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, compositeTokenCacheInvalidator)
|
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, compositeTokenCacheInvalidator)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
|
|||||||
router := gin.New()
|
router := gin.New()
|
||||||
adminSvc := newStubAdminService()
|
adminSvc := newStubAdminService()
|
||||||
|
|
||||||
userHandler := NewUserHandler(adminSvc)
|
userHandler := NewUserHandler(adminSvc, nil)
|
||||||
groupHandler := NewGroupHandler(adminSvc)
|
groupHandler := NewGroupHandler(adminSvc)
|
||||||
proxyHandler := NewProxyHandler(adminSvc)
|
proxyHandler := NewProxyHandler(adminSvc)
|
||||||
redeemHandler := NewRedeemHandler(adminSvc)
|
redeemHandler := NewRedeemHandler(adminSvc)
|
||||||
|
|||||||
@@ -11,15 +11,23 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UserWithConcurrency wraps AdminUser with current concurrency info
|
||||||
|
type UserWithConcurrency struct {
|
||||||
|
dto.AdminUser
|
||||||
|
CurrentConcurrency int `json:"current_concurrency"`
|
||||||
|
}
|
||||||
|
|
||||||
// UserHandler handles admin user management
|
// UserHandler handles admin user management
|
||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
adminService service.AdminService
|
adminService service.AdminService
|
||||||
|
concurrencyService *service.ConcurrencyService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserHandler creates a new admin user handler
|
// NewUserHandler creates a new admin user handler
|
||||||
func NewUserHandler(adminService service.AdminService) *UserHandler {
|
func NewUserHandler(adminService service.AdminService, concurrencyService *service.ConcurrencyService) *UserHandler {
|
||||||
return &UserHandler{
|
return &UserHandler{
|
||||||
adminService: adminService,
|
adminService: adminService,
|
||||||
|
concurrencyService: concurrencyService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,10 +95,30 @@ func (h *UserHandler) List(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.AdminUser, 0, len(users))
|
// Batch get current concurrency (nil map if unavailable)
|
||||||
for i := range users {
|
var loadInfo map[int64]*service.UserLoadInfo
|
||||||
out = append(out, *dto.UserFromServiceAdmin(&users[i]))
|
if len(users) > 0 && h.concurrencyService != nil {
|
||||||
|
usersConcurrency := make([]service.UserWithConcurrency, len(users))
|
||||||
|
for i := range users {
|
||||||
|
usersConcurrency[i] = service.UserWithConcurrency{
|
||||||
|
ID: users[i].ID,
|
||||||
|
MaxConcurrency: users[i].Concurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadInfo, _ = h.concurrencyService.GetUsersLoadBatch(c.Request.Context(), usersConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build response with concurrency info
|
||||||
|
out := make([]UserWithConcurrency, len(users))
|
||||||
|
for i := range users {
|
||||||
|
out[i] = UserWithConcurrency{
|
||||||
|
AdminUser: *dto.UserFromServiceAdmin(&users[i]),
|
||||||
|
}
|
||||||
|
if info := loadInfo[users[i].ID]; info != nil {
|
||||||
|
out[i].CurrentConcurrency = info.CurrentConcurrency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.Paginated(c, out, total, page, pageSize)
|
response.Paginated(c, out, total, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
frontend/src/components/user/UserConcurrencyCell.vue
Normal file
43
frontend/src/components/user/UserConcurrencyCell.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium',
|
||||||
|
statusClass
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- Four-square grid icon -->
|
||||||
|
<svg class="h-3 w-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">{{ current }}</span>
|
||||||
|
<span class="text-gray-400 dark:text-gray-500">/</span>
|
||||||
|
<span class="font-mono">{{ max }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
current: number
|
||||||
|
max: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Status color based on usage
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const { current, max } = props
|
||||||
|
|
||||||
|
// Full: red
|
||||||
|
if (current >= max && max > 0) {
|
||||||
|
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||||
|
}
|
||||||
|
// In use: yellow
|
||||||
|
if (current > 0) {
|
||||||
|
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||||||
|
}
|
||||||
|
// Idle: gray
|
||||||
|
return 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -43,6 +43,8 @@ export interface AdminUser extends User {
|
|||||||
notes: string
|
notes: string
|
||||||
// 用户专属分组倍率配置 (group_id -> rate_multiplier)
|
// 用户专属分组倍率配置 (group_id -> rate_multiplier)
|
||||||
group_rates?: Record<number, number>
|
group_rates?: Record<number, number>
|
||||||
|
// 当前并发数(仅管理员列表接口返回)
|
||||||
|
current_concurrency?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
|
|||||||
@@ -342,8 +342,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-concurrency="{ value }">
|
<template #cell-concurrency="{ row }">
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ value }}</span>
|
<UserConcurrencyCell
|
||||||
|
:current="row.current_concurrency ?? 0"
|
||||||
|
:max="row.concurrency"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #cell-status="{ value }">
|
<template #cell-status="{ value }">
|
||||||
@@ -535,6 +538,7 @@ import EmptyState from '@/components/common/EmptyState.vue'
|
|||||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import UserAttributesConfigModal from '@/components/user/UserAttributesConfigModal.vue'
|
import UserAttributesConfigModal from '@/components/user/UserAttributesConfigModal.vue'
|
||||||
|
import UserConcurrencyCell from '@/components/user/UserConcurrencyCell.vue'
|
||||||
import UserCreateModal from '@/components/admin/user/UserCreateModal.vue'
|
import UserCreateModal from '@/components/admin/user/UserCreateModal.vue'
|
||||||
import UserEditModal from '@/components/admin/user/UserEditModal.vue'
|
import UserEditModal from '@/components/admin/user/UserEditModal.vue'
|
||||||
import UserApiKeysModal from '@/components/admin/user/UserApiKeysModal.vue'
|
import UserApiKeysModal from '@/components/admin/user/UserApiKeysModal.vue'
|
||||||
|
|||||||
Reference in New Issue
Block a user