Files
000-platform/backend/app/routers/cost.py
111 6c6c48cf71
All checks were successful
continuous-integration/drone/push Build is passing
feat: 新增告警、成本、配额、微信模块及缓存服务
- 新增告警模块 (alerts): 告警规则配置与触发
- 新增成本管理模块 (cost): 成本统计与分析
- 新增配额模块 (quota): 配额管理与限制
- 新增微信模块 (wechat): 微信相关功能接口
- 新增缓存服务 (cache): Redis 缓存封装
- 新增请求日志中间件 (request_logger)
- 新增异常处理和链路追踪中间件
- 更新 dashboard 前端展示
- 更新 SDK stats_client 功能
2026-01-24 16:53:47 +08:00

334 lines
10 KiB
Python

"""费用管理路由"""
from typing import Optional, List
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
from sqlalchemy import desc
from ..database import get_db
from ..models.pricing import ModelPricing, TenantBilling
from ..services.cost import CostCalculator
from .auth import get_current_user, require_operator
from ..models.user import User
router = APIRouter(prefix="/cost", tags=["费用管理"])
# ============= Schemas =============
class ModelPricingCreate(BaseModel):
model_name: str
provider: Optional[str] = None
display_name: Optional[str] = None
input_price_per_1k: float = 0
output_price_per_1k: float = 0
fixed_price_per_call: float = 0
pricing_type: str = "token"
description: Optional[str] = None
class ModelPricingUpdate(BaseModel):
provider: Optional[str] = None
display_name: Optional[str] = None
input_price_per_1k: Optional[float] = None
output_price_per_1k: Optional[float] = None
fixed_price_per_call: Optional[float] = None
pricing_type: Optional[str] = None
description: Optional[str] = None
status: Optional[int] = None
class CostCalculateRequest(BaseModel):
model_name: str
input_tokens: int = 0
output_tokens: int = 0
# ============= Model Pricing API =============
@router.get("/pricing")
async def list_model_pricing(
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=100),
provider: Optional[str] = None,
status: Optional[int] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取模型价格配置列表"""
query = db.query(ModelPricing)
if provider:
query = query.filter(ModelPricing.provider == provider)
if status is not None:
query = query.filter(ModelPricing.status == status)
total = query.count()
items = query.order_by(ModelPricing.model_name).offset((page - 1) * size).limit(size).all()
return {
"total": total,
"page": page,
"size": size,
"items": [format_pricing(p) for p in items]
}
@router.get("/pricing/{pricing_id}")
async def get_model_pricing(
pricing_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取模型价格详情"""
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
if not pricing:
raise HTTPException(status_code=404, detail="模型价格配置不存在")
return format_pricing(pricing)
@router.post("/pricing")
async def create_model_pricing(
data: ModelPricingCreate,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""创建模型价格配置"""
# 检查是否已存在
existing = db.query(ModelPricing).filter(ModelPricing.model_name == data.model_name).first()
if existing:
raise HTTPException(status_code=400, detail="该模型价格配置已存在")
pricing = ModelPricing(
model_name=data.model_name,
provider=data.provider,
display_name=data.display_name,
input_price_per_1k=Decimal(str(data.input_price_per_1k)),
output_price_per_1k=Decimal(str(data.output_price_per_1k)),
fixed_price_per_call=Decimal(str(data.fixed_price_per_call)),
pricing_type=data.pricing_type,
description=data.description,
status=1
)
db.add(pricing)
db.commit()
db.refresh(pricing)
return {"success": True, "id": pricing.id}
@router.put("/pricing/{pricing_id}")
async def update_model_pricing(
pricing_id: int,
data: ModelPricingUpdate,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""更新模型价格配置"""
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
if not pricing:
raise HTTPException(status_code=404, detail="模型价格配置不存在")
update_data = data.model_dump(exclude_unset=True)
# 转换价格字段
for field in ['input_price_per_1k', 'output_price_per_1k', 'fixed_price_per_call']:
if field in update_data and update_data[field] is not None:
update_data[field] = Decimal(str(update_data[field]))
for key, value in update_data.items():
setattr(pricing, key, value)
db.commit()
return {"success": True}
@router.delete("/pricing/{pricing_id}")
async def delete_model_pricing(
pricing_id: int,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""删除模型价格配置"""
pricing = db.query(ModelPricing).filter(ModelPricing.id == pricing_id).first()
if not pricing:
raise HTTPException(status_code=404, detail="模型价格配置不存在")
db.delete(pricing)
db.commit()
return {"success": True}
# ============= Cost Calculation API =============
@router.post("/calculate")
async def calculate_cost(
request: CostCalculateRequest,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""计算调用费用"""
calculator = CostCalculator(db)
cost = calculator.calculate_cost(
model_name=request.model_name,
input_tokens=request.input_tokens,
output_tokens=request.output_tokens
)
return {
"model": request.model_name,
"input_tokens": request.input_tokens,
"output_tokens": request.output_tokens,
"cost": float(cost)
}
@router.get("/summary")
async def get_cost_summary(
tenant_id: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取费用汇总"""
calculator = CostCalculator(db)
return calculator.get_cost_summary(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
@router.get("/by-tenant")
async def get_cost_by_tenant(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""按租户统计费用"""
calculator = CostCalculator(db)
return calculator.get_cost_by_tenant(
start_date=start_date,
end_date=end_date
)
@router.get("/by-model")
async def get_cost_by_model(
tenant_id: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""按模型统计费用"""
calculator = CostCalculator(db)
return calculator.get_cost_by_model(
tenant_id=tenant_id,
start_date=start_date,
end_date=end_date
)
# ============= Billing API =============
@router.get("/billing")
async def list_billing(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
tenant_id: Optional[str] = None,
billing_month: Optional[str] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取账单列表"""
query = db.query(TenantBilling)
if tenant_id:
query = query.filter(TenantBilling.tenant_id == tenant_id)
if billing_month:
query = query.filter(TenantBilling.billing_month == billing_month)
total = query.count()
items = query.order_by(desc(TenantBilling.billing_month)).offset((page - 1) * size).limit(size).all()
return {
"total": total,
"page": page,
"size": size,
"items": [format_billing(b) for b in items]
}
@router.post("/billing/generate")
async def generate_billing(
tenant_id: str = Query(...),
billing_month: str = Query(..., description="格式: YYYY-MM"),
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""生成月度账单"""
calculator = CostCalculator(db)
billing = calculator.generate_monthly_billing(tenant_id, billing_month)
return {
"success": True,
"billing": format_billing(billing)
}
@router.post("/recalculate")
async def recalculate_costs(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
user: User = Depends(require_operator),
db: Session = Depends(get_db)
):
"""重新计算事件费用"""
calculator = CostCalculator(db)
updated = calculator.update_event_costs(start_date, end_date)
return {
"success": True,
"updated_count": updated
}
# ============= Helper Functions =============
def format_pricing(pricing: ModelPricing) -> dict:
return {
"id": pricing.id,
"model_name": pricing.model_name,
"provider": pricing.provider,
"display_name": pricing.display_name,
"input_price_per_1k": float(pricing.input_price_per_1k or 0),
"output_price_per_1k": float(pricing.output_price_per_1k or 0),
"fixed_price_per_call": float(pricing.fixed_price_per_call or 0),
"pricing_type": pricing.pricing_type,
"description": pricing.description,
"status": pricing.status,
"created_at": pricing.created_at,
"updated_at": pricing.updated_at
}
def format_billing(billing: TenantBilling) -> dict:
return {
"id": billing.id,
"tenant_id": billing.tenant_id,
"billing_month": billing.billing_month,
"total_calls": billing.total_calls,
"total_input_tokens": billing.total_input_tokens,
"total_output_tokens": billing.total_output_tokens,
"total_cost": float(billing.total_cost or 0),
"cost_by_model": billing.cost_by_model,
"cost_by_app": billing.cost_by_app,
"status": billing.status,
"created_at": billing.created_at,
"updated_at": billing.updated_at
}