feat: 全局模型新增活跃 Provider 数量统计

- 后端优化查询,使用条件聚合同时获取总 Provider 数和活跃数
- 前端展示格式改为「活跃数/总数」,便于了解可用 Provider 情况
This commit is contained in:
fawney19
2026-02-09 22:26:30 +08:00
parent 7b28680cda
commit b11e9de6a7
5 changed files with 64 additions and 9 deletions

View File

@@ -810,6 +810,7 @@ export interface GlobalModelResponse {
config?: Record<string, any> | null config?: Record<string, any> | null
// 统计数据 // 统计数据
provider_count?: number provider_count?: number
active_provider_count?: number
usage_count?: number usage_count?: number
created_at: string created_at: string
updated_at?: string updated_at?: string

View File

@@ -386,7 +386,7 @@
<Building2 class="w-4 h-4 text-muted-foreground" /> <Building2 class="w-4 h-4 text-muted-foreground" />
</div> </div>
<p class="text-2xl font-bold mt-1"> <p class="text-2xl font-bold mt-1">
{{ model.provider_count || 0 }} {{ model.active_provider_count || 0 }}<span class="text-sm text-muted-foreground font-normal">/{{ model.provider_count || 0 }}</span>
</p> </p>
</div> </div>
<div class="p-3 rounded-lg border bg-muted/20"> <div class="p-3 rounded-lg border bg-muted/20">

View File

@@ -168,7 +168,7 @@
</TableCell> </TableCell>
<TableCell class="text-center"> <TableCell class="text-center">
<Badge variant="secondary"> <Badge variant="secondary">
{{ model.provider_count || 0 }} {{ model.active_provider_count || 0 }}/{{ model.provider_count || 0 }}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell class="text-center"> <TableCell class="text-center">
@@ -302,7 +302,7 @@
<!-- 第三行统计信息 --> <!-- 第三行统计信息 -->
<div class="flex flex-wrap items-center gap-3 text-xs text-muted-foreground"> <div class="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
<span>提供商 {{ model.provider_count || 0 }}</span> <span>提供商 {{ model.active_provider_count || 0 }}/{{ model.provider_count || 0 }}</span>
<span>调用 {{ formatUsageCount(model.usage_count || 0) }}</span> <span>调用 {{ formatUsageCount(model.usage_count || 0) }}</span>
<span <span
v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')" v-if="getFirstTierPrice(model, 'input') || getFirstTierPrice(model, 'output')"

View File

@@ -261,9 +261,9 @@ class AdminListGlobalModelsAdapter(AdminApiAdapter):
search: str | None search: str | None
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
from sqlalchemy import func from sqlalchemy import and_, case, func
from src.models.database import Model from src.models.database import Model, Provider
models = GlobalModelService.list_global_models( models = GlobalModelService.list_global_models(
db=context.db, db=context.db,
@@ -274,24 +274,44 @@ class AdminListGlobalModelsAdapter(AdminApiAdapter):
) )
# 一次性查询所有 GlobalModel 的 provider_count优化 N+1 问题) # 一次性查询所有 GlobalModel 的 provider_count优化 N+1 问题)
# 用条件聚合同时获取总数和活跃数,减少一次 DB 往返
model_ids = [gm.id for gm in models] model_ids = [gm.id for gm in models]
provider_counts = {} provider_counts = {}
active_provider_counts = {}
if model_ids: if model_ids:
count_results = ( count_results = (
context.db.query( context.db.query(
Model.global_model_id, func.count(func.distinct(Model.provider_id)) Model.global_model_id,
func.count(func.distinct(Model.provider_id)),
func.count(
func.distinct(
case(
(
and_(
Model.is_active.is_(True),
Provider.is_active.is_(True),
),
Model.provider_id,
),
else_=None,
)
)
),
) )
.join(Provider, Model.provider_id == Provider.id)
.filter(Model.global_model_id.in_(model_ids)) .filter(Model.global_model_id.in_(model_ids))
.group_by(Model.global_model_id) .group_by(Model.global_model_id)
.all() .all()
) )
provider_counts = {gm_id: count for gm_id, count in count_results} provider_counts = {gm_id: total for gm_id, total, _ in count_results}
active_provider_counts = {gm_id: active for gm_id, _, active in count_results}
# 构建响应 # 构建响应
model_responses = [] model_responses = []
for gm in models: for gm in models:
response = GlobalModelResponse.model_validate(gm) response = GlobalModelResponse.model_validate(gm)
response.provider_count = provider_counts.get(gm.id, 0) response.provider_count = provider_counts.get(gm.id, 0)
response.active_provider_count = active_provider_counts.get(gm.id, 0)
model_responses.append(response) model_responses.append(response)
return GlobalModelListResponse( return GlobalModelListResponse(
@@ -307,11 +327,44 @@ class AdminGetGlobalModelAdapter(AdminApiAdapter):
global_model_id: str global_model_id: str
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override] async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
from sqlalchemy import and_, case, func
from src.models.database import Model, Provider
global_model = GlobalModelService.get_global_model(context.db, self.global_model_id) global_model = GlobalModelService.get_global_model(context.db, self.global_model_id)
stats = GlobalModelService.get_global_model_stats(context.db, self.global_model_id) stats = GlobalModelService.get_global_model_stats(context.db, self.global_model_id)
# 查询 provider_count 和 active_provider_count与列表 API 一致)
count_row = (
context.db.query(
func.count(func.distinct(Model.provider_id)),
func.count(
func.distinct(
case(
(
and_(
Model.is_active.is_(True),
Provider.is_active.is_(True),
),
Model.provider_id,
),
else_=None,
)
)
),
)
.join(Provider, Model.provider_id == Provider.id)
.filter(Model.global_model_id == global_model.id)
.first()
)
total_count, active_count = count_row if count_row else (0, 0)
response = GlobalModelResponse.model_validate(global_model)
response.provider_count = total_count
response.active_provider_count = active_count
return GlobalModelWithStats( return GlobalModelWithStats(
**GlobalModelResponse.model_validate(global_model).model_dump(), **response.model_dump(),
total_models=stats["total_models"], total_models=stats["total_models"],
total_providers=stats["total_providers"], total_providers=stats["total_providers"],
price_range=stats["price_range"], price_range=stats["price_range"],

View File

@@ -236,7 +236,8 @@ class GlobalModelResponse(BaseModel):
default=None, description="模型配置streaming, vision, context_limit, description 等)" default=None, description="模型配置streaming, vision, context_limit, description 等)"
) )
# 统计数据(可选) # 统计数据(可选)
provider_count: int | None = Field(default=0, description="支持的 Provider 数量") provider_count: int | None = Field(default=0, description="支持的 Provider 数量")
active_provider_count: int | None = Field(default=0, description="可用的 Provider 数量")
usage_count: int | None = Field(default=0, description="调用次数") usage_count: int | None = Field(default=0, description="调用次数")
created_at: datetime created_at: datetime
updated_at: datetime | None updated_at: datetime | None