""" 计费与资源管理逻辑 EU(执行单元)计算说明: ======================= **重要:1 EU = 1 美元** EU 是系统的计费单位,直接等于成本(美元)。 eu_consumed 字段的值应该等于 cost 字段的值。 计费方式: 1. Agent 运行时长计费 - 平台 Agent:按小时固定费率($0.10-$0.15/小时) - 自定义 Agent:按资源使用量(CPU $0.05/核/小时 + 内存 $0.01/GB/小时) - EU = Cost(美元) 2. 模型 Token 计费 - 按模型定价计算成本 - EU = Cost(美元) 数据存储: - Agent计费记录:agent_billing_records 表 - 模型计费记录:model_billing_records 表 - 用户余额:balances 表(Balance模型) 注意:User.eu_balance 和 User.balance 字段已废弃,请使用 Balance 表 """ import math import logging from datetime import datetime from decimal import Decimal from typing import Optional, Tuple from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from models import User, Channel, BillingRecord, Agent, Balance, AgentBillingRecord logger = logging.getLogger(__name__) # ============= EU定价配置(已废弃) ============= # # 重要:1 EU = 1 美元 # EU 现在直接等于成本(美元),不再需要单独的定价配置 # 以下配置仅用于向后兼容旧的 BillingRecord 表 # EU单价配置(按订阅等级)- 已废弃,仅用于旧表兼容 EU_PRICING = { "free": Decimal("0.015"), # 入门级(Starter):$0.015 / EU "starter": Decimal("0.015"), # 入门级别名 "pro": Decimal("0.02"), # 专业级(Pro):$0.02 / EU "enterprise": Decimal("0.03"), # 企业级(Enterprise):$0.03 / EU } # 默认单价(未知等级时使用专业级价格)- 已废弃 DEFAULT_EU_PRICE = Decimal("0.02") def get_eu_price_by_tier(subscription_tier: str) -> Decimal: """ 根据订阅等级获取EU单价 ⚠️ 已废弃:此函数仅用于旧的 BillingRecord 表兼容 新的计费逻辑中,1 EU = 1 美元,不需要单独的定价 Args: subscription_tier: 订阅等级(free/starter/pro/enterprise) Returns: EU单价(USD) """ import warnings warnings.warn( "get_eu_price_by_tier() 已废弃,新计费逻辑中 1 EU = 1 美元", DeprecationWarning, stacklevel=2 ) tier = subscription_tier.lower() if subscription_tier else "pro" return EU_PRICING.get(tier, DEFAULT_EU_PRICE) # ============= EU计算规则 ============= # # 重要:1 EU = 1 美元 # EU 应该等于 Cost(成本),不再基于时间计算 def calculate_eu(duration_seconds: int) -> int: """ ⚠️ 已废弃:基于时间的 EU 计算 此函数使用旧的计算方式(1 EU = 10秒),已废弃。 新的计费逻辑中,EU = Cost(美元),即 1 EU = 1 美元。 请使用 calculate_agent_cost_by_resources() 或 calculate_platform_agent_cost() 计算成本,然后将成本值作为 EU 值。 Args: duration_seconds: 调用时长(秒) Returns: EU数量(已废弃的计算方式) """ import warnings warnings.warn( "calculate_eu() 已废弃,新计费逻辑中 EU = Cost(美元)", DeprecationWarning, stacklevel=2 ) return math.ceil(duration_seconds / 10) def calculate_cost(eu: int, unit_price: Decimal = None, subscription_tier: str = None) -> Decimal: """ ⚠️ 已废弃:基于 EU 数量计算成本 此函数使用旧的计算方式,已废弃。 新的计费逻辑中,EU = Cost(美元),即 1 EU = 1 美元。 Args: eu: EU数量 unit_price: 单价(可直接指定,优先级最高) subscription_tier: 订阅等级(用于自动获取单价) Returns: 成本金额(USD) """ import warnings warnings.warn( "calculate_cost() 已废弃,新计费逻辑中 EU = Cost(美元)", DeprecationWarning, stacklevel=2 ) if unit_price is None: unit_price = get_eu_price_by_tier(subscription_tier) if subscription_tier else DEFAULT_EU_PRICE return Decimal(eu) * unit_price # ============= 余额与授信管理 ============= async def get_available_balance(user_id: str, db: AsyncSession) -> Tuple[Decimal, Decimal, Decimal]: """ 获取可用额度:账户余额 + 授信额度 Returns: (账户余额, 授信额度, 可用额度) """ # 从 Balance 表获取余额 balance_result = await db.execute( select(Balance).where(Balance.user_id == user_id) ) balance_obj = balance_result.scalar_one_or_none() # 从 User 表获取授信额度 user_result = await db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: return Decimal(0), Decimal(0), Decimal(0) balance = Decimal(str(balance_obj.eu_balance)) if balance_obj else Decimal(0) credit_limit = Decimal(str(user.credit_limit)) available = balance + credit_limit return balance, credit_limit, available async def check_balance_sufficient( user_id: str, required_amount: Decimal, db: AsyncSession ) -> bool: """ 检查余额是否充足 Args: user_id: 用户ID required_amount: 所需金额 db: 数据库会话 Returns: 是否充足 """ balance, credit_limit, available = await get_available_balance(user_id, db) return available >= required_amount async def deduct_balance( user_id: str, amount: Decimal, db: AsyncSession, description: str = "消费", auto_commit: bool = True # 🔧 修改:默认自动提交,确保扣款立即生效 ) -> Tuple[bool, str]: """ 扣除余额(优先扣除账户余额,不足时使用授信额度) 🔧 安全修复:使用真正的原子操作,在一条SQL中完成余额检查和扣款, 彻底避免竞态条件。 Args: user_id: 用户ID amount: 扣除金额(自动统一精度到6位小数) db: 数据库会话 description: 描述 auto_commit: 是否自动提交(默认True,确保扣款立即生效) Returns: (是否成功, 消息) Note: 修复后默认 auto_commit=True,确保扣款立即生效,避免长时间事务导致的问题。 如需批量操作,请明确设置 auto_commit=False。 """ from sqlalchemy import text # 🔧 修复1: 统一精度到6位小数,避免累积误差 amount = amount.quantize(Decimal('0.000001')) # 从 User 表获取授信额度 user_result = await db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: return False, "用户不存在" credit_limit = Decimal(str(user.credit_limit)) amount_str = str(amount) credit_str = str(credit_limit) # 🔧 修复2: 真正的原子操作 - 分步执行但在同一事务中 # Step 1: 确保余额记录存在(生成UUID) await db.execute( text(""" INSERT INTO balances (id, user_id, eu_balance, created_at, updated_at) VALUES (gen_random_uuid(), :uid, 0.0, NOW(), NOW()) ON CONFLICT (user_id) DO NOTHING """), {"uid": user_id} ) # Step 2: 原子更新 - 只有在余额充足时才扣款 result = await db.execute( text(""" WITH current AS ( SELECT eu_balance FROM balances WHERE user_id = :uid FOR UPDATE ) UPDATE balances SET eu_balance = eu_balance - CAST(:amount AS NUMERIC(15, 6)), updated_at = NOW() WHERE user_id = :uid AND (SELECT eu_balance FROM current) + CAST(:credit AS NUMERIC(15, 6)) >= CAST(:amount AS NUMERIC(15, 6)) RETURNING eu_balance as new_balance, eu_balance + CAST(:amount AS NUMERIC(15, 6)) as old_balance """), {"uid": user_id, "amount": amount_str, "credit": credit_str} ) row = result.fetchone() if row is None: # UPDATE 影响了0行,说明余额不足 # 查询当前余额用于错误信息 balance_result = await db.execute( text("SELECT eu_balance FROM balances WHERE user_id = :uid"), {"uid": user_id} ) balance_row = balance_result.fetchone() current = Decimal(str(balance_row[0])) if balance_row else Decimal("0") available = current + credit_limit if auto_commit: await db.commit() # 即使失败也提交(没有实际修改) return False, f"余额不足,当前可用: {available:.6f}, 需要: {amount:.6f}" new_balance = Decimal(str(row[0])) old_balance = Decimal(str(row[1])) # 可选:自动提交 if auto_commit: await db.commit() logger.info( f"💰 扣款成功并已提交: user={user_id[:8]}, " f"金额={amount:.6f}, 余额: {old_balance:.6f} → {new_balance:.6f}" ) else: logger.info( f"💰 扣款成功待提交: user={user_id[:8]}, " f"金额={amount:.6f}, 余额: {old_balance:.6f} → {new_balance:.6f}" ) return True, f"成功扣除 {amount:.6f} 元,余额: {old_balance:.6f} → {new_balance:.6f}" async def add_balance( user_id: str, amount: Decimal, db: AsyncSession, description: str = "充值", auto_commit: bool = True # 🔧 修改:默认自动提交,与deduct_balance保持一致 ) -> Tuple[bool, str]: """ 增加余额 🔧 安全修复:使用原子操作,统一精度处理 Args: user_id: 用户ID amount: 充值金额(自动统一精度到6位小数) db: 数据库会话 description: 描述 auto_commit: 是否自动提交(默认True) Returns: (是否成功, 消息) Note: 修复后默认 auto_commit=True,与 deduct_balance 保持一致。 """ from sqlalchemy import text # 🔧 修复:统一精度到6位小数 amount = amount.quantize(Decimal('0.000001')) # 检查用户是否存在 user_result = await db.execute( select(User).where(User.id == user_id) ) user = user_result.scalar_one_or_none() if not user: return False, "用户不存在" amount_str = str(amount) # 🔧 修复:使用原子操作 - 分步执行 # Step 1: 确保余额记录存在(生成UUID) await db.execute( text(""" INSERT INTO balances (id, user_id, eu_balance, created_at, updated_at) VALUES (gen_random_uuid(), :uid, 0.0, NOW(), NOW()) ON CONFLICT (user_id) DO NOTHING """), {"uid": user_id} ) # Step 2: 原子更新 - 增加余额 result = await db.execute( text(""" UPDATE balances SET eu_balance = eu_balance + CAST(:amount AS NUMERIC(15, 6)), updated_at = NOW() WHERE user_id = :uid RETURNING eu_balance as new_balance, eu_balance - CAST(:amount AS NUMERIC(15, 6)) as old_balance """), {"uid": user_id, "amount": amount_str} ) row = result.fetchone() new_balance = Decimal(str(row[0])) if row else amount old_balance = Decimal(str(row[1])) if row else Decimal("0") # 可选:自动提交 if auto_commit: await db.commit() logger.info( f"💰 充值成功并已提交: user={user_id[:8]}, " f"金额={amount:.6f}, 余额: {old_balance:.6f} → {new_balance:.6f}" ) else: logger.info( f"💰 充值成功待提交: user={user_id[:8]}, " f"金额={amount:.6f}, 余额: {old_balance:.6f} → {new_balance:.6f}" ) return True, f"成功充值 {amount:.6f} 元,余额: {old_balance:.6f} → {new_balance:.6f}" return True, f"成功充值 {amount} 元,当前余额: {new_balance}" # ============= 计费记录创建(旧版,已废弃) ============= async def create_billing_record( tenant_id: str, agent_id: str, agent_name: str, duration_seconds: int, db: AsyncSession, channel_id: Optional[str] = None, ) -> BillingRecord: """ 创建计费记录 ⚠️ 已废弃 (DEPRECATED) 此函数使用旧的 BillingRecord 表,已废弃。 请使用 create_agent_billing_record() 函数替代。 Args: tenant_id: 租户ID agent_id: Agent ID agent_name: Agent名称 duration_seconds: 调用时长(秒) db: 数据库会话 channel_id: 渠道ID(可选) Returns: 计费记录 计费说明: 根据租户订阅等级自动计算EU单价: - 入门级(Starter/free):$0.015 / EU - 专业级(Pro):$0.02 / EU - 企业级(Enterprise):$0.03 / EU """ import warnings warnings.warn( "create_billing_record() 已废弃,请使用 create_agent_billing_record() 替代", DeprecationWarning, stacklevel=2 ) # 获取用户信息(包括订阅等级和channel_id) result = await db.execute( select(User.channel_id, User.subscription_tier).where(User.id == tenant_id) ) row = result.first() subscription_tier = "pro" # 默认专业级 if row: if not channel_id and row[0]: channel_id = str(row[0]) if row[1]: subscription_tier = row[1] # 计算EU和成本(根据订阅等级) eu = calculate_eu(duration_seconds) cost = calculate_cost(eu, subscription_tier=subscription_tier) # 创建计费记录 record = BillingRecord( timestamp=datetime.utcnow(), channel_id=channel_id, tenant_id=tenant_id, agent_id=agent_id, agent_name=agent_name, duration=duration_seconds, eu=eu, cost=cost, ) db.add(record) # 扣除用户余额 success, message = await deduct_balance(tenant_id, cost, db, f"Agent调用: {agent_name}") if not success: # 如果余额不足,记录但不扣除(实际应该阻止调用) # 这里简化处理,只记录 pass await db.commit() await db.refresh(record) return record # ============= 资源配额检查 ============= async def check_agent_quota( user_id: str, agent_id: str, db: AsyncSession ) -> Tuple[bool, str]: """ 检查Agent配额是否足够 Args: user_id: 用户ID agent_id: Agent ID db: 数据库会话 Returns: (是否有配额, 消息) """ # 简化实现:实际应该查询ResourceAllocation表 # 这里只是返回True,实际应该检查配额 return True, "配额充足" async def check_model_quota( user_id: str, model_name: str, db: AsyncSession ) -> Tuple[bool, int, int]: """ 检查模型配额(RPM/TPM) Args: user_id: 用户ID model_name: 模型名称 db: 数据库会话 Returns: (是否有配额, 当前RPM, 当前TPM) """ # 简化实现:实际应该查询ResourceAllocation和使用情况 # 这里只是返回默认值 return True, 60, 60000 # ============= 资源分配层级验证 ============= async def validate_resource_allocation( parent_type: str, # channel | tenant parent_id: str, resource_type: str, # agent | model resource_id: str, quantity: int, db: AsyncSession ) -> Tuple[bool, str]: """ 验证资源分配是否超过上级分配的资源 Args: parent_type: 父级类型 parent_id: 父级ID resource_type: 资源类型 resource_id: 资源ID quantity: 数量 db: 数据库会话 Returns: (是否有效, 消息) """ # 简化实现:实际应该递归检查上级的配额 # 这里只是返回True return True, "资源分配有效" # ============= 工作流限制验证 ============= def validate_workflow_nodes(nodes: list) -> Tuple[bool, str]: """ 验证工作流节点数量限制 Args: nodes: 节点列表 Returns: (是否有效, 消息) """ if len(nodes) > 3: return False, "工作流最多支持3个Agent节点" return True, "节点数量有效" # ============= 平台Agent资源固定配置 ============= PLATFORM_AGENT_CONFIG = { "cpu": 2.0, # 2核 "memory": 4.0, # 4GB } def get_platform_agent_resources() -> dict: """ 获取平台Agent的固定资源配置 Returns: 资源配置字典 """ return PLATFORM_AGENT_CONFIG.copy() # ============= 统计辅助函数 ============= async def calculate_monthly_cost(user_id: str, db: AsyncSession) -> Decimal: """ 计算用户本月消费(从 AgentBillingRecord 和 ModelBillingRecord 合并统计) Args: user_id: 用户ID db: 数据库会话 Returns: 本月消费金额 """ from sqlalchemy import func from models import ModelBillingRecord # 获取本月第一天 now = datetime.utcnow() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) # Agent 计费 agent_result = await db.execute( select(func.sum(AgentBillingRecord.cost)) .where(AgentBillingRecord.user_id == user_id) .where(AgentBillingRecord.start_time >= month_start) ) agent_cost = agent_result.scalar() or 0 # 模型调用计费 model_result = await db.execute( select(func.sum(ModelBillingRecord.total_cost)) .where(ModelBillingRecord.tenant_id == user_id) .where(ModelBillingRecord.created_at >= month_start) ) model_cost = model_result.scalar() or 0 total = Decimal(str(agent_cost)) + Decimal(str(model_cost)) return total async def calculate_channel_commission( channel_id: str, start_date: datetime, end_date: datetime, db: AsyncSession ) -> Tuple[Decimal, Decimal]: """ 计算渠道佣金(从 AgentBillingRecord 和 ModelBillingRecord 合并统计) Args: channel_id: 渠道ID start_date: 开始日期 end_date: 结束日期 db: 数据库会话 Returns: (总收入, 佣金金额) """ from sqlalchemy import func from models import ModelBillingRecord # 查询渠道佣金率 result = await db.execute( select(Channel.commission_rate).where(Channel.id == channel_id) ) row = result.first() commission_rate = Decimal(str(row[0])) / 100 if row else Decimal(0) # Agent 计费收入 agent_result = await db.execute( select(func.sum(AgentBillingRecord.cost)) .where(AgentBillingRecord.channel_id == channel_id) .where(AgentBillingRecord.start_time >= start_date) .where(AgentBillingRecord.start_time <= end_date) ) agent_revenue = agent_result.scalar() or 0 # 模型调用计费收入 model_result = await db.execute( select(func.sum(ModelBillingRecord.total_cost)) .where(ModelBillingRecord.channel_id == channel_id) .where(ModelBillingRecord.created_at >= start_date) .where(ModelBillingRecord.created_at <= end_date) ) model_revenue = model_result.scalar() or 0 total_revenue = Decimal(str(agent_revenue)) + Decimal(str(model_revenue)) commission = total_revenue * commission_rate return total_revenue, commission # ============= Agent 计费配置 ============= # Agent 资源单价配置(按资源类型) AGENT_RESOURCE_PRICING = { "cpu_per_hour": Decimal("0.05"), # CPU 每核每小时 $0.05 "memory_per_gb_hour": Decimal("0.01"), # 内存每 GB 每小时 $0.01 } # 平台 Agent 固定价格(按模板) PLATFORM_AGENT_PRICING = { "gpt-assistant": Decimal("0.10"), # GPT 助手每小时 $0.10 "code-reviewer": Decimal("0.15"), # 代码审查每小时 $0.15 "data-analyst": Decimal("0.12"), # 数据分析每小时 $0.12 "default": Decimal("0.10"), # 默认每小时 $0.10 } # API 调用计费配置(Agent Manager 回调) API_CALL_PRICING = { "per_call": Decimal("0.01"), # 每次 API 调用 $0.01(0.01 EU/call) } def get_platform_agent_hourly_price(template_name: str) -> Decimal: """ 获取平台 Agent 每小时价格 Args: template_name: 模板名称 Returns: 每小时价格(USD) """ return PLATFORM_AGENT_PRICING.get(template_name, PLATFORM_AGENT_PRICING["default"]) def calculate_agent_cost_by_resources( cpu_cores: float, memory_gb: float, duration_seconds: int ) -> Decimal: """ 根据资源使用量计算成本(用于自定义 Agent) Args: cpu_cores: CPU 核心数 memory_gb: 内存 GB 数 duration_seconds: 运行时长(秒) Returns: 成本金额(USD) """ hours = Decimal(str(duration_seconds)) / Decimal("3600") cpu_cost = Decimal(str(cpu_cores)) * hours * AGENT_RESOURCE_PRICING["cpu_per_hour"] memory_cost = Decimal(str(memory_gb)) * hours * AGENT_RESOURCE_PRICING["memory_per_gb_hour"] return cpu_cost + memory_cost def calculate_platform_agent_cost( template_name: str, duration_seconds: int ) -> Decimal: """ 计算平台 Agent 成本(固定价格) Args: template_name: 模板名称 duration_seconds: 运行时长(秒) Returns: 成本金额(USD) """ hours = Decimal(str(duration_seconds)) / Decimal("3600") hourly_price = get_platform_agent_hourly_price(template_name) return hours * hourly_price def calculate_api_call_cost() -> Decimal: """ 计算 API 调用成本(固定费用) 用于 Agent Manager 回调的 API 调用计费。 每次调用固定收费 0.001 EU(= $0.001),与运行时间无关。 Returns: 成本金额(USD):0.001 """ return API_CALL_PRICING["per_call"] # ============= Agent 计费记录创建 ============= async def create_agent_billing_record( user_id: str, agent_name: str, agent_type: str, is_platform_agent: bool, duration_seconds: int, db: AsyncSession, channel_id: Optional[str] = None, cpu_used: Optional[str] = None, memory_used: Optional[str] = None, tools_used: Optional[list] = None, request_id: Optional[str] = None, ) -> "AgentBillingRecord": """ 创建 Agent 计费记录 Args: user_id: 用户 ID agent_name: Agent 名称 agent_type: Agent 类型/模板名称 is_platform_agent: 是否为平台 Agent duration_seconds: 运行时长(秒) db: 数据库会话 channel_id: 渠道 ID(可选) cpu_used: CPU 使用量(K8s 格式,如 "100m") memory_used: 内存使用量(K8s 格式,如 "128Mi") tools_used: 使用的工具列表(可选) request_id: 请求 ID(可选) Returns: 计费记录 """ from models import AgentBillingRecord # 计算成本 if is_platform_agent: cost = calculate_platform_agent_cost(agent_type, duration_seconds) else: # 解析资源量 cpu_cores = _parse_cpu_to_cores(cpu_used) if cpu_used else 0.1 memory_gb = _parse_memory_to_gb(memory_used) if memory_used else 0.125 cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, duration_seconds) # EU = Cost(1 EU = 1 美元) eu = float(cost) # 创建记录 record = AgentBillingRecord( user_id=user_id, channel_id=channel_id, agent_name=agent_name, agent_type=agent_type, is_platform_agent=is_platform_agent, period_start=datetime.utcnow(), duration_seconds=duration_seconds, eu_consumed=eu, # EU = Cost(美元) cpu_used=cpu_used, memory_used=memory_used, cost=cost, tools_used=tools_used, request_id=request_id, ) db.add(record) # 扣除用户余额 success, message = await deduct_balance(user_id, cost, db, f"Agent 使用: {agent_name}") await db.commit() await db.refresh(record) return record async def finalize_agent_billing( agent_name: str, user_id: str, db: AsyncSession ) -> Optional["AgentBillingRecord"]: """ 结算 Agent 计费(Agent 停止时调用) Args: agent_name: Agent 名称 user_id: 用户 ID db: 数据库会话 Returns: 更新后的计费记录 """ from models import AgentBillingRecord from sqlalchemy import and_ # 查找未结束的计费记录 result = await db.execute( select(AgentBillingRecord).where( and_( AgentBillingRecord.agent_name == agent_name, AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_end == None ) ) ) record = result.scalar_one_or_none() if not record: return None # 计算实际运行时长 record.period_end = datetime.utcnow() duration = (record.period_end - record.period_start).total_seconds() record.duration_seconds = int(duration) # 重新计算成本 if record.is_platform_agent: record.cost = calculate_platform_agent_cost(record.agent_type, int(duration)) else: cpu_cores = _parse_cpu_to_cores(record.cpu_used) if record.cpu_used else 0.1 memory_gb = _parse_memory_to_gb(record.memory_used) if record.memory_used else 0.125 record.cost = calculate_agent_cost_by_resources(cpu_cores, memory_gb, int(duration)) # EU = Cost(1 EU = 1 美元) record.eu_consumed = float(record.cost) await db.commit() await db.refresh(record) return record # ============= Agent 计费统计 ============= async def get_agent_billing_stats( user_id: str, start_date: datetime, end_date: datetime, db: AsyncSession ) -> dict: """ 获取 Agent 计费统计 Args: user_id: 用户 ID start_date: 开始日期 end_date: 结束日期 db: 数据库会话 Returns: 统计数据 """ from models import AgentBillingRecord from sqlalchemy import func, and_ # 总体统计 result = await db.execute( select( func.sum(AgentBillingRecord.cost).label("total_cost"), func.sum(AgentBillingRecord.duration_seconds).label("total_duration"), func.count(AgentBillingRecord.id).label("total_count") ).where( and_( AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ) ) row = result.first() total_cost = float(row.total_cost) if row.total_cost else 0 total_duration = int(row.total_duration) if row.total_duration else 0 total_count = int(row.total_count) if row.total_count else 0 # 按 Agent 类型统计 result = await db.execute( select( AgentBillingRecord.is_platform_agent, func.sum(AgentBillingRecord.cost).label("cost") ).where( and_( AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ).group_by(AgentBillingRecord.is_platform_agent) ) by_agent_type = {} for row in result.all(): agent_type = "platform" if row.is_platform_agent else "custom" by_agent_type[agent_type] = float(row.cost) if row.cost else 0 # 按模板统计 result = await db.execute( select( AgentBillingRecord.agent_type, func.sum(AgentBillingRecord.cost).label("cost") ).where( and_( AgentBillingRecord.user_id == user_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ).group_by(AgentBillingRecord.agent_type) ) by_template = {} for row in result.all(): by_template[row.agent_type] = float(row.cost) if row.cost else 0 return { "totalCost": total_cost, "totalDurationSeconds": total_duration, "totalRequests": total_count, "byAgentType": by_agent_type, "byTemplate": by_template, } async def get_channel_agent_billing_stats( channel_id: str, start_date: datetime, end_date: datetime, db: AsyncSession ) -> dict: """ 获取渠道 Agent 计费统计 Args: channel_id: 渠道 ID start_date: 开始日期 end_date: 结束日期 db: 数据库会话 Returns: 统计数据 """ from models import AgentBillingRecord from sqlalchemy import func, and_ # 总体统计 result = await db.execute( select( func.sum(AgentBillingRecord.cost).label("total_cost"), func.sum(AgentBillingRecord.duration_seconds).label("total_duration"), func.count(AgentBillingRecord.id).label("total_count") ).where( and_( AgentBillingRecord.channel_id == channel_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ) ) row = result.first() total_cost = float(row.total_cost) if row.total_cost else 0 total_duration = int(row.total_duration) if row.total_duration else 0 total_count = int(row.total_count) if row.total_count else 0 # 按用户统计 result = await db.execute( select( AgentBillingRecord.user_id, func.sum(AgentBillingRecord.cost).label("cost"), func.count(AgentBillingRecord.id).label("count") ).where( and_( AgentBillingRecord.channel_id == channel_id, AgentBillingRecord.period_start >= start_date, AgentBillingRecord.period_start <= end_date ) ).group_by(AgentBillingRecord.user_id) ) by_user = [] for row in result.all(): by_user.append({ "userId": row.user_id, "cost": float(row.cost) if row.cost else 0, "count": int(row.count) if row.count else 0, }) return { "totalCost": total_cost, "totalDurationSeconds": total_duration, "totalRequests": total_count, "byUser": by_user, } # ============= 资源解析辅助函数 ============= def _parse_cpu_to_cores(cpu_str: str) -> float: """ 解析 CPU 字符串为核心数 支持格式: - "100m" -> 0.1 核 - "1" -> 1 核 - "1.5" -> 1.5 核 """ if not cpu_str: return 0.0 cpu_str = cpu_str.strip().lower() if cpu_str.endswith("m"): return float(cpu_str[:-1]) / 1000 else: return float(cpu_str) def _parse_memory_to_gb(memory_str: str) -> float: """ 解析内存字符串为 GB 支持格式: - "128Mi" -> 0.125 GB - "1Gi" -> 1 GB - "512M" -> 0.5 GB - "2G" -> 2 GB """ if not memory_str: return 0.0 memory_str = memory_str.strip() # 处理 Kubernetes 格式 if memory_str.endswith("Gi"): return float(memory_str[:-2]) elif memory_str.endswith("Mi"): return float(memory_str[:-2]) / 1024 elif memory_str.endswith("Ki"): return float(memory_str[:-2]) / (1024 * 1024) # 处理简化格式 elif memory_str.endswith("G"): return float(memory_str[:-1]) elif memory_str.endswith("M"): return float(memory_str[:-1]) / 1024 elif memory_str.endswith("K"): return float(memory_str[:-1]) / (1024 * 1024) else: # 假设是字节 return float(memory_str) / (1024 * 1024 * 1024)