| 模块 | 表名 | bill字段 | 计费单位 | 计费依据 |
|---|---|---|---|---|
| 文本对话 | ai_conversation | ✅ | tokens | input_tokens + output_tokens |
| 图片生成 | ai_picture | ✅ | 张 | image_count |
| 视频生成 | ai_video | ✅ | 秒 | video_duration |
| 语音合成(TTS) | audio_synthesis | ✅ | 字符 | characters |
| 同步语音识别 | asr_recognition | ✅ | 秒 | duration |
| 异步语音识别 | asr_task | ✅ | 秒 | duration |
| 声音复刻 | voice_clone | ✅ | 次 | 固定费用 |
| 表名 | 用途 |
|---|---|
| users | 用户信息,含balance余额字段 |
| user_recharge_record | 充值记录 |
| user_consumption_daily | 每日消费汇总(按模块) |
| 表名 | 用途 |
|---|---|
| models | 模型信息,关联price_id |
| model_price | 模型价格(input_price, output_price, unit, pricing_mode) |
| model_price_tier | 阶梯价格(用于视频按分辨率计费) |
billing_service.py - 仅支持ai_conversation和ai_picture的账单查询billing_calculator.py - 通用计费计算器image_billing.py - 图片计费video_billing.py - 视频计费audio_billing.py - 音频计费(新增)class BillModule(str, Enum):
"""账单模块枚举"""
CONVERSATION = "ai_conversation" # 文本对话
PICTURE = "ai_picture" # 图片生成
VIDEO = "ai_video" # 视频生成
TTS = "audio_synthesis" # 语音合成
ASR_SYNC = "asr_recognition" # 同步语音识别
ASR_ASYNC = "asr_task" # 异步语音识别
VOICE_CLONE = "voice_clone" # 声音复刻
# 模块显示名称
MODULE_DISPLAY_MAP = {
"ai_conversation": "AI对话",
"ai_picture": "AI生图",
"ai_video": "AI视频",
"audio_synthesis": "语音合成",
"asr_recognition": "语音识别",
"asr_task": "录音转写",
"voice_clone": "声音复刻",
}
# 模块分类
MODULE_CATEGORY_MAP = {
"ai_conversation": "文本",
"ai_picture": "图像",
"ai_video": "视频",
"audio_synthesis": "语音",
"asr_recognition": "语音",
"asr_task": "语音",
"voice_clone": "语音",
}
class BillRecord(BaseModel):
"""统一账单记录"""
id: int
module: str # 模块标识
module_display: str # 模块显示名
category: str # 分类(文本/图像/视频/语音)
model_name: str # 模型名称
description: str # 描述
bill: Decimal # 费用(元)
original_usage: float # 原始用量
usage_unit: str # 用量单位(tokens/张/秒/字符/次)
created_at: datetime # 创建时间
details: Optional[dict] = None # 详细信息(各模块特有字段)
GET /api/billing/summary
Response:
{
"current_balance": 1250.00, // 当前余额
"total_spent": 3450.00, // 累计消费
"monthly_spent": 450.00, // 当月消费
"total_recharged": 4700.00, // 累计充值
"module_stats": [ // 各模块统计
{
"module": "ai_conversation",
"module_display": "AI对话",
"category": "文本",
"total_amount": 1200.00,
"count": 150
},
...
]
}
GET /api/billing/records
Query Parameters:
- module: string (可选,模块筛选)
- category: string (可选,分类筛选:文本/图像/视频/语音)
- model_name: string (可选,模型名称筛选)
- start_date: date (可选)
- end_date: date (可选)
- page: int (默认1)
- page_size: int (默认10)
Response:
{
"records": [BillRecord],
"total": 100,
"page": 1,
"page_size": 10,
"total_pages": 10,
"total_amount": 500.00 // 筛选结果的总金额
}
GET /api/billing/records/{module}/{record_id}
Response: BillRecord (含完整details)
GET /api/billing/export
Query Parameters: 同账单列表
Response: Excel文件流
GET /api/billing/models
Response:
{
"models": [
{"name": "qwen-max", "display": "通义千问Max", "category": "文本"},
{"name": "flux-schnell", "display": "Flux Schnell", "category": "图像"},
...
]
}
在任务开始前进行余额预检查(可选,防止余额不足时浪费API调用):
async def check_balance(user_id: str, estimated_cost: Decimal) -> bool:
"""检查余额是否足够"""
user = db.query(User).filter(User.id == user_id).first()
return user.balance >= estimated_cost
class BalanceService:
"""余额管理服务"""
def deduct_balance(
self,
user_id: str,
amount: Decimal,
module: str,
record_id: int,
description: str
) -> bool:
"""
扣减用户余额
使用数据库事务确保一致性:
1. 检查余额是否足够
2. 扣减余额
3. 更新任务记录的bill字段
4. 更新每日消费汇总
"""
with self.db.begin():
user = self.db.query(User).filter(
User.id == user_id
).with_for_update().first()
if user.balance < amount:
return False
user.balance -= amount
self._update_daily_consumption(user_id, module, amount)
return True
由于账单数据分布在多个表中,采用以下策略:
方案A:应用层合并(当前方案)
方案B:创建账单视图(推荐)
CREATE VIEW aigcspace.v_bill_records AS
SELECT
id, 'ai_conversation' as module, model_name, bill,
total_tokens as original_usage, 'tokens' as usage_unit,
created_at, user_id
FROM aigcspace.ai_conversation
UNION ALL
SELECT
id, 'ai_picture' as module, model_name, bill,
image_count as original_usage, '张' as usage_unit,
created_at, user_id
FROM aigcspace.ai_picture
UNION ALL
SELECT
id, 'ai_video' as module, model_name, bill,
video_duration as original_usage, '秒' as usage_unit,
created_at, user_id
FROM aigcspace.ai_video
UNION ALL
SELECT
id, 'audio_synthesis' as module, model as model_name, bill,
characters as original_usage, '字符' as usage_unit,
created_at, user_id
FROM aigcspace.audio_synthesis
UNION ALL
SELECT
id, 'asr_recognition' as module, model as model_name, bill,
duration as original_usage, '秒' as usage_unit,
created_at, user_id
FROM aigcspace.asr_recognition
UNION ALL
SELECT
id, 'asr_task' as module, model as model_name, bill,
duration as original_usage, '秒' as usage_unit,
created_at, user_id
FROM aigcspace.asr_task
UNION ALL
SELECT
id, 'voice_clone' as module, target_model as model_name, bill,
1 as original_usage, '次' as usage_unit,
created_at, user_id
FROM aigcspace.voice_clone;
确保各表的以下字段有索引:
user_idcreated_atbill(用于汇总计算)将Billing.tsx中的硬编码数据替换为API调用:
// services/billingApi.ts
export const billingApi = {
getSummary: () => api.get('/billing/summary'),
getRecords: (params) => api.get('/billing/records', { params }),
getRecordDetail: (module, id) => api.get(`/billing/records/${module}/${id}`),
exportRecords: (params) => api.get('/billing/export', { params, responseType: 'blob' }),
getModels: () => api.get('/billing/models'),
};
SELECT FOR UPDATE防止余额超扣用户发起任务
│
▼
┌─────────────────┐
│ 余额预检查 │ ──── 余额不足 ──→ 返回错误
│ (可选) │
└────────┬────────┘
│ 余额充足
▼
┌─────────────────┐
│ 执行任务 │
│ (调用AI API) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 计算费用 │
│ (XxxBilling) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 保存任务记录 │
│ (含bill字段) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 扣减余额 │
│ (BalanceService)│
└────────┬────────┘
│
▼
┌─────────────────┐
│ 更新每日汇总 │
│ (可选/异步) │
└─────────────────┘