# 账单模块设计方案 ## 一、现状分析 ### 1.1 现有数据表结构 #### 任务记录表(含bill字段) | 模块 | 表名 | 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 | 阶梯价格(用于视频按分辨率计费) | ### 1.2 现有服务层 - `billing_service.py` - 仅支持ai_conversation和ai_picture的账单查询 - `billing_calculator.py` - 通用计费计算器 - `image_billing.py` - 图片计费 - `video_billing.py` - 视频计费 - `audio_billing.py` - 音频计费(新增) ### 1.3 现有问题 1. **账单服务不完整**:billing_service.py仅查询ai_conversation和ai_picture,缺少视频和音频模块 2. **前端使用Mock数据**:Billing.tsx使用硬编码的假数据 3. **余额扣减未实现**:各模块计算了bill但未从用户余额扣除 4. **每日汇总不完整**:sync_daily_consumption仅处理对话和图片模块 --- ## 二、设计目标 ### 2.1 功能需求 1. **统一账单查询**:支持查询所有模块(文本/图片/视频/音频)的消费记录 2. **余额管理**:任务完成时自动扣减用户余额,余额不足时拒绝任务 3. **账单统计**: - 当前余额 - 累计消费(总计 + 按模块) - 累计充值 - 当月消费 - 按日期/模块分组统计 4. **账单明细**:支持按时间、模块、模型筛选,分页查询 5. **导出功能**:支持导出Excel ### 2.2 非功能需求 1. **性能**:大数据量下的分页查询优化 2. **一致性**:余额扣减的事务一致性 3. **可扩展**:便于新增模块 --- ## 三、技术设计 ### 3.1 模块枚举定义 ```python 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": "语音", } ``` ### 3.2 统一账单记录Schema ```python 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 # 详细信息(各模块特有字段) ``` ### 3.3 API设计 #### 3.3.1 获取账单汇总 ``` 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 }, ... ] } ``` #### 3.3.2 获取账单列表 ``` 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 // 筛选结果的总金额 } ``` #### 3.3.3 获取账单详情 ``` GET /api/billing/records/{module}/{record_id} Response: BillRecord (含完整details) ``` #### 3.3.4 导出账单 ``` GET /api/billing/export Query Parameters: 同账单列表 Response: Excel文件流 ``` #### 3.3.5 获取可用模型列表(用于筛选) ``` GET /api/billing/models Response: { "models": [ {"name": "qwen-max", "display": "通义千问Max", "category": "文本"}, {"name": "flux-schnell", "display": "Flux Schnell", "category": "图像"}, ... ] } ``` ### 3.4 余额扣减机制 #### 3.4.1 扣减时机 - **同步任务**(TTS、同步ASR、图片生成):任务完成后立即扣减 - **异步任务**(视频生成、异步ASR):任务状态变为SUCCEEDED时扣减 - **对话**:每轮对话完成后扣减 #### 3.4.2 余额检查 在任务开始前进行余额预检查(可选,防止余额不足时浪费API调用): ```python 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 ``` #### 3.4.3 扣减服务 ```python 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 ``` ### 3.5 数据查询优化 #### 3.5.1 联合查询策略 由于账单数据分布在多个表中,采用以下策略: **方案A:应用层合并(当前方案)** - 分别查询各表,在应用层合并排序 - 优点:简单,无需修改表结构 - 缺点:分页不精确,大数据量时性能差 **方案B:创建账单视图(推荐)** ```sql 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; ``` #### 3.5.2 索引优化 确保各表的以下字段有索引: - `user_id` - `created_at` - `bill`(用于汇总计算) ### 3.6 前端对接 #### 3.6.1 替换Mock数据 将Billing.tsx中的硬编码数据替换为API调用: ```typescript // 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'), }; ``` #### 3.6.2 筛选器增强 - 添加分类筛选(文本/图像/视频/语音) - 模型下拉列表从API获取 - 支持多选筛选 --- ## 四、实现计划 ### Phase 1: 后端账单查询完善 1. 扩展BillingService,支持所有模块的账单查询 2. 创建账单视图(可选) 3. 实现统一的BillRecord转换逻辑 ### Phase 2: 余额扣减机制 1. 创建BalanceService 2. 在各模块服务中集成余额扣减 3. 添加余额不足的错误处理 ### Phase 3: API路由完善 1. 完善/api/billing/下的所有端点 2. 添加导出功能 3. 添加模型列表接口 ### Phase 4: 前端对接 1. 创建billingApi服务 2. 替换Billing.tsx中的Mock数据 3. 完善筛选和详情展示 ### Phase 5: 每日汇总同步 1. 扩展sync_daily_consumption支持所有模块 2. 添加定时任务或触发器 --- ## 五、注意事项 1. **事务一致性**:余额扣减必须与任务记录更新在同一事务中 2. **并发控制**:使用`SELECT FOR UPDATE`防止余额超扣 3. **精度问题**:金额使用Decimal类型,保留4位小数 4. **向后兼容**:现有API保持兼容,新增接口扩展功能 5. **错误处理**:余额不足时返回明确的错误码和提示 --- ## 六、数据流图 ``` 用户发起任务 │ ▼ ┌─────────────────┐ │ 余额预检查 │ ──── 余额不足 ──→ 返回错误 │ (可选) │ └────────┬────────┘ │ 余额充足 ▼ ┌─────────────────┐ │ 执行任务 │ │ (调用AI API) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 计算费用 │ │ (XxxBilling) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 保存任务记录 │ │ (含bill字段) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 扣减余额 │ │ (BalanceService)│ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 更新每日汇总 │ │ (可选/异步) │ └─────────────────┘ ``` --- ## 七、待确认事项 1. **余额预检查**:是否在任务开始前检查余额?(可能导致预估不准确) 2. **余额不足处理**: - 方案A:拒绝任务,返回错误 - 方案B:允许任务执行,余额变为负数(需要后续充值) 3. **账单视图**:是否创建数据库视图优化查询? 4. **导出格式**:除Excel外是否需要支持CSV? 5. **账单详情**:各模块需要展示哪些特有字段?