"""费用管理路由""" 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 }