Files
taiji-AI-PAD/services/mcp-server/app/routes/channel.py
T
2025-12-26 08:32:00 +00:00

784 lines
23 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, RechargeRecord, Application, ModelProvider,
ChannelProviderAccess, ProviderApplication
)
from app.auth import require_auth, get_password_hash
from app.schemas import (
SuccessResponse,
TenantCreateRequest,
AllocateResourcesRequest,
UpdateBillingRequest,
RechargeTenantRequest,
RechargeTenantResponse,
SetCreditLimitRequest,
SetCreditLimitResponse,
ResourceApplicationRequest,
ChannelBillingResponse,
ApplyProviderRequest,
)
router = APIRouter(prefix="/api/channel", tags=["渠道合作伙伴"])
def _verify_channel_permission(principal: dict):
"""验证渠道管理员权限"""
role = principal.get("claims", {}).get("role")
if role != "channel_admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要渠道管理员权限"
)
# ============= 租户管理 =============
@router.get("/tenants", response_model=SuccessResponse)
async def list_tenants(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取渠道下的租户列表
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
result = await db.execute(
select(User).where(User.channel_id == channel_id)
)
tenants = result.scalars().all()
data = [
{
"id": str(tenant.id),
"name": tenant.name,
"email": tenant.email,
"subscriptionTier": tenant.subscription_tier,
"balance": float(tenant.balance),
"creditLimit": float(tenant.credit_limit),
"status": tenant.status,
"createdAt": tenant.created_at.isoformat(),
}
for tenant in tenants
]
return SuccessResponse(data={"tenants": data})
@router.post("/tenants/create", response_model=SuccessResponse)
async def create_tenant(
req: TenantCreateRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
创建租户
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
# 检查邮箱是否已存在
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)
tenant = 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="user",
channel_id=channel_id,
subscription_tier=req.subscriptionTier,
status="active",
balance=0,
credit_limit=0,
)
db.add(tenant)
await db.commit()
await db.refresh(tenant)
return SuccessResponse(
data={
"id": str(tenant.id),
"name": tenant.name,
"email": tenant.email,
},
message="租户创建成功"
)
@router.put("/tenants/{tenant_id}/resources", response_model=SuccessResponse)
async def allocate_tenant_resources(
tenant_id: str,
req: AllocateResourcesRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
为租户分配资源
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
# 验证租户属于该渠道
result = await db.execute(
select(User).where(
and_(
User.id == tenant_id,
User.channel_id == channel_id
)
)
)
tenant = result.scalar_one_or_none()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在或不属于该渠道"
)
# 删除现有资源分配
await db.execute(
select(ResourceAllocation).where(
and_(
ResourceAllocation.target_id == tenant_id,
ResourceAllocation.target_type == "tenant"
)
)
)
# 分配Agent资源
for agent_alloc in req.agents:
allocation = ResourceAllocation(
target_id=tenant_id,
target_type="tenant",
resource_type="agent",
resource_id=agent_alloc.agentId,
quantity=agent_alloc.quantity,
)
db.add(allocation)
# 分配模型资源
for model_alloc in req.models:
# 查找模型供应商
result = await db.execute(
select(ModelProvider).where(ModelProvider.name == model_alloc.modelName)
)
model_provider = result.scalar_one_or_none()
if model_provider:
allocation = ResourceAllocation(
target_id=tenant_id,
target_type="tenant",
resource_type="model",
resource_id=str(model_provider.id),
rpm=model_alloc.rpm,
tpm=model_alloc.tpm,
)
db.add(allocation)
await db.commit()
return SuccessResponse(message="资源分配成功")
@router.put("/tenants/{tenant_id}/billing", response_model=SuccessResponse)
async def update_tenant_billing(
tenant_id: str,
req: UpdateBillingRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
更新租户计费设置
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
# 验证租户
result = await db.execute(
select(User).where(
and_(
User.id == tenant_id,
User.channel_id == channel_id
)
)
)
tenant = result.scalar_one_or_none()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在"
)
# 更新计费设置
tenant.subscription_tier = req.subscriptionTier
tenant.discount = req.discount
await db.commit()
return SuccessResponse(message="计费设置更新成功")
@router.post("/tenants/{tenant_id}/recharge", response_model=SuccessResponse)
async def recharge_tenant(
tenant_id: str,
req: RechargeTenantRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
为租户充值
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
# 验证租户
result = await db.execute(
select(User).where(
and_(
User.id == tenant_id,
User.channel_id == channel_id
)
)
)
tenant = result.scalar_one_or_none()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在"
)
# 更新余额
tenant.balance = float(tenant.balance) + req.amount
# 创建充值记录
recharge = RechargeRecord(
user_id=tenant_id,
channel_id=channel_id,
amount=req.amount,
payment_method="channel_recharge",
status="success",
order_id=f"CH{datetime.utcnow().strftime('%Y%m%d%H%M%S')}{uuid.uuid4().hex[:8]}",
completed_at=datetime.utcnow(),
)
db.add(recharge)
await db.commit()
await db.refresh(tenant)
return SuccessResponse(
data={
"tenantId": str(tenant.id),
"newBalance": float(tenant.balance),
"rechargeAmount": req.amount,
}
)
@router.put("/tenants/{tenant_id}/credit", response_model=SuccessResponse)
async def set_tenant_credit_limit(
tenant_id: str,
req: SetCreditLimitRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
设置租户授信额度
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
# 验证租户
result = await db.execute(
select(User).where(
and_(
User.id == tenant_id,
User.channel_id == channel_id
)
)
)
tenant = result.scalar_one_or_none()
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="租户不存在"
)
# 更新授信额度
tenant.credit_limit = req.creditLimit
await db.commit()
return SuccessResponse(
data={
"tenantId": str(tenant.id),
"creditLimit": float(req.creditLimit),
}
)
# ============= 资源申请 =============
@router.post("/resources/apply", response_model=SuccessResponse)
async def apply_for_resources(
req: ResourceApplicationRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
申请资源(模型或Agent)
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
# 创建申请
application = Application(
channel_id=channel_id,
type=req.type,
model_name=req.modelName,
rpm=req.rpm,
tpm=req.tpm,
agent_type=req.agentType,
quantity=req.quantity,
reason=req.reason,
status="pending",
)
db.add(application)
await db.commit()
await db.refresh(application)
return SuccessResponse(
data={"id": str(application.id), "status": "pending"},
message="申请已提交,等待审批"
)
# ============= 计费统计 =============
@router.get("/billing/stats", response_model=SuccessResponse)
async def get_channel_billing_stats(
startTime: str = Query(...),
endTime: str = Query(...),
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_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
# 解析时间
start_dt = datetime.fromisoformat(startTime.replace("Z", "+00:00"))
end_dt = datetime.fromisoformat(endTime.replace("Z", "+00:00"))
# 查询渠道下的租户
tenants_result = await db.execute(
select(User).where(User.channel_id == channel_id)
)
tenants = {str(t.id): t for t in tenants_result.scalars().all()}
tenant_ids = list(tenants.keys())
# 租户统计
tenant_stats_result = await db.execute(
select(
BillingRecord.tenant_id,
func.count(BillingRecord.id).label("calls"),
func.sum(BillingRecord.eu).label("total_eu"),
func.sum(BillingRecord.cost).label("total_cost"),
)
.where(
and_(
BillingRecord.tenant_id.in_(tenant_ids),
BillingRecord.timestamp >= start_dt,
BillingRecord.timestamp <= end_dt,
)
)
.group_by(BillingRecord.tenant_id)
)
tenant_stats = []
for row in tenant_stats_result.all():
tenant = tenants.get(str(row.tenant_id))
if tenant:
tenant_stats.append({
"tenantId": str(row.tenant_id),
"tenantName": tenant.name,
"calls": row.calls,
"totalEU": float(row.total_eu or 0),
"totalCost": float(row.total_cost or 0),
})
# 调用记录
records_result = await db.execute(
select(BillingRecord)
.where(
and_(
BillingRecord.tenant_id.in_(tenant_ids),
BillingRecord.timestamp >= start_dt,
BillingRecord.timestamp <= end_dt,
)
)
.order_by(desc(BillingRecord.timestamp))
.limit(100)
)
call_records = []
for record in records_result.scalars().all():
tenant = tenants.get(str(record.tenant_id))
if tenant:
call_records.append({
"id": str(record.id),
"timestamp": record.timestamp.isoformat(),
"tenantName": tenant.name,
"agentName": record.agent_name,
"duration": record.duration,
"eu": record.eu,
"cost": float(record.cost),
})
# 如果是导出请求
if export:
file_url = f"https://exports.taiji-ai.com/{channel_id}/{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={
"tenantStats": tenant_stats,
"callRecords": call_records,
}
)
# ============= 供应商管理 =============
@router.get("/providers", response_model=SuccessResponse)
async def list_available_providers(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取所有可用的模型供应商列表(渠道管理员视图)
返回所有活跃的供应商,并标注该渠道是否已获得授权使用
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
# 获取所有活跃的供应商
providers_result = await db.execute(
select(ModelProvider).where(ModelProvider.is_active == True)
)
providers = providers_result.scalars().all()
# 获取该渠道的供应商授权
access_result = await db.execute(
select(ChannelProviderAccess).where(
ChannelProviderAccess.channel_id == channel_id
)
)
access_map = {str(a.provider_id): a for a in access_result.scalars().all()}
# 获取该渠道的待审批申请
pending_result = await db.execute(
select(ProviderApplication).where(
and_(
ProviderApplication.channel_id == channel_id,
ProviderApplication.status == "pending"
)
)
)
pending_apps = {str(a.provider_id) for a in pending_result.scalars().all()}
data = []
for provider in providers:
provider_id = str(provider.id)
access = access_map.get(provider_id)
item = {
"id": provider_id,
"name": provider.name,
"provider": provider.provider,
"supportedModels": provider.supported_models,
"rpm": provider.rpm,
"tpm": provider.tpm,
"status": provider.status,
"hasAccess": access is not None and access.status == "active",
"accessStatus": access.status if access else None,
"rpmLimit": access.rpm_limit if access else None,
"tpmLimit": access.tpm_limit if access else None,
"pendingApplication": provider_id in pending_apps,
}
data.append(item)
return SuccessResponse(data={"providers": data})
@router.post("/providers/apply", response_model=SuccessResponse)
async def apply_for_provider(
req: ApplyProviderRequest,
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
申请使用模型供应商
渠道管理员可以申请使用某个模型供应商,需要管理员审批后才能使用
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
# 检查供应商是否存在
provider_result = await db.execute(
select(ModelProvider).where(
and_(
ModelProvider.id == req.providerId,
ModelProvider.is_active == True
)
)
)
provider = provider_result.scalar_one_or_none()
if not provider:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="供应商不存在或已停用"
)
# 检查是否已有授权
access_result = await db.execute(
select(ChannelProviderAccess).where(
and_(
ChannelProviderAccess.channel_id == channel_id,
ChannelProviderAccess.provider_id == req.providerId,
ChannelProviderAccess.status == "active"
)
)
)
if access_result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="您已获得该供应商的使用授权"
)
# 检查是否有待审批的申请
pending_result = await db.execute(
select(ProviderApplication).where(
and_(
ProviderApplication.channel_id == channel_id,
ProviderApplication.provider_id == req.providerId,
ProviderApplication.status == "pending"
)
)
)
if pending_result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="您已有一个待审批的申请"
)
# 创建申请
application = ProviderApplication(
channel_id=channel_id,
provider_id=req.providerId,
requested_rpm=req.requestedRpm,
requested_tpm=req.requestedTpm,
reason=req.reason,
status="pending",
)
db.add(application)
await db.commit()
await db.refresh(application)
return SuccessResponse(
data={
"id": str(application.id),
"providerId": req.providerId,
"providerName": provider.name,
"status": "pending",
},
message="申请已提交,等待管理员审批"
)
@router.get("/providers/applications", response_model=SuccessResponse)
async def list_provider_applications(
status_filter: Optional[str] = Query(None, alias="status", pattern="^(pending|approved|rejected)$"),
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取渠道的供应商申请列表
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
# 构建查询
query = select(ProviderApplication).where(
ProviderApplication.channel_id == channel_id
)
if status_filter:
query = query.where(ProviderApplication.status == status_filter)
query = query.order_by(desc(ProviderApplication.created_at))
result = await db.execute(query)
applications = result.scalars().all()
# 获取供应商信息
provider_ids = [str(a.provider_id) for a in applications]
providers_result = await db.execute(
select(ModelProvider).where(ModelProvider.id.in_(provider_ids))
)
providers_map = {str(p.id): p for p in providers_result.scalars().all()}
data = []
for app in applications:
provider = providers_map.get(str(app.provider_id))
data.append({
"id": str(app.id),
"providerId": str(app.provider_id),
"providerName": provider.name if provider else "未知",
"requestedRpm": app.requested_rpm,
"requestedTpm": app.requested_tpm,
"reason": app.reason,
"status": app.status,
"createdAt": app.created_at.isoformat(),
"reviewedAt": app.reviewed_at.isoformat() if app.reviewed_at else None,
"reviewReason": app.review_reason,
})
return SuccessResponse(data={"applications": data})
@router.get("/providers/access", response_model=SuccessResponse)
async def list_provider_access(
principal: dict = Depends(require_auth),
db: AsyncSession = Depends(get_db)
):
"""
获取渠道已授权的供应商列表
"""
_verify_channel_permission(principal)
channel_id = principal.get("claims", {}).get("channelId")
if not channel_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无法获取渠道ID"
)
# 获取授权列表
result = await db.execute(
select(ChannelProviderAccess).where(
ChannelProviderAccess.channel_id == channel_id
)
)
access_list = result.scalars().all()
# 获取供应商信息
provider_ids = [str(a.provider_id) for a in access_list]
providers_result = await db.execute(
select(ModelProvider).where(ModelProvider.id.in_(provider_ids))
)
providers_map = {str(p.id): p for p in providers_result.scalars().all()}
data = []
for access in access_list:
provider = providers_map.get(str(access.provider_id))
data.append({
"id": str(access.id),
"providerId": str(access.provider_id),
"providerName": provider.name if provider else "未知",
"provider": provider.provider if provider else "unknown",
"supportedModels": provider.supported_models if provider else [],
"status": access.status,
"rpmLimit": access.rpm_limit,
"tpmLimit": access.tpm_limit,
"approvedAt": access.approved_at.isoformat() if access.approved_at else None,
"expiresAt": access.expires_at.isoformat() if access.expires_at else None,
})
return SuccessResponse(data={"accessList": data})