feat: 新增告警、成本、配额、微信模块及缓存服务
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- 新增告警模块 (alerts): 告警规则配置与触发 - 新增成本管理模块 (cost): 成本统计与分析 - 新增配额模块 (quota): 配额管理与限制 - 新增微信模块 (wechat): 微信相关功能接口 - 新增缓存服务 (cache): Redis 缓存封装 - 新增请求日志中间件 (request_logger) - 新增异常处理和链路追踪中间件 - 更新 dashboard 前端展示 - 更新 SDK stats_client 功能
This commit is contained in:
333
backend/app/routers/cost.py
Normal file
333
backend/app/routers/cost.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""费用管理路由"""
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user