You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

9.7 KiB

RFC 138: 动态定价集成

状态: 已完成
创建日期: 2026-01-27
作者: System
类型: 功能增强

概述

将 Credit Service 的定价计算与 AI Service 集成,优先使用 ai_models 表的动态定价,并实现 Redis 缓存和降级策略。

背景

当前 calculate_credits() 方法使用 credit_pricing 表的静态定价规则:

  1. 维护成本高: 需要手动维护两套定价数据(credit_pricingai_models
  2. 数据不一致: 两个表的定价可能不同步
  3. 扩展性差: 添加新模型需要同时更新两个表
  4. 缺少缓存: 每次计算都查询数据库

问题

  1. AI Service 的 ai_models 表已包含 credits_per_unit 字段,但未被使用
  2. Credit Service 和 AI Service 的定价逻辑分离
  3. 没有缓存机制,性能不佳
  4. 缺少降级策略,AI Service 不可用时无法计算

解决方案

设计原则

  1. 优先级: AI Service > credit_pricing 表
  2. 缓存优化: 使用 Redis 缓存模型定价(TTL 1 小时)
  3. 降级策略: AI Service 不可用时使用 credit_pricing 表
  4. 向后兼容: 保留原有定价逻辑作为降级方案

实现细节

1. Service 层 - 注入 AI Model Repository

# app/services/credit_service.py

class CreditService:
    def __init__(self, session: AsyncSession):
        self.repository = CreditRepository(session)
        self.session = session
        # AI Model Repository 用于动态定价(延迟导入避免循环依赖)
        self._ai_model_repository = None
    
    @property
    def ai_model_repository(self):
        """延迟加载 AI Model Repository"""
        if self._ai_model_repository is None:
            from app.repositories.ai_model_repository import AIModelRepository
            self._ai_model_repository = AIModelRepository(self.session)
        return self._ai_model_repository

2. 从 AI Service 获取定价(带缓存)

async def _get_model_pricing_from_ai_service(
    self,
    feature_type: int,
    model_name: Optional[str] = None
) -> Optional[Dict[str, Any]]:
    """从 AI Service 获取模型定价(带 Redis 缓存)"""
    try:
        from app.core.cache import cache
        from app.models.ai_model import AIModelType
        
        # 映射 feature_type 到 model_type
        feature_to_model_type = {
            FeatureType.IMAGE_GENERATION: AIModelType.IMAGE,
            FeatureType.VIDEO_GENERATION: AIModelType.VIDEO,
            FeatureType.TEXT_PROCESSING: AIModelType.TEXT,
            FeatureType.AUDIO_GENERATION: AIModelType.AUDIO,
            FeatureType.SOUND_GENERATION: AIModelType.AUDIO,
            FeatureType.VOICE_GENERATION: AIModelType.AUDIO,
            FeatureType.SUBTITLE_GENERATION: AIModelType.AUDIO,
        }
        
        model_type = feature_to_model_type.get(feature_type)
        if not model_type:
            return None
        
        # 尝试从缓存获取
        cache_key = f"ai_model:pricing:{model_name or f'default_{model_type}'}"
        cached = await cache.get(cache_key)
        if cached:
            import json
            return json.loads(cached)
        
        # 从数据库查询
        if model_name:
            model = await self.ai_model_repository.get_by_name(model_name)
        else:
            model = await self.ai_model_repository.get_default_model(model_type)
        
        if not model or not model.is_active:
            return None
        
        pricing_info = {
            'model_id': model.model_id,
            'model_name': model.model_name,
            'credits_per_unit': model.credits_per_unit,
            'unit_type': model.unit_type,
            'cost_per_unit': float(model.cost_per_unit)
        }
        
        # 缓存 1 小时
        await cache.set(cache_key, pricing_info, ttl=3600)
        
        return pricing_info
        
    except Exception as e:
        logger.error(f"从 AI Service 获取定价失败: {e}")
        return None

3. 重构 calculate_credits 方法

async def calculate_credits(
    self,
    feature_type: int,
    params: Dict[str, Any]
) -> int:
    """计算所需积分(优先使用 AI Service 定价,降级到 credit_pricing 表)"""
    self._validate_feature_type(feature_type)

    # 尝试从 AI Service 获取定价
    model_name = params.get('model')
    ai_pricing = await self._get_model_pricing_from_ai_service(feature_type, model_name)
    
    if ai_pricing:
        # 使用 AI Service 的定价
        credits_per_unit = ai_pricing['credits_per_unit']
        
        # 根据功能类型计算单位数
        if feature_type == FeatureType.IMAGE_GENERATION:
            return credits_per_unit
        
        elif feature_type == FeatureType.VIDEO_GENERATION:
            duration = params.get('duration', 5)
            return credits_per_unit * duration
        
        elif feature_type == FeatureType.TEXT_PROCESSING:
            char_count = params.get('char_count', 0)
            units = max(1, (char_count // 1000))
            return credits_per_unit * units
        
        # ... 其他类型
    
    # 降级:使用 credit_pricing 表(原有逻辑)
    pricing = await self.repository.get_pricing(feature_type)
    if not pricing:
        raise NotFoundError(f"未找到定价配置")
    
    # 原有的复杂定价逻辑...

架构图

┌─────────────────┐
│  Credit Service │
└────────┬────────┘
         │
         ├─────────────────────────────────┐
         │                                 │
         ▼                                 ▼
┌────────────────────┐          ┌──────────────────┐
│ AI Model Repository│          │ Credit Repository│
│ (ai_models 表)     │          │ (credit_pricing) │
└─────────┬──────────┘          └──────────────────┘
          │                              ▲
          │                              │
          ▼                              │
    ┌──────────┐                        │
    │  Redis   │                        │
    │  缓存    │                        │
    └──────────┘                        │
          │                              │
          │ 不可用时                     │
          └──────────────────────────────┘
                   降级策略

优势

  1. 单一数据源: AI Service 的 ai_models 表作为主要定价来源
  2. 性能优化: Redis 缓存减少数据库查询(缓存命中率预计 >90%)
  3. 高可用性: 降级策略保证 AI Service 不可用时仍可计算
  4. 易于维护: 只需维护 ai_models 表,credit_pricing 作为备份
  5. 向后兼容: 保留原有逻辑,平滑迁移

性能对比

场景 原方案 新方案 提升
首次查询 1 次 DB 查询 1 次 DB 查询 + 写缓存 持平
缓存命中 1 次 DB 查询 0 次 DB 查询 100%
AI Service 不可用 N/A 降级到 credit_pricing 100% 可用性

风险与缓解

风险 影响 缓解措施
AI Service 不可用 无法获取定价 降级到 credit_pricing 表
Redis 不可用 性能下降 直接查询数据库
缓存数据过期 定价不准确 TTL 设置为 1 小时,定期刷新
循环依赖 导入失败 使用延迟导入和 @property

实施步骤

  1. CreditService 添加 ai_model_repository 属性
  2. 实现 _get_model_pricing_from_ai_service() 方法
  3. 重构 calculate_credits() 方法
  4. 添加 feature_type 到 model_type 的映射
  5. 测试缓存机制
  6. 测试降级策略
  7. 性能测试和优化

测试策略

单元测试

async def test_calculate_credits_with_ai_service():
    """测试使用 AI Service 定价"""
    # Mock AI Model Repository
    # 调用 calculate_credits
    # 验证使用了 ai_models 表的定价

async def test_calculate_credits_fallback():
    """测试降级到 credit_pricing 表"""
    # Mock AI Service 不可用
    # 调用 calculate_credits
    # 验证使用了 credit_pricing 表的定价

async def test_pricing_cache():
    """测试 Redis 缓存"""
    # 第一次查询(写缓存)
    # 第二次查询(读缓存)
    # 验证缓存命中

集成测试

async def test_end_to_end_pricing():
    """端到端测试定价计算"""
    # 创建 AI 模型记录
    # 调用 calculate_credits
    # 验证返回正确的积分数

监控指标

  1. 缓存命中率: Redis 缓存的命中率
  2. 降级次数: 使用 credit_pricing 表的次数
  3. 查询耗时: 定价计算的平均耗时
  4. 错误率: 定价计算失败的比例

迁移计划

阶段 1:并行运行(1 周)

  • 新旧逻辑同时运行
  • 对比结果差异
  • 记录日志分析

阶段 2:灰度发布(1 周)

  • 10% 流量使用新逻辑
  • 监控性能和错误率
  • 逐步提升到 100%

阶段 3:完全切换(1 周)

  • 100% 流量使用新逻辑
  • 保留降级策略
  • 持续监控

相关文档

变更日志

  • 2026-01-27: 初始版本,实现动态定价集成