feat: add admin UI frontend and complete backend APIs
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
- Add Vue 3 frontend with Element Plus - Implement login, dashboard, tenant management - Add app configuration, logs viewer, stats pages - Add user management for admins - Update Drone CI to build and deploy frontend - Frontend ports: 3001 (test), 4001 (prod)
This commit is contained in:
301
backend/app/routers/tenants.py
Normal file
301
backend/app/routers/tenants.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""租户管理路由"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.tenant import Tenant, Subscription
|
||||
from ..models.stats import TenantUsageDaily
|
||||
from .auth import get_current_user, require_operator
|
||||
from ..models.user import User
|
||||
|
||||
router = APIRouter(prefix="/tenants", tags=["租户管理"])
|
||||
|
||||
|
||||
# Schemas
|
||||
|
||||
class TenantCreate(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
contact_info: Optional[dict] = None
|
||||
status: str = "active"
|
||||
expired_at: Optional[date] = None
|
||||
|
||||
|
||||
class TenantUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
contact_info: Optional[dict] = None
|
||||
status: Optional[str] = None
|
||||
expired_at: Optional[date] = None
|
||||
|
||||
|
||||
class SubscriptionCreate(BaseModel):
|
||||
tenant_id: int
|
||||
app_code: str
|
||||
start_date: Optional[date] = None
|
||||
end_date: Optional[date] = None
|
||||
quota: Optional[dict] = None
|
||||
status: str = "active"
|
||||
|
||||
|
||||
class SubscriptionUpdate(BaseModel):
|
||||
start_date: Optional[date] = None
|
||||
end_date: Optional[date] = None
|
||||
quota: Optional[dict] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# API Endpoints
|
||||
|
||||
@router.get("")
|
||||
async def list_tenants(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
status: Optional[str] = None,
|
||||
keyword: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取租户列表"""
|
||||
query = db.query(Tenant)
|
||||
|
||||
if status:
|
||||
query = query.filter(Tenant.status == status)
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(Tenant.code.contains(keyword)) | (Tenant.name.contains(keyword))
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
tenants = query.order_by(Tenant.id.desc()).offset((page - 1) * size).limit(size).all()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"items": [
|
||||
{
|
||||
"id": t.id,
|
||||
"code": t.code,
|
||||
"name": t.name,
|
||||
"contact_info": t.contact_info,
|
||||
"status": t.status,
|
||||
"expired_at": t.expired_at,
|
||||
"created_at": t.created_at
|
||||
}
|
||||
for t in tenants
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{tenant_id}")
|
||||
async def get_tenant(
|
||||
tenant_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取租户详情"""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="租户不存在")
|
||||
|
||||
# 获取订阅
|
||||
subscriptions = db.query(Subscription).filter(
|
||||
Subscription.tenant_id == tenant_id
|
||||
).all()
|
||||
|
||||
# 获取用量统计(最近30天)
|
||||
usage = db.query(
|
||||
func.sum(TenantUsageDaily.ai_calls).label('total_calls'),
|
||||
func.sum(TenantUsageDaily.ai_tokens).label('total_tokens'),
|
||||
func.sum(TenantUsageDaily.ai_cost).label('total_cost')
|
||||
).filter(
|
||||
TenantUsageDaily.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
return {
|
||||
"id": tenant.id,
|
||||
"code": tenant.code,
|
||||
"name": tenant.name,
|
||||
"contact_info": tenant.contact_info,
|
||||
"status": tenant.status,
|
||||
"expired_at": tenant.expired_at,
|
||||
"created_at": tenant.created_at,
|
||||
"updated_at": tenant.updated_at,
|
||||
"subscriptions": [
|
||||
{
|
||||
"id": s.id,
|
||||
"app_code": s.app_code,
|
||||
"start_date": s.start_date,
|
||||
"end_date": s.end_date,
|
||||
"quota": s.quota,
|
||||
"status": s.status
|
||||
}
|
||||
for s in subscriptions
|
||||
],
|
||||
"usage_summary": {
|
||||
"total_calls": int(usage.total_calls or 0),
|
||||
"total_tokens": int(usage.total_tokens or 0),
|
||||
"total_cost": float(usage.total_cost or 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_tenant(
|
||||
data: TenantCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建租户"""
|
||||
# 检查 code 是否重复
|
||||
exists = db.query(Tenant).filter(Tenant.code == data.code).first()
|
||||
if exists:
|
||||
raise HTTPException(status_code=400, detail="租户代码已存在")
|
||||
|
||||
tenant = Tenant(
|
||||
code=data.code,
|
||||
name=data.name,
|
||||
contact_info=data.contact_info,
|
||||
status=data.status,
|
||||
expired_at=data.expired_at
|
||||
)
|
||||
db.add(tenant)
|
||||
db.commit()
|
||||
db.refresh(tenant)
|
||||
|
||||
return {"success": True, "id": tenant.id}
|
||||
|
||||
|
||||
@router.put("/{tenant_id}")
|
||||
async def update_tenant(
|
||||
tenant_id: int,
|
||||
data: TenantUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新租户"""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="租户不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(tenant, key, value)
|
||||
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/{tenant_id}")
|
||||
async def delete_tenant(
|
||||
tenant_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除租户"""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="租户不存在")
|
||||
|
||||
# 删除关联的订阅
|
||||
db.query(Subscription).filter(Subscription.tenant_id == tenant_id).delete()
|
||||
db.delete(tenant)
|
||||
db.commit()
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
# 订阅管理
|
||||
|
||||
@router.get("/{tenant_id}/subscriptions")
|
||||
async def list_subscriptions(
|
||||
tenant_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取租户订阅列表"""
|
||||
subscriptions = db.query(Subscription).filter(
|
||||
Subscription.tenant_id == tenant_id
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": s.id,
|
||||
"tenant_id": s.tenant_id,
|
||||
"app_code": s.app_code,
|
||||
"start_date": s.start_date,
|
||||
"end_date": s.end_date,
|
||||
"quota": s.quota,
|
||||
"status": s.status,
|
||||
"created_at": s.created_at
|
||||
}
|
||||
for s in subscriptions
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{tenant_id}/subscriptions")
|
||||
async def create_subscription(
|
||||
tenant_id: int,
|
||||
data: SubscriptionCreate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建订阅"""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
raise HTTPException(status_code=404, detail="租户不存在")
|
||||
|
||||
subscription = Subscription(
|
||||
tenant_id=tenant_id,
|
||||
app_code=data.app_code,
|
||||
start_date=data.start_date,
|
||||
end_date=data.end_date,
|
||||
quota=data.quota,
|
||||
status=data.status
|
||||
)
|
||||
db.add(subscription)
|
||||
db.commit()
|
||||
db.refresh(subscription)
|
||||
|
||||
return {"success": True, "id": subscription.id}
|
||||
|
||||
|
||||
@router.put("/subscriptions/{subscription_id}")
|
||||
async def update_subscription(
|
||||
subscription_id: int,
|
||||
data: SubscriptionUpdate,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新订阅"""
|
||||
subscription = db.query(Subscription).filter(Subscription.id == subscription_id).first()
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="订阅不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(subscription, key, value)
|
||||
|
||||
db.commit()
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.delete("/subscriptions/{subscription_id}")
|
||||
async def delete_subscription(
|
||||
subscription_id: int,
|
||||
user: User = Depends(require_operator),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除订阅"""
|
||||
subscription = db.query(Subscription).filter(Subscription.id == subscription_id).first()
|
||||
if not subscription:
|
||||
raise HTTPException(status_code=404, detail="订阅不存在")
|
||||
|
||||
db.delete(subscription)
|
||||
db.commit()
|
||||
|
||||
return {"success": True}
|
||||
Reference in New Issue
Block a user