Files
2026-08-12 11:49:53 +08:00

83 lines
3.2 KiB
Python

from __future__ import annotations
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.llm_billing import LlmBillingExecutionStatus
from app.models.llm_billing.call_attempt import LlmCallAttempt
from app.models.llm_billing.execution import LlmBillingExecution
def _normalized_execution_status(status: str) -> str:
if status == LlmBillingExecutionStatus.PRE_DEDUCTED.value:
return LlmBillingExecutionStatus.CHARGED.value
return status
def _execution_status_filter(status: str):
# 新接口只暴露 charged;查询时同时兼容历史 pre_deducted 数据。
if status in {
LlmBillingExecutionStatus.CHARGED.value,
LlmBillingExecutionStatus.PRE_DEDUCTED.value,
}:
return or_(
LlmBillingExecution.status == LlmBillingExecutionStatus.CHARGED.value,
LlmBillingExecution.status == LlmBillingExecutionStatus.PRE_DEDUCTED.value,
)
return LlmBillingExecution.status == status
async def list_executions_with_calls(
db: AsyncSession,
*,
page: int = 1,
page_size: int = 20,
scene_code: str | None = None,
status: str | None = None,
user_id: str | None = None,
) -> tuple[list[dict], int]:
filters = []
if scene_code:
filters.append(LlmBillingExecution.scene_code == scene_code)
if status:
filters.append(_execution_status_filter(status))
if user_id:
filters.append(LlmBillingExecution.user_id == user_id)
total_result = await db.execute(select(func.count(LlmBillingExecution.id)).where(*filters))
total = int(total_result.scalar_one() or 0)
result = await db.execute(
select(LlmBillingExecution)
.where(*filters)
.order_by(LlmBillingExecution.created_at.desc(), LlmBillingExecution.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
executions = list(result.scalars().all())
# 先收集 execution ids,再一次批查所有 CallAttempt 并按 execution_id 回填,避免 N+1。
execution_ids = [item.id for item in executions]
calls_by_execution: dict[str, list[LlmCallAttempt]] = {item_id: [] for item_id in execution_ids}
if execution_ids:
call_result = await db.execute(
select(LlmCallAttempt)
.where(LlmCallAttempt.billing_execution_id.in_(execution_ids))
.order_by(LlmCallAttempt.billing_execution_id, LlmCallAttempt.call_sequence)
)
for call in call_result.scalars().all():
calls_by_execution.setdefault(call.billing_execution_id, []).append(call)
items: list[dict] = []
for execution in executions:
# 数据库物理字段 pre_deduct_credits 为历史命名;Admin/API 统一只暴露 charge_credits。
data = {
column.name: getattr(execution, column.name)
for column in LlmBillingExecution.__table__.columns
if column.name != "pre_deduct_credits"
}
data["charge_credits"] = execution.pre_deduct_credits
data["status"] = _normalized_execution_status(execution.status)
data["calls"] = calls_by_execution.get(execution.id, [])
items.append(data)
return items, total