from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_admin_user, get_db from app.enums.llm_billing import LLM_BILLING_SCENE_LABELS from app.models.llm_billing.policy import LlmBillingPolicyModel from app.models.user import User from app.schemas.llm_billing import LlmBillingPolicyCreate, LlmBillingPolicyUpdate from app.services.llm_billing.query_service import list_executions_with_calls from app.services.operation_log_service import log_operation_event from app.utils.id_gen import generate_id router = APIRouter(prefix="/admin/llm-billing", tags=["admin-llm-billing"]) def _policy_out(policy: LlmBillingPolicyModel) -> dict: # 数据库 pre_deduct_credits 是历史物理字段名;Admin API 统一输出 charge_credits。 return { "id": policy.id, "scene_code": policy.scene_code, "scene_name": policy.scene_name, "charge_credits": float(policy.pre_deduct_credits), "is_active": policy.is_active, "version": policy.version, "created_by": policy.created_by, "updated_by": policy.updated_by, "created_at": policy.created_at, "updated_at": policy.updated_at, } @router.get("/policies") async def list_policies( admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(LlmBillingPolicyModel).order_by(LlmBillingPolicyModel.scene_code)) return [_policy_out(item) for item in result.scalars().all()] @router.post("/policies") async def create_policy( data: LlmBillingPolicyCreate, admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): if data.scene_code not in LLM_BILLING_SCENE_LABELS: raise HTTPException(status_code=400, detail="不支持的LLM业务场景") existing = await db.execute( select(LlmBillingPolicyModel.id) .where(LlmBillingPolicyModel.scene_code == data.scene_code) .limit(1) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=409, detail="该LLM业务场景已存在") policy = LlmBillingPolicyModel( id=generate_id(), scene_code=data.scene_code, scene_name=LLM_BILLING_SCENE_LABELS[data.scene_code], # 保留历史物理字段,不做无业务价值的数据库重命名迁移。 pre_deduct_credits=data.charge_credits, is_active=data.is_active, version=1, created_by=admin.id, updated_by=admin.id, ) db.add(policy) await db.flush() policy_id = policy.id scene_code = policy.scene_code charge_credits = float(policy.pre_deduct_credits) log_operation_event( domain="llm_billing", module="admin", event_type="LLM_BILLING_POLICY_CREATED", user_id=admin.id, detail={"scene_code": scene_code, "charge_credits": charge_credits, "policy_id": policy_id}, ) return _policy_out(policy) @router.put("/policies/{policy_id}") async def update_policy( policy_id: str, data: LlmBillingPolicyUpdate, admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(LlmBillingPolicyModel) .where(LlmBillingPolicyModel.id == policy_id) .limit(1) .with_for_update() ) policy = result.scalar_one_or_none() if not policy: raise HTTPException(status_code=404, detail="LLM积分场景不存在") payload = data.model_dump(exclude_unset=True) payload.pop("scene_name", None) if "charge_credits" in payload: policy.pre_deduct_credits = payload.pop("charge_credits") if "is_active" in payload: policy.is_active = payload["is_active"] policy.scene_name = LLM_BILLING_SCENE_LABELS.get(policy.scene_code, policy.scene_name) policy.version += 1 policy.updated_by = admin.id await db.flush() log_operation_event( domain="llm_billing", module="admin", event_type="LLM_BILLING_POLICY_UPDATED", user_id=admin.id, detail={ "scene_code": policy.scene_code, "policy_id": policy.id, "version": policy.version, "charge_credits": float(policy.pre_deduct_credits), "is_active": policy.is_active, }, ) return _policy_out(policy) @router.get("/executions") async def list_executions( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), scene_code: str | None = Query(default=None), status: str | None = Query(default=None), user_id: str | None = Query(default=None), admin: User = Depends(get_admin_user), db: AsyncSession = Depends(get_db), ): items, total = await list_executions_with_calls( db, page=page, page_size=page_size, scene_code=scene_code, status=status, user_id=user_id, ) return {"items": items, "total": total}