Files
taiji-AI-PAD/services/mcp-server/app/routes/admin.py
T
2025-12-26 04:14:27 +00:00

982 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
超级管理员API路由
"""
from datetime import datetime, timedelta
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select, func, and_, desc, or_
from sqlalchemy.ext.asyncio import AsyncSession
import uuid
from database import get_db
from models import (
User, Channel, Agent, ResourceAllocation,
BillingRecord, Application, ModelProvider
)
from app.auth import require_auth, get_password_hash
from app.schemas import (
SuccessResponse,
CreateAdminRequest,
CreateChannelRequest,
UpdateChannelRequest,
UpdateAgentConfigRequest,
ChannelInfo,
ChannelResourceAllocation,
ApplicationInfo,
ReviewApplicationRequest,
AdminBillingResponse,
)
from app.permissions import has_permission, get_role_permissions
router = APIRouter(prefix="/api/admin", tags=["超级管理员"])
def _get_role(principal: dict) -> str:
"""从principal获取角色"""
return principal.get("claims", {}).get("role", "")
def _verify_permission(principal: dict, permission: str):
"""验证是否拥有指定权限"""
role = _get_role(principal)
if not has_permission(role, permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"需要权限: {permission}"
)
def _verify_super_admin_permission(principal: dict):
"""验证超级管理员权限(仅 super_admin 可管理管理员)"""
role = _get_role(principal)
if role != "super_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要超级管理员权限"
)
def _verify_write_permission(principal: dict):
"""验证写入权限(super_admin 或 billing_admin)"""
role = _get_role(principal)
# 只有 super_admin 和 billing_admin 有写入权限
if role not in ["super_admin", "billing_admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要写入权限(super_admin 或 billing_admin)"
)
def _verify_read_permission(principal: dict):
"""验证读取权限(super_admin, billing_admin, operations_admin)"""
role = _get_role(principal)
if role not in ["super_admin", "billing_admin", "operations_admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限"
)
# ============= 管理员管理 =============
@router.get("/admins", response_model=SuccessResponse)
async def list_admins(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取管理员列表(仅超级管理员可用)
"""
_verify_super_admin_permission(principal)
# 查询所有管理员角色的用户
result = await db.execute(
select(User).where(
User.role.in_(["billing_admin", "operations_admin"])
)
)
admins = result.scalars().all()
data = [
{
"id": str(admin.id),
"name": admin.name or admin.full_name,
"email": admin.email,
"role": admin.role,
"status": admin.status,
"createdAt": admin.created_at.isoformat() if admin.created_at else None,
}
for admin in admins
]
return SuccessResponse(data={"admins": data})
@router.post("/admins/create", response_model=SuccessResponse)
async def create_admin(
req: CreateAdminRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
创建管理员(仅超级管理员可用)
可创建的角色:
- billing_admin: 计费管理员(完整写入权限,可创建渠道、管理租户、计费操作)
- operations_admin: 运维管理员(只读权限,仅查看和监控)
"""
_verify_super_admin_permission(principal)
# 检查邮箱是否已存在
result = await db.execute(
select(User).where(User.email == req.email)
)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被使用"
)
# 创建管理员用户
password_hash = get_password_hash(req.password)
admin = User(
name=req.name,
email=req.email,
password_hash=password_hash,
hashed_password=password_hash, # 兼容
username=req.email.split("@")[0],
full_name=req.name,
role=req.role,
status="active",
balance=0,
credit_limit=0,
)
db.add(admin)
await db.commit()
await db.refresh(admin)
return SuccessResponse(
data={
"id": str(admin.id),
"name": admin.name,
"email": admin.email,
"role": admin.role,
},
message=f"管理员创建成功"
)
@router.delete("/admins/{admin_id}", response_model=SuccessResponse)
async def delete_admin(
admin_id: str,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
删除管理员(仅超级管理员可用,软删除)
"""
_verify_super_admin_permission(principal)
# 验证管理员存在
result = await db.execute(
select(User).where(User.id == admin_id)
)
admin = result.scalar_one_or_none()
if not admin:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="管理员不存在"
)
# 检查是否是管理员角色
if admin.role not in ["billing_admin", "operations_admin"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="只能删除管理员角色的用户"
)
# 软删除:标记为不活跃
admin.status = "inactive"
await db.commit()
return SuccessResponse(
data={"id": str(admin.id)},
message="管理员已删除"
)
# ============= 概览 =============
@router.get("/dashboard/stats", response_model=SuccessResponse)
async def get_admin_dashboard_stats(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取平台全局统计(所有管理员可查看)
"""
_verify_read_permission(principal)
# 渠道总数
channels_count = await db.execute(select(func.count(Channel.id)))
total_channels = channels_count.scalar() or 0
# 租户总数
tenants_count = await db.execute(
select(func.count(User.id)).where(User.role == "user")
)
total_tenants = tenants_count.scalar() or 0
# Agent总数
agents_count = await db.execute(select(func.count(Agent.id)))
total_agents = agents_count.scalar() or 0
# 总调用次数
calls_count = await db.execute(select(func.count(BillingRecord.id)))
total_calls = calls_count.scalar() or 0
# 总收入
revenue = await db.execute(select(func.sum(BillingRecord.cost)))
total_revenue = float(revenue.scalar() or 0)
return SuccessResponse(
data={
"totalChannels": total_channels,
"totalTenants": total_tenants,
"totalAgents": total_agents,
"totalCalls": total_calls,
"totalRevenue": total_revenue,
}
)
# ============= 渠道管理 =============
@router.get("/channels", response_model=SuccessResponse)
async def list_channels(
include_inactive: bool = Query(False, description="是否包含已删除(inactive)的渠道"),
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取所有渠道列表(所有管理员可查看)
Args:
include_inactive: 是否包含已删除的渠道,默认False只返回活跃的
"""
_verify_read_permission(principal)
# 默认只返回活跃的渠道,除非明确请求包含已删除的
if include_inactive:
result = await db.execute(select(Channel))
else:
result = await db.execute(
select(Channel).where(Channel.status != "inactive")
)
channels = result.scalars().all()
data = [
{
"id": str(channel.id),
"name": channel.name,
"email": channel.email,
"commissionRate": float(channel.commission_rate),
"channelCredit": float(channel.channel_credit),
"customAgentCpu": float(channel.custom_agent_cpu),
"customAgentMemory": float(channel.custom_agent_memory),
"status": channel.status,
"createdAt": channel.created_at.isoformat(),
}
for channel in channels
]
return SuccessResponse(data={"channels": data})
@router.post("/channels/create", response_model=SuccessResponse)
async def create_channel(
req: CreateChannelRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
创建渠道(super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
# 检查邮箱是否已存在
result = await db.execute(
select(Channel).where(Channel.email == req.email)
)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被使用"
)
# 创建渠道
password_hash = get_password_hash(req.password)
channel = Channel(
name=req.name,
email=req.email,
password_hash=password_hash,
commission_rate=req.commissionRate,
channel_credit=0,
custom_agent_cpu=2,
custom_agent_memory=4,
status="active",
)
db.add(channel)
await db.commit()
await db.refresh(channel)
return SuccessResponse(
data={
"id": str(channel.id),
"name": channel.name,
"email": channel.email,
},
message="渠道创建成功"
)
@router.put("/channels/{channel_id}", response_model=SuccessResponse)
async def update_channel(
channel_id: str,
req: UpdateChannelRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
更新渠道信息(super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
# 验证渠道存在
result = await db.execute(
select(Channel).where(Channel.id == channel_id)
)
channel = result.scalar_one_or_none()
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="渠道不存在"
)
# 如果更新邮箱,检查邮箱是否已被其他渠道使用
if req.email and req.email != channel.email:
email_check = await db.execute(
select(Channel).where(
and_(
Channel.email == req.email,
Channel.id != channel_id
)
)
)
if email_check.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被其他渠道使用"
)
channel.email = req.email
# 更新其他字段
if req.name is not None:
channel.name = req.name
if req.commissionRate is not None:
channel.commission_rate = req.commissionRate
if req.status is not None:
channel.status = req.status
await db.commit()
await db.refresh(channel)
return SuccessResponse(
data={
"id": str(channel.id),
"name": channel.name,
"email": channel.email,
"commissionRate": float(channel.commission_rate),
"status": channel.status,
},
message="渠道信息更新成功"
)
@router.delete("/channels/{channel_id}", response_model=SuccessResponse)
async def delete_channel(
channel_id: str,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
删除渠道(软删除,super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
# 验证渠道存在
result = await db.execute(
select(Channel).where(Channel.id == channel_id)
)
channel = result.scalar_one_or_none()
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="渠道不存在"
)
# 检查是否有关联的租户
tenants_result = await db.execute(
select(func.count(User.id)).where(
and_(
User.channel_id == channel_id,
User.role == "user",
User.status == "active"
)
)
)
tenant_count = tenants_result.scalar() or 0
if tenant_count > 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"渠道下有 {tenant_count} 个活跃租户,无法删除。请先移除或停用所有租户。"
)
# 软删除:标记为不活跃
channel.status = "inactive"
await db.commit()
return SuccessResponse(
data={"id": str(channel.id)},
message="渠道已删除"
)
@router.put("/channels/{channel_id}/resources", response_model=SuccessResponse)
async def allocate_channel_resources(
channel_id: str,
req: ChannelResourceAllocation,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
统一管理渠道资源(super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
# 验证渠道
result = await db.execute(
select(Channel).where(Channel.id == channel_id)
)
channel = result.scalar_one_or_none()
if not channel:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="渠道不存在"
)
# 删除现有资源分配
await db.execute(
select(ResourceAllocation).where(
and_(
ResourceAllocation.target_id == channel_id,
ResourceAllocation.target_type == "channel"
)
)
)
# 分配模型供应商资源
for model_id in req.models:
allocation = ResourceAllocation(
target_id=channel_id,
target_type="channel",
resource_type="model",
resource_id=model_id,
)
db.add(allocation)
# 分配Agent资源
for agent_alloc in req.agents:
allocation = ResourceAllocation(
target_id=channel_id,
target_type="channel",
resource_type="agent",
resource_id=agent_alloc.agentId,
quantity=agent_alloc.quantity,
)
db.add(allocation)
# 更新自定义Agent资源和授信额度
if req.customAgentResources:
channel.custom_agent_cpu = req.customAgentResources.cpu
channel.custom_agent_memory = req.customAgentResources.memory
channel.channel_credit = req.channelCredit
await db.commit()
return SuccessResponse(message="渠道资源分配成功")
# ============= 申请审批 =============
@router.get("/channels/applications", response_model=SuccessResponse)
async def list_applications(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取所有渠道申请(所有管理员可查看)
"""
_verify_read_permission(principal)
result = await db.execute(
select(Application, Channel)
.join(Channel, Application.channel_id == Channel.id)
.order_by(desc(Application.created_at))
)
data = []
for app, channel in result.all():
details = {}
if app.type == "model":
details = {
"modelName": app.model_name,
"rpm": app.rpm,
"tpm": app.tpm,
}
else:
details = {
"agentType": app.agent_type,
"quantity": app.quantity,
}
data.append({
"id": str(app.id),
"channelId": str(app.channel_id),
"channelName": channel.name,
"type": app.type,
"details": details,
"reason": app.reason or "",
"status": app.status,
"createdAt": app.created_at.isoformat(),
})
return SuccessResponse(data={"data": data})
@router.put("/channels/applications/{application_id}/review", response_model=SuccessResponse)
async def review_application(
application_id: str,
req: ReviewApplicationRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
审批申请(super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
user_id = principal.get("user_id")
# 查询申请
result = await db.execute(
select(Application).where(Application.id == application_id)
)
application = result.scalar_one_or_none()
if not application:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="申请不存在"
)
if application.status != "pending":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该申请已被处理"
)
# 更新申请状态
application.status = "approved" if req.approved else "rejected"
application.reviewed_by = user_id
application.review_reason = req.reason
application.reviewed_at = datetime.utcnow()
# 如果批准,自动分配资源
if req.approved:
if application.type == "model":
# 查找模型供应商
result = await db.execute(
select(ModelProvider).where(ModelProvider.name == application.model_name)
)
model_provider = result.scalar_one_or_none()
if model_provider:
allocation = ResourceAllocation(
target_id=str(application.channel_id),
target_type="channel",
resource_type="model",
resource_id=str(model_provider.id),
rpm=application.rpm,
tpm=application.tpm,
)
db.add(allocation)
elif application.type == "agent":
# 查找Agent(简化处理,实际应该根据agent_type查找)
result = await db.execute(
select(Agent).where(Agent.name == application.agent_type).limit(1)
)
agent = result.scalar_one_or_none()
if agent:
allocation = ResourceAllocation(
target_id=str(application.channel_id),
target_type="channel",
resource_type="agent",
resource_id=str(agent.id),
quantity=application.quantity,
)
db.add(allocation)
await db.commit()
return SuccessResponse(
message=f"申请已{'批准' if req.approved else '拒绝'}"
)
# ============= 资源管理 =============
@router.get("/resources/models", response_model=SuccessResponse)
async def list_model_providers(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取所有模型供应商(所有管理员可查看)
"""
_verify_read_permission(principal)
result = await db.execute(select(ModelProvider))
providers = result.scalars().all()
data = [
{
"id": str(provider.id),
"name": provider.name,
"provider": provider.provider,
"apiUrl": provider.api_url,
"supportedModels": provider.supported_models,
"rpm": provider.rpm,
"tpm": provider.tpm,
"status": provider.status,
"isActive": provider.is_active,
}
for provider in providers
]
return SuccessResponse(data={"providers": data})
@router.get("/resources/agents", response_model=SuccessResponse)
async def list_all_agents(
include_inactive: bool = Query(False, description="是否包含已删除(inactive)的Agent"),
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取所有Agent资源
Args:
include_inactive: 是否包含已删除的Agent,默认False只返回活跃的
"""
_verify_read_permission(principal)
# 默认只返回活跃的Agent,除非明确请求包含已删除的
if include_inactive:
result = await db.execute(select(Agent))
else:
result = await db.execute(
select(Agent).where(Agent.status != "inactive")
)
agents = result.scalars().all()
data = [
{
"id": str(agent.id),
"name": agent.name,
"type": agent.type,
"category": agent.category,
"cpu": float(agent.cpu),
"memory": float(agent.memory),
"status": agent.status,
}
for agent in agents
]
return SuccessResponse(data={"agents": data})
@router.delete("/resources/agents/{agent_id}", response_model=SuccessResponse)
async def delete_agent_resource(
agent_id: str,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
删除Agent资源(软删除,super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
# 验证Agent存在
result = await db.execute(
select(Agent).where(Agent.id == agent_id)
)
agent = result.scalar_one_or_none()
if not agent:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Agent资源不存在"
)
# 软删除:标记为不活跃
agent.status = "inactive"
await db.commit()
return SuccessResponse(
data={"id": str(agent.id), "name": agent.name},
message="Agent资源已删除"
)
@router.put("/resources/agents/{agent_id}/config", response_model=SuccessResponse)
async def update_agent_config(
agent_id: str,
req: UpdateAgentConfigRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
更新Agent资源配置(super_admin 和 billing_admin 可用)
"""
_verify_write_permission(principal)
# 验证Agent存在
result = await db.execute(
select(Agent).where(Agent.id == agent_id)
)
agent = result.scalar_one_or_none()
if not agent:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Agent资源不存在"
)
# 更新配置
if req.cpu is not None:
agent.cpu = req.cpu
if req.memory is not None:
agent.memory = req.memory
if req.maxInstances is not None:
agent.max_instances = req.maxInstances
await db.commit()
await db.refresh(agent)
return SuccessResponse(
data={
"id": str(agent.id),
"name": agent.name,
"cpu": float(agent.cpu),
"memory": float(agent.memory),
"maxInstances": agent.max_instances,
},
message="Agent资源配置更新成功"
)
# ============= 监控 =============
@router.get("/monitoring/agents", response_model=SuccessResponse)
async def monitor_agents(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
监控Agent健康状态和性能指标(所有管理员可查看)
"""
_verify_read_permission(principal)
result = await db.execute(
select(Agent).where(Agent.type == "platform")
)
agents = result.scalars().all()
data = [
{
"id": str(agent.id),
"name": agent.name,
"status": agent.status,
"totalExecutions": agent.total_executions,
"successRate": float(agent.success_rate),
"avgExecutionTime": float(agent.avg_execution_time),
"cpu": float(agent.cpu),
"memory": float(agent.memory),
}
for agent in agents
]
return SuccessResponse(data={"agents": data})
# ============= 计费(三维度) =============
@router.get("/billing/overview", response_model=SuccessResponse)
async def get_billing_overview(
startTime: str = Query(...),
endTime: str = Query(...),
channelName: Optional[str] = Query(None),
tenantName: Optional[str] = Query(None),
minCalls: Optional[int] = Query(None),
maxCalls: Optional[int] = Query(None),
export: Optional[str] = Query(None, pattern="^(excel|csv|pdf)$"),
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取三维度计费统计(所有管理员可查看)
"""
_verify_read_permission(principal)
# 解析时间
start_dt = datetime.fromisoformat(startTime.replace("Z", "+00:00"))
end_dt = datetime.fromisoformat(endTime.replace("Z", "+00:00"))
# 渠道统计
channel_stats_result = await db.execute(
select(
Channel.id,
Channel.name,
func.count(BillingRecord.id).label("calls"),
func.sum(BillingRecord.eu).label("total_eu"),
func.sum(BillingRecord.cost).label("total_cost"),
)
.select_from(BillingRecord)
.join(Channel, BillingRecord.channel_id == Channel.id)
.where(
and_(
BillingRecord.timestamp >= start_dt,
BillingRecord.timestamp <= end_dt,
)
)
.group_by(Channel.id, Channel.name)
)
channel_stats = [
{
"channelId": str(row.id),
"channelName": row.name,
"calls": row.calls,
"totalEU": float(row.total_eu or 0),
"totalCost": float(row.total_cost or 0),
}
for row in channel_stats_result.all()
]
# 租户统计
tenant_stats_result = await db.execute(
select(
User.id,
User.name,
Channel.name.label("channel_name"),
func.count(BillingRecord.id).label("calls"),
func.sum(BillingRecord.eu).label("total_eu"),
func.sum(BillingRecord.cost).label("total_cost"),
)
.select_from(BillingRecord)
.join(User, BillingRecord.tenant_id == User.id)
.join(Channel, User.channel_id == Channel.id)
.where(
and_(
BillingRecord.timestamp >= start_dt,
BillingRecord.timestamp <= end_dt,
)
)
.group_by(User.id, User.name, Channel.name)
)
tenant_stats = [
{
"tenantId": str(row.id),
"tenantName": row.name,
"channelName": row.channel_name,
"calls": row.calls,
"totalEU": float(row.total_eu or 0),
"totalCost": float(row.total_cost or 0),
}
for row in tenant_stats_result.all()
]
# 调用记录
records_result = await db.execute(
select(BillingRecord, Channel.name, User.name)
.join(Channel, BillingRecord.channel_id == Channel.id)
.join(User, BillingRecord.tenant_id == User.id)
.where(
and_(
BillingRecord.timestamp >= start_dt,
BillingRecord.timestamp <= end_dt,
)
)
.order_by(desc(BillingRecord.timestamp))
.limit(100)
)
call_records = [
{
"id": str(record.id),
"timestamp": record.timestamp.isoformat(),
"channelName": channel_name,
"tenantName": tenant_name,
"agentName": record.agent_name,
"duration": record.duration,
"eu": record.eu,
"cost": float(record.cost),
}
for record, channel_name, tenant_name in records_result.all()
]
# 如果是导出请求
if export:
file_url = f"https://exports.taiji-ai.com/admin/{export}/billing_{datetime.utcnow().strftime('%Y%m%d%H%M%S')}.{export}"
expires_at = (datetime.utcnow() + timedelta(hours=24)).isoformat()
return SuccessResponse(
data={
"fileUrl": file_url,
"format": export,
"expiresAt": expires_at,
}
)
return SuccessResponse(
data={
"channelStats": channel_stats,
"tenantStats": tenant_stats,
"callRecords": call_records,
}
)