Files
taiji-AI-PAD/plans/计费系统代码修复指南.md
T
2026-01-09 06:56:43 +00:00

28 KiB
Raw Blame History

计费系统代码修复指南

版本: v1.0.0
创建时间: 2026-01-09
目标: 提供完整的代码修复方案,解决计费系统关键问题


🚨 紧急修复清单

优先级 问题 文件 状态
P0 Dashboard EU显示为0 monitoring.py ⚠️ 立即修复
P0 平台Agent分配时未计费 platform_agent_quota.py ⚠️ 立即修复
P0 缺少LiteLLM Token计费 billing_webhook.py 🚨 关键缺失
P1 计费表数据不统一 多个文件 🔄 下版本
P2 缺少model_name字段 models.py 📋 规划中

🔧 立即修复 - P0问题

修复1: Dashboard EU消耗显示为0

问题: Dashboard查询billing_records表,但Agent计费写入agent_billing_records表

修复文件: services/mcp-server/monitoring.py

# 🔍 定位: 第527-589行 _get_tenant_eu_consumption_24h 方法

async def _get_tenant_eu_consumption_24h(
    self,
    session: AsyncSession,
    tenant_id: str
) -> Dict[str, Any]:
    """
    获取租户过去24小时的EU消耗情况
    
    ✅ 修复:改为查询 agent_billing_records 表
    """
    try:
        # ❌ 原代码:查询 billing_records
        # ✅ 新代码:查询 agent_billing_records
        total_result = await session.execute(
            text("""
                SELECT
                    COALESCE(SUM(eu_consumed), 0) as total_eu,
                    COALESCE(SUM(cost), 0) as total_cost,
                    COUNT(*) as total_calls
                FROM agent_billing_records
                WHERE user_id = :tenant_id
                AND start_time > NOW() - INTERVAL '24 hours'
            """),
            {"tenant_id": tenant_id}
        )
        
        # ✅ 同样修改每小时统计查询
        hourly_result = await session.execute(
            text("""
                SELECT
                    DATE_TRUNC('hour', start_time) as hour,
                    COALESCE(SUM(eu_consumed), 0) as eu,
                    COALESCE(SUM(cost), 0) as cost,
                    COUNT(*) as calls
                FROM agent_billing_records
                WHERE user_id = :tenant_id
                AND start_time > NOW() - INTERVAL '24 hours'
                GROUP BY DATE_TRUNC('hour', start_time)
                ORDER BY hour
            """),
            {"tenant_id": tenant_id}
        )
        
        total_row = total_result.fetchone()
        hourly_rows = hourly_result.fetchall()
        
        return {
            "total": float(total_row[0]) if total_row else 0,
            "totalCost": float(total_row[1]) if total_row else 0,
            "totalCalls": int(total_row[2]) if total_row else 0,
            "hourlyData": [
                {
                    "timestamp": row[0].isoformat() if row[0] else None,
                    "value": float(row[1]),
                    "cost": float(row[2]),
                    "calls": int(row[3])
                }
                for row in hourly_rows
            ]
        }
    except Exception as e:
        logger.warning(f"获取租户EU消耗失败: {e}")
        return {"total": 0, "totalCost": 0, "totalCalls": 0, "hourlyData": []}

⚠️ 注意事项:

  • 确保所有相关查询都改为使用agent_billing_records表
  • 字段映射: eu → eu_consumed, timestamp → start_time
  • ⚠️ 重要: 这只解决Agent运行时长计费,模型Token计费需要LiteLLM webhook
  • 测试验证: 修改后检查Dashboard显示是否正常

修复2: 平台Agent分配时未创建计费记录

问题: 渠道分配平台Agent给租户时,Pod启动但没有创建计费记录

修复文件: services/mcp-server/app/routes/platform_agent_quota.py

# 🔍 定位: 第540-568行,在创建Agent记录后添加计费记录

# 现有代码: 创建 Agent 记录
agent = Agent(
    name=pod_name,
    type="platform",
    template=request.templateName,
    pod_name=result.name,
    k8s_namespace=result.namespace,
    k8s_status=result.status,
    service_port=result.service_port,
    owner_id=tenant_uuid,
    status="active"
)
db.add(agent)

# ✅ 新增: 创建计费记录
from datetime import datetime
from models import AgentBillingRecord

billing_record = AgentBillingRecord(
    user_id=tenant_uuid,
    channel_id=channel_uuid,  # 需要获取channel_uuid
    agent_type=request.templateName,
    agent_name=pod_name,
    is_platform_agent=True,
    start_time=datetime.utcnow(),
    cpu_used=agent_config.cpu_request,
    memory_used=agent_config.memory_request,
    duration_seconds=0,  # 初始为0,停止时更新
    eu_consumed=0,       # 初始为0,停止时计算
    cost=0,              # 初始为0,停止时计算
)
db.add(billing_record)

logger.info(
    f"创建平台Agent计费记录: tenant_id={tenant_id}, "
    f"agent_name={pod_name}, template={request.templateName}"
)

修复停止Agent时的计费逻辑: platform_agent_quota.py 第987-1068行

# 🔍 在stop_platform_agent函数中,删除Agent记录之前添加:

# ✅ 新增: 结束计费记录
from sqlalchemy import and_
from app.billing import calculate_platform_agent_cost, calculate_eu

billing_result = await db.execute(
    select(AgentBillingRecord).where(
        and_(
            AgentBillingRecord.agent_name == agent_name,
            AgentBillingRecord.user_id == uuid.UUID(user_id),
            AgentBillingRecord.is_platform_agent == True,
            AgentBillingRecord.end_time == None  # 查找未结束的记录
        )
    )
)
billing_record = billing_result.scalar_one_or_none()

if billing_record:
    billing_record.end_time = datetime.utcnow()
    duration = (billing_record.end_time - billing_record.start_time).total_seconds()
    billing_record.duration_seconds = int(duration)
    billing_record.eu_consumed = calculate_eu(int(duration))
    billing_record.cost = calculate_platform_agent_cost(
        billing_record.agent_type, 
        int(duration)
    )
    
    logger.info(
        f"结束平台Agent计费: {agent_name}, "
        f"运行时长={duration}秒, EU={billing_record.eu_consumed}"
    )
else:
    logger.warning(f"未找到Agent {agent_name} 的计费记录")

⚠️ 注意事项:

  • 需要在函数开头获取channel_uuid
  • 确保导入必要的模块和函数
  • 添加错误处理和日志记录
  • ⚠️ 重要: 这只是Agent容器运行计费,真正的模型调用计费需要LiteLLM webhook

🚨 关键缺失 - LiteLLM Token计费实现

问题: 当前系统只有Agent运行时长计费,缺少基于Token的精确模型调用计费

根本原因:

  • 现有计费只能统计Agent容器运行时间
  • 无法获取实际的模型调用次数、Token消耗、具体使用的模型
  • Dashboard的模型使用统计完全无数据来源

修复3: 实现LiteLLM Callback Webhook

必要性: 这是最重要的计费功能,涉及:

  • ✅ 精确的Token计费(按实际使用量)
  • ✅ 模型使用统计(GPT-4、Claude等)
  • ✅ 成本控制(根据不同模型定价)
  • ✅ 用户配额管理(防止超额使用)

步骤1: 创建模型计费表

新建文件: services/mcp-server/migrations/012_add_model_billing_records.sql

-- 模型调用计费记录表 - LiteLLM Callback数据
CREATE TABLE IF NOT EXISTS model_billing_records (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    -- 关联信息(从LiteLLM metadata解析)
    tenant_id UUID REFERENCES users(id),
    channel_id UUID REFERENCES channels(id),
    
    -- LiteLLM回调数据
    litellm_call_id VARCHAR(100) UNIQUE NOT NULL,
    api_key VARCHAR(255),  -- 使用的API Key(脱敏)
    team_id VARCHAR(100),  -- LiteLLM Team ID
    
    -- 模型信息
    model_name VARCHAR(100) NOT NULL,  -- 如 azure/gpt-4
    model_id VARCHAR(100),
    
    -- Token用量
    input_tokens INTEGER NOT NULL DEFAULT 0,
    output_tokens INTEGER NOT NULL DEFAULT 0,
    total_tokens INTEGER NOT NULL DEFAULT 0,
    
    -- 费用计算
    input_cost NUMERIC(12, 6) DEFAULT 0,
    output_cost NUMERIC(12, 6) DEFAULT 0,
    total_cost NUMERIC(12, 6) DEFAULT 0,
    eu_consumed NUMERIC(12, 4) DEFAULT 0,
    
    -- 调用信息
    request_id VARCHAR(100),
    call_type VARCHAR(50) DEFAULT 'completion',
    status VARCHAR(20) DEFAULT 'success',
    
    -- 时间信息
    start_time TIMESTAMP WITH TIME ZONE,
    end_time TIMESTAMP WITH TIME ZONE,
    response_time_ms INTEGER,
    
    -- 原始数据
    raw_callback_data JSONB
);

-- 创建索引
CREATE INDEX idx_model_billing_tenant ON model_billing_records(tenant_id);
CREATE INDEX idx_model_billing_created ON model_billing_records(created_at);
CREATE INDEX idx_model_billing_model ON model_billing_records(model_name);

步骤2: 创建数据模型

修改文件: services/mcp-server/models.py - 添加新模型

class ModelBillingRecord(BaseModel, Base):
    """模型调用计费记录 - LiteLLM Callback数据"""
    __tablename__ = "model_billing_records"
    
    # 关联信息
    tenant_id = Column(GUID(), ForeignKey("users.id"))
    channel_id = Column(GUID(), ForeignKey("channels.id"))
    
    # LiteLLM回调数据
    litellm_call_id = Column(String(100), unique=True, nullable=False)
    api_key = Column(String(255))
    team_id = Column(String(100))
    
    # 模型信息
    model_name = Column(String(100), nullable=False)
    model_id = Column(String(100))
    
    # Token用量
    input_tokens = Column(Integer, default=0)
    output_tokens = Column(Integer, default=0)
    total_tokens = Column(Integer, default=0)
    
    # 费用计算
    input_cost = Column(sa.Numeric(12, 6), default=0)
    output_cost = Column(sa.Numeric(12, 6), default=0)
    total_cost = Column(sa.Numeric(12, 6), default=0)
    eu_consumed = Column(sa.Numeric(12, 4), default=0)
    
    # 调用信息
    request_id = Column(String(100))
    call_type = Column(String(50), default="completion")
    status = Column(String(20), default="success")
    
    # 时间信息
    start_time = Column(DateTime)
    end_time = Column(DateTime)
    response_time_ms = Column(Integer)
    
    # 原始数据
    raw_callback_data = Column(JSON)
    
    # 关联关系
    tenant = relationship("User")
    channel = relationship("Channel")

步骤3: 创建Webhook接口

新建文件: services/mcp-server/app/routes/billing_webhook.py

"""
LiteLLM Callback Webhook路由
接收LiteLLM的实时Token计费数据
"""
import logging
from datetime import datetime
from typing import Optional, Dict, Any
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, Field

from database import get_db
from models import ModelBillingRecord, User, TenantModelKey

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/billing", tags=["计费Webhook"])


class LiteLLMUsage(BaseModel):
    """Token使用量"""
    prompt_tokens: int = 0
    completion_tokens: int = 0
    total_tokens: int = 0


class LiteLLMCallbackData(BaseModel):
    """LiteLLM Callback数据"""
    call_id: str = Field(..., alias="id")
    model: str
    usage: Optional[LiteLLMUsage] = None
    response_cost: Optional[float] = None
    api_key: Optional[str] = None
    team_id: Optional[str] = None
    startTime: Optional[str] = None
    endTime: Optional[str] = None
    response_time: Optional[float] = None
    status: str = "success"
    metadata: Optional[Dict[str, Any]] = None


async def get_tenant_from_api_key(api_key: str, db: AsyncSession) -> tuple[Optional[str], Optional[str]]:
    """从API Key解析租户ID和渠道ID"""
    if not api_key:
        return None, None
    
    # 查询租户模型密钥表
    result = await db.execute(
        select(TenantModelKey).where(TenantModelKey.encrypted_key.contains(api_key[:10]))
    )
    key_record = result.scalar_one_or_none()
    
    if key_record:
        return str(key_record.tenant_id), str(key_record.channel_id)
    
    return None, None


def calculate_eu_from_tokens(total_tokens: int, model_name: str) -> Decimal:
    """根据Token计算EU消耗"""
    # 不同模型的Token到EU转换率
    MODEL_EU_RATE = {
        "gpt-4": Decimal("0.0001"),      # 1000 tokens = 0.1 EU
        "gpt-3.5-turbo": Decimal("0.00005"),  # 1000 tokens = 0.05 EU
        "claude": Decimal("0.0001"),
        "default": Decimal("0.0001"),
    }
    
    # 匹配模型名称
    rate = MODEL_EU_RATE["default"]
    for model_key, model_rate in MODEL_EU_RATE.items():
        if model_key in model_name.lower():
            rate = model_rate
            break
    
    return Decimal(total_tokens) * rate


@router.post("/litellm-callback")
async def litellm_callback(
    callback_data: LiteLLMCallbackData,
    db: AsyncSession = Depends(get_db)
):
    """接收LiteLLM的Token计费回调"""
    try:
        # 检查是否已处理过(幂等性)
        existing = await db.execute(
            select(ModelBillingRecord).where(
                ModelBillingRecord.litellm_call_id == callback_data.call_id
            )
        )
        if existing.scalar_one_or_none():
            return {"message": "Already processed", "call_id": callback_data.call_id}
        
        # 解析租户和渠道ID
        tenant_id, channel_id = await get_tenant_from_api_key(callback_data.api_key, db)
        
        if not tenant_id:
            logger.warning(f"无法解析租户ID: api_key={callback_data.api_key[:10]}...")
        
        # 计算Token和EU
        usage = callback_data.usage or LiteLLMUsage()
        eu_consumed = calculate_eu_from_tokens(usage.total_tokens, callback_data.model)
        
        # 解析时间
        start_time = None
        end_time = None
        if callback_data.startTime:
            start_time = datetime.fromisoformat(callback_data.startTime.replace('Z', '+00:00'))
        if callback_data.endTime:
            end_time = datetime.fromisoformat(callback_data.endTime.replace('Z', '+00:00'))
        
        # 创建计费记录
        record = ModelBillingRecord(
            tenant_id=tenant_id,
            channel_id=channel_id,
            litellm_call_id=callback_data.call_id,
            api_key=callback_data.api_key[:8] + "..." + callback_data.api_key[-4:] if callback_data.api_key else None,
            team_id=callback_data.team_id,
            model_name=callback_data.model,
            input_tokens=usage.prompt_tokens,
            output_tokens=usage.completion_tokens,
            total_tokens=usage.total_tokens,
            total_cost=Decimal(callback_data.response_cost or 0),
            eu_consumed=eu_consumed,
            status=callback_data.status,
            start_time=start_time,
            end_time=end_time,
            response_time_ms=int((callback_data.response_time or 0) * 1000),
            raw_callback_data=callback_data.dict()
        )
        
        db.add(record)
        await db.commit()
        
        logger.info(f"记录模型调用计费: {callback_data.model}, tokens={usage.total_tokens}, EU={eu_consumed}")
        
        return {
            "message": "Success", 
            "call_id": callback_data.call_id,
            "record_id": str(record.id)
        }
        
    except Exception as e:
        logger.error(f"LiteLLM Callback处理失败: {e}")
        await db.rollback()
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/litellm-callback/health")
async def callback_health():
    """Webhook健康检查"""
    return {"status": "ok", "endpoint": "/api/v1/billing/litellm-callback"}

步骤4: 注册路由

修改文件: services/mcp-server/main.py - 添加路由

# 导入新路由
from app.routes.billing_webhook import router as billing_webhook_router

# 注册路由
app.include_router(billing_webhook_router)

步骤5: 配置LiteLLM Webhook

修改文件: services/model-gateway/config/litellm.yaml

general_settings:
  master_key: "sk-taiji-prod-2026"
  database_url: "postgresql://..."
  
  # ✅ 关键配置:成功回调
  success_callback: ["webhook"]
  failure_callback: ["webhook"]
  
  # Webhook配置 - 指向mcp-server的8002端口
  webhook_url: "http://mcp-server:8002/api/v1/billing/litellm-callback"
  webhook_headers: 
    "Content-Type": "application/json"
  
# 模型配置
model_list:
  - model_name: azure/gpt-4
    litellm_params:
      model: azure/gpt-4
      api_key: os.environ/AZURE_OPENAI_API_KEY
      # 在metadata中传递租户信息
      metadata:
        tenant_tracking: true

步骤5.1: Docker环境变量配置

修改文件: docker-compose.yml - 更新mcp-server服务配置

services:
  mcp-server:
    ports:
      - "8002:8002"  # 确保使用8002端口
    environment:
      - PORT=8002
      - LITELLM_BASE_URL=https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io
      - LITELLM_MASTER_KEY=sk-taiji-prod-2026
      - CALLBACK_SECRET=taiji-webhook-secret-2026

步骤5.2: 网络连通性验证

⚠️ 关键检查:确保LiteLLM能访问mcp-server的webhook端点

# 从LiteLLM容器测试连通性(如果LiteLLM运行在Docker内)
docker exec -it litellm-container curl http://mcp-server:8002/api/v1/billing/litellm-callback/health

# 或者从外部测试(如果LiteLLM是外部服务)
curl http://your-mcp-server-public-ip:8002/api/v1/billing/litellm-callback/health

步骤6: 更新Dashboard查询

修改文件: services/mcp-server/monitoring.py - 合并两种计费数据

async def _get_tenant_eu_consumption_24h(
    self,
    session: AsyncSession,
    tenant_id: str
) -> Dict[str, Any]:
    """获取租户EU消耗 - 合并Agent运行时长和模型Token计费"""
    try:
        # Agent运行时长计费
        agent_result = await session.execute(
            text("""
                SELECT
                    COALESCE(SUM(eu_consumed), 0) as total_eu,
                    COALESCE(SUM(cost), 0) as total_cost,
                    COUNT(*) as total_calls
                FROM agent_billing_records
                WHERE user_id = :tenant_id
                AND start_time > NOW() - INTERVAL '24 hours'
            """),
            {"tenant_id": tenant_id}
        )
        
        # 模型Token计费
        model_result = await session.execute(
            text("""
                SELECT
                    COALESCE(SUM(eu_consumed), 0) as total_eu,
                    COALESCE(SUM(total_cost), 0) as total_cost,
                    COUNT(*) as total_calls
                FROM model_billing_records
                WHERE tenant_id = :tenant_id
                AND created_at > NOW() - INTERVAL '24 hours'
            """),
            {"tenant_id": tenant_id}
        )
        
        agent_row = agent_result.fetchone()
        model_row = model_result.fetchone()
        
        # 合并数据
        total_eu = float(agent_row[0] if agent_row else 0) + float(model_row[0] if model_row else 0)
        total_cost = float(agent_row[1] if agent_row else 0) + float(model_row[1] if model_row else 0)
        total_calls = int(agent_row[2] if agent_row else 0) + int(model_row[2] if model_row else 0)
        
        return {
            "total": total_eu,
            "totalCost": total_cost,
            "totalCalls": total_calls,
            "agentEU": float(agent_row[0] if agent_row else 0),
            "modelEU": float(model_row[0] if model_row else 0)
        }
        
    except Exception as e:
        logger.error(f"获取EU消耗失败: {e}")
        return {"total": 0, "totalCost": 0, "totalCalls": 0}

🔧 代码修改清单

文件1: services/mcp-server/monitoring.py

# 需要修改的方法列表:
- _get_tenant_eu_consumption_24h (第527-589行)
- _get_tenant_model_usage_30d (第605-619行) 
- get_dashboard_stats (第65-132行相关查询)

# 关键修改点:
- billing_records → agent_billing_records
- eu → eu_consumed  
- timestamp → start_time
- tenant_id → user_id

文件2: services/mcp-server/app/routes/platform_agent_quota.py

# 需要修改的函数:
- allocate_platform_agent_to_tenant (第404-579行)
- stop_platform_agent (第987-1068行)

# 需要导入:
+ from datetime import datetime
+ from models import AgentBillingRecord
+ from app.billing import calculate_eu, calculate_platform_agent_cost
+ from sqlalchemy import and_

文件3: services/mcp-server/app/billing.py

确保包含以下函数(如果不存在需要添加):

def calculate_platform_agent_cost(agent_type: str, duration_seconds: int) -> Decimal:
    """
    计算平台Agent运行成本
    
    Args:
        agent_type: Agent模板类型 (如 gpt-assistant)
        duration_seconds: 运行时长(秒)
    
    Returns:
        成本(美元)
    """
    # 平台Agent固定小时价格
    PLATFORM_AGENT_PRICING = {
        "gpt-assistant": Decimal("0.10"),
        "code-reviewer": Decimal("0.15"), 
        "data-analyst": Decimal("0.12"),
        "default": Decimal("0.10"),
    }
    
    hourly_rate = PLATFORM_AGENT_PRICING.get(agent_type, PLATFORM_AGENT_PRICING["default"])
    hours = Decimal(duration_seconds) / 3600
    return hourly_rate * hours

def calculate_eu(duration_seconds: int) -> int:
    """
    计算EU:1 EU = 10秒,不足10秒按1 EU计算
    """
    import math
    return math.ceil(duration_seconds / 10)

🧪 测试验证指南

测试账号

角色 邮箱 密码 说明
超级管理员 superadmin@taiji-ai.com Admin@123456 全局配置和审批
渠道管理员 66@66.com 66 渠道资源管理
租户用户 55@55.com 55 租户端使用

测试1: Dashboard EU显示测试

# 1. 启动一个自定义Agent(应该已有计费记录)
curl -X POST http://localhost:8002/api/user/custom-agents \
  -H "Authorization: Bearer $TENANT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "test-agent",
    "template": "gpt-assistant",
    "cpuRequest": "100m",
    "memoryRequest": "128Mi"
  }'

# 2. 等待片刻,然后检查Dashboard
curl http://localhost:8002/api/v1/monitoring/dashboard \
  -H "Authorization: Bearer $TENANT_TOKEN"

# 3. 验证返回数据中 euConsumption24h.total > 0

测试2: 平台Agent计费记录

# 1. 渠道分配平台Agent给租户
curl -X POST http://localhost:8002/api/channel/tenants/{tenant_id}/platform-agents \
  -H "Authorization: Bearer $CHANNEL_TOKEN" \
  -d '{
    "templateName": "gpt-assistant",
    "podQuota": 1
  }'

# 2. 检查数据库计费记录
psql -d taiji -c "
SELECT agent_name, is_platform_agent, start_time, end_time, cost 
FROM agent_billing_records 
WHERE is_platform_agent = true 
ORDER BY created_at DESC LIMIT 5;
"

# 3. 停止Agent并验证计费结束
curl -X DELETE http://localhost:8002/api/user/platform-agents/gpt-assistant \
  -H "Authorization: Bearer $TENANT_TOKEN"

# 4. 再次检查数据库,确认end_time已更新

测试4: LiteLLM Webhook计费

# 1. 配置LiteLLM Callback
# 修改 services/model-gateway/config/litellm.yaml

# 2. 重启LiteLLM Gateway
docker-compose restart litellm-gateway

# 3. 模拟模型调用(使用生产LiteLLM地址)
curl -X POST https://litellm.graystone-fb459c5d.southeastasia.azurecontainerapps.io/v1/chat/completions \
  -H "Authorization: Bearer sk-taiji-prod-2026" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

# 4. 检查Webhook是否被调用
curl http://localhost:8002/api/v1/billing/litellm-callback/health

# 5. 验证模型计费记录
psql -d taiji -c "
SELECT model_name, input_tokens, output_tokens, total_cost, eu_consumed 
FROM model_billing_records 
ORDER BY created_at DESC LIMIT 5;
"

测试5: 完整计费流程测试

# 1. 创建Agent(容器计费)+ 调用模型(Token计费)
# 2. 检查两种计费记录都已创建
# 3. 验证Dashboard显示合并后的EU消耗
curl http://localhost:8002/api/v1/monitoring/dashboard \
  -H "Authorization: Bearer $TENANT_TOKEN" | jq '.euConsumption24h'
-- 检查未结束的计费记录
SELECT COUNT(*) as active_records 
FROM agent_billing_records 
WHERE end_time IS NULL;

-- 检查EU计算是否正确
SELECT 
    agent_name,
    duration_seconds,
    eu_consumed,
    (duration_seconds / 10.0) as expected_eu
FROM agent_billing_records 
WHERE end_time IS NOT NULL 
AND ABS(eu_consumed - CEIL(duration_seconds / 10.0)) > 0;

-- 检查成本计算
SELECT 
    agent_name,
    agent_type,
    duration_seconds,
    cost,
    is_platform_agent
FROM agent_billing_records 
WHERE cost = 0 AND end_time IS NOT NULL;

⚠️ 重要注意事项

数据库修改注意事项

  1. 备份数据库: 修改前务必备份

    pg_dump taiji > backup_$(date +%Y%m%d_%H%M%S).sql
    
  2. 逐步部署: 建议先在测试环境验证

  3. 监控日志: 修改后密切监控应用日志

    tail -f logs/app.log | grep -E "计费|billing|EU"
    

代码修改注意事项

  1. 导入检查: 确保所有必要的模块都已导入
  2. 异常处理: 添加适当的try-catch块
  3. 日志记录: 添加详细的日志便于调试
  4. 类型检查: 注意UUID和字符串的类型转换

性能考虑

  1. 数据库索引: 确保agent_billing_records表有适当索引

    CREATE INDEX IF NOT EXISTS idx_agent_billing_user_time 
    ON agent_billing_records(user_id, start_time);
    
  2. 查询优化: 对于大量数据,考虑分页查询


🚀 部署步骤

步骤1: 准备工作

# 1. 进入项目目录
cd /home/taiji/tools/taiji-AI-PAD

# 2. 备份数据库
docker exec -it taiji-postgres pg_dump -U postgres taiji > backup.sql

# 3. 创建功能分支
git checkout -b fix/billing-system-with-litellm

步骤2: 数据库迁移

# 1. 执行模型计费表迁移
psql -d taiji -f services/mcp-server/migrations/012_add_model_billing_records.sql

# 2. 验证表创建成功
psql -d taiji -c "\d model_billing_records"

步骤3: 构建和部署代码

# 1. 构建mcp-server镜像(包含所有代码修改)
docker compose build mcp-server

# 2. 启动服务
docker-compose up -d --build

# 3. 查看日志确认启动成功
docker logs -f mcp-server | grep -E "billing|startup"

2. 执行数据库迁移

docker exec -i taiji-postgres psql -U postgres taiji < services/mcp-server/migrations/012_add_model_billing_records.sql

3. 测试LiteLLM Webhook

curl http://localhost:8002/api/v1/billing/litellm-callback/health

4. 运行完整测试套件

python test_billing_system.py



## 📋 完成检查清单

### 核心修复 ✅
- [ ] ✅ 修复`monitoring.py`中的EU查询逻辑
- [ ] ✅ 修复`platform_agent_quota.py`中的计费记录创建
- [ ] ✅ 添加平台Agent停止时的计费结束逻辑  
- [ ] ✅ 确保`billing.py`包含必要的计费函数

### LiteLLM Token计费 🚨 **最重要**
- [ ] 🗃️ 创建`model_billing_records`表迁移
- [ ] 📊 添加`ModelBillingRecord`模型到`models.py`
- [ ] 🔗 创建`billing_webhook.py`路由文件
- [ ] 📝 在`main.py`中注册webhook路由
- [ ] ⚙️ 配置`litellm.yaml`的success_callback
- [ ] 🌐 **配置LiteLLM生产环境地址和密钥**
- [ ] 🔌 **验证网络连通性**(LiteLLM→mcp-server:8002)
- [ ] 🔄 修改Dashboard查询合并两种计费数据

### 测试验证 🧪
- [ ] 🧪 通过Dashboard EU显示测试
- [ ] 🧪 通过平台Agent计费测试
- [ ] 🧪 通过LiteLLM Webhook计费测试
- [ ] 🧪 通过数据库完整性检查
- [ ] 🧪 通过完整计费流程测试

---


---

| 文件 | 路径 | 说明 |
|------|------|------|
| 数据库迁移 | `services/mcp-server/migrations/012_add_model_billing_records.sql` | **新建**模型Token计费表 |
| 数据模型 | `services/mcp-server/models.py` | 添加ModelBillingRecord模型 |
| Webhook路由 | `services/mcp-server/app/routes/billing_webhook.py` | **新建**LiteLLM回调接口 |
| 主程序 | `services/mcp-server/main.py` | 注册webhook路由 |
| Dashboard查询 | `services/mcp-server/monitoring.py` | EU消耗统计查询(合并两种计费) |
| 平台Agent管理 | `services/mcp-server/app/routes/platform_agent_quota.py` | 分配和停止逻辑 |
| 计费逻辑 | `services/mcp-server/app/billing.py` | 成本和EU计算 |
| LiteLLM配置 | `services/model-gateway/config/litellm.yaml` | **关键**配置success_callback |
| 用户API | `services/mcp-server/app/routes/user.py` | 自定义Agent管理 |

---

**最后更新**: 2026-01-09  
**负责人**: 系统管理员  
**状态**: 待实施 ⏳