# 计费管理三维度接口文档 ## 接口信息 **接口路径**: `GET /api/admin/billing/overview` **功能**: 获取计费管理三维度统计数据(渠道维度、租户维度、调用记录) **认证**: 需要 Bearer Token **权限**: 管理员角色(super_admin、billing_admin、operations_admin) --- ## 请求参数 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | startTime | string | ✅ | 开始时间,ISO 8601格式,如 `2026-03-01T00:00:00Z` | | endTime | string | ✅ | 结束时间,ISO 8601格式,如 `2026-03-31T23:59:59Z` | | channelName | string | ❌ | 渠道名称筛选 | | tenantName | string | ❌ | 租户名称筛选 | | minCalls | int | ❌ | 最小调用次数筛选 | | maxCalls | int | ❌ | 最大调用次数筛选 | | export | string | ❌ | 导出格式:`excel`、`csv`、`pdf` | --- ## 响应数据结构 ```json { "success": true, "data": { "channelStats": [...], "tenantStats": [...], "callRecords": [...] } } ``` --- ## 一、渠道维度 (channelStats) ### 数据结构 ```json { "channelStats": [ { "channelId": "550e8400-e29b-41d4-a716-446655440001", "channelName": "渠道A", "calls": 1500, "totalEU": 7500.0, "totalCost": 750.00 }, { "channelId": "550e8400-e29b-41d4-a716-446655440002", "channelName": "渠道B", "calls": 800, "totalEU": 4000.0, "totalCost": 400.00 } ] } ``` ### 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | channelId | string | 渠道ID(UUID) | | channelName | string | 渠道名称 | | calls | int | 调用次数 | | totalEU | float | 总EU消耗 | | totalCost | float | 渠道总价(USD) | ### 前端汇总计算 ```javascript // 渠道总数 const channelCount = channelStats.length; // 总计费额 const totalCost = channelStats.reduce((sum, c) => sum + c.totalCost, 0); // 总EU消耗 const totalEU = channelStats.reduce((sum, c) => sum + c.totalEU, 0); ``` --- ## 二、租户维度 (tenantStats) ### 数据结构 ```json { "tenantStats": [ { "tenantId": "b0d02105-55f8-41a7-b09a-bba8f49a9d62", "tenantName": "xiaohei", "channelName": "66", "calls": 84, "totalEU": 44197.14, "totalCost": 12.2905, "avgCost": 0.1463 }, { "tenantId": "6b49508d-9c2c-4eac-99e1-c36263fd1699", "tenantName": "cccccccc", "channelName": "66", "calls": 2, "totalEU": 18.0, "totalCost": 0.0046, "avgCost": 0.0023 } ] } ``` ### 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | tenantId | string | 租户ID(UUID) | | tenantName | string | 租户名称 | | channelName | string | 所属渠道名称(无渠道时显示"无渠道") | | calls | int | 调用次数(Agent使用 + 模型调用) | | totalEU | float | 总EU消耗 | | totalCost | float | 用户总价(USD) | | avgCost | float | 平均消费(totalCost / calls) | ### 前端汇总计算 ```javascript // 租户总数 const tenantCount = tenantStats.length; // 用户总价 const userTotalCost = tenantStats.reduce((sum, t) => sum + t.totalCost, 0); // 平均消费 const avgCost = tenantCount > 0 ? userTotalCost / tenantCount : 0; ``` --- ## 三、调用记录 (callRecords) 调用记录包含两种类型: - **agent**: Agent 使用记录(来自 AgentBillingRecord 表) - **model**: 模型调用记录(来自 ModelBillingRecord 表,LiteLLM 回调数据) ### 数据结构 ```json { "callRecords": [ { "id": "7d984099-534f-4b8b-a892-74dead3fd5fd", "type": "agent", "timestamp": "2026-03-09T07:05:41.118384", "channelName": "无渠道", "tenantName": "xiaohei", "agentName": "search-agent-b0d02105-21015e", "modelName": "taiji/claude-sonnet-4-5", "duration": 83430, "eu": 8343, "cost": 2.3175 }, { "id": "d69e4da8-0852-4e35-94dc-ee2bd584e9ca", "type": "model", "timestamp": "2026-03-05T16:02:47.186423", "channelName": "66", "tenantName": "xiaohei", "agentName": null, "modelName": "openrouter/nousresearch/hermes-3-llama-3.1-405b", "duration": 44.76, "eu": 0.3862, "cost": 0.003862, "inputTokens": 2932, "outputTokens": 930, "totalTokens": 3862 } ] } ``` ### 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | id | string | 调用ID(UUID) | | type | string | 记录类型:`agent`(Agent使用)或 `model`(模型调用) | | timestamp | string | 时间戳(ISO 8601格式) | | channelName | string | 渠道名称(无渠道时显示"无渠道") | | tenantName | string | 租户名称 | | agentName | string | Agent名称(模型调用时为 null) | | modelName | string | 模型名称(如 `taiji/claude-sonnet-4-5`) | | duration | float | 时长(秒)- Agent为运行时长,模型为响应时间 | | eu | float | EU消耗 | | cost | float | 单次调用总价(USD) | | inputTokens | int | 输入Token数(仅 type=model 时有值) | | outputTokens | int | 输出Token数(仅 type=model 时有值) | | totalTokens | int | 总Token数(仅 type=model 时有值) | ### EU计算规则 **Agent 使用(type=agent):** ``` 1 EU = 10秒运行时间 不足10秒按1 EU计算 公式:EU = ceil(duration / 10) 示例: - 5秒 → 1 EU - 10秒 → 1 EU - 15秒 → 2 EU - 35秒 → 4 EU ``` **模型调用(type=model):** ``` EU 按 Token 数量计算 公式由 LiteLLM 回调提供 ``` --- ## 前端调用示例 ### 请求示例 ```javascript const response = await fetch( '/api/admin/billing/overview?' + new URLSearchParams({ startTime: '2026-03-01T00:00:00Z', endTime: '2026-03-31T23:59:59Z' }), { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); const data = await response.json(); ``` ### 数据处理示例 ```javascript if (data.success) { const { channelStats, tenantStats, callRecords } = data.data; // ========== 渠道维度汇总 ========== const channelSummary = { totalChannels: channelStats.length, totalCost: channelStats.reduce((sum, c) => sum + c.totalCost, 0), totalEU: channelStats.reduce((sum, c) => sum + c.totalEU, 0) }; // ========== 租户维度汇总 ========== const tenantSummary = { totalTenants: tenantStats.length, userTotalCost: tenantStats.reduce((sum, t) => sum + t.totalCost, 0), avgCost: tenantStats.length > 0 ? tenantStats.reduce((sum, t) => sum + t.totalCost, 0) / tenantStats.length : 0 }; // ========== 渠道计费详情表格数据 ========== const channelTableData = channelStats.map(c => ({ 渠道名称: c.channelName, 调用次数: c.calls, 总EU: c.totalEU, 渠道总价: `$${c.totalCost.toFixed(2)}` })); // ========== 租户计费详情表格数据 ========== const tenantTableData = tenantStats.map(t => ({ 租户名称: t.tenantName, 所属渠道: t.channelName, 调用次数: t.calls, 总EU: t.totalEU, 用户总价: `$${t.totalCost.toFixed(2)}` })); // ========== 调用记录明细表格数据 ========== const callTableData = callRecords.map(r => ({ 调用ID: r.id, 租户: r.tenantName, 渠道: r.channelName, 调用时间: r.agentName, '时长(秒)': r.duration, EU: r.eu, 单次调用总价: `$${r.cost.toFixed(2)}`, 时间戳: r.timestamp })); } ``` --- ## 前端页面字段映射 ### 渠道维度卡片 | 前端显示 | 数据来源 | |---------|---------| | 渠道总数 | `channelStats.length` | | 总计费额 | `channelStats.reduce((sum, c) => sum + c.totalCost, 0)` | | 总EU消耗 | `channelStats.reduce((sum, c) => sum + c.totalEU, 0)` | ### 渠道计费详情表格 | 表头 | 字段 | |------|------| | 渠道名称 | `channelName` | | 调用次数 | `calls` | | 总EU | `totalEU` | | 渠道总价 | `totalCost` | ### 租户维度卡片 | 前端显示 | 数据来源 | |---------|---------| | 租户总数 | `tenantStats.length` | | 用户总价 | `tenantStats.reduce((sum, t) => sum + t.totalCost, 0)` | | 平均消费 | `用户总价 / 租户总数` | ### 租户计费详情表格 | 表头 | 字段 | |------|------| | 租户名称 | `tenantName` | | 所属渠道 | `channelName` | | 调用次数 | `calls` | | 总EU | `totalEU` | | 用户总价 | `totalCost` | ### 调用记录明细表格 | 表头 | 字段 | |------|------| | 调用ID | `id` | | 类型 | `type`(agent/model) | | 租户 | `tenantName` | | 渠道 | `channelName` | | Agent名称 | `agentName` | | 模型名称 | `modelName` | | 时长(秒) | `duration` | | EU | `eu` | | 单次调用总价 | `cost` | | 输入Token | `inputTokens`(仅模型调用) | | 输出Token | `outputTokens`(仅模型调用) | | 总Token | `totalTokens`(仅模型调用) | | 时间戳 | `timestamp` | --- ## 数据来源说明 本接口从以下两个表聚合数据: | 表名 | 说明 | 对应 type | |------|------|----------| | AgentBillingRecord | Agent 使用计费记录 | `agent` | | ModelBillingRecord | 模型调用计费记录(LiteLLM 回调) | `model` | --- ## 注意事项 1. **时间格式**: 请求参数中的时间需要使用 ISO 8601 格式 2. **金额单位**: 所有金额字段单位为 USD(美元) 3. **调用记录限制**: 默认返回最近100条记录(Agent 50条 + 模型 50条),按时间倒序排列 4. **EU计算**: Agent 使用按 1 EU = 10秒计算;模型调用按 Token 数量计算 5. **渠道名称**: 如果租户未关联渠道,显示"无渠道" 6. **记录类型**: 通过 `type` 字段区分 Agent 使用记录和模型调用记录