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.
 

12 KiB

AI Service 与 Credit Service 集成

日期:2026-01-29
类型:Feature
影响范围:AI Service, Credit Service, Repositories

概述

完成 AI Service 与 Credit Service 的完整集成,实现积分预扣、确认、退还流程,并添加应用层引用完整性验证。

变更内容

1. Repository 层:添加 exists() 方法

为所有 Repository 添加 exists() 方法,用于应用层引用完整性验证。

1.1 AIJobRepository

async def exists(self, job_id: str) -> bool:
    """检查任务是否存在(应用层引用完整性保证)"""

1.2 AIModelRepository

async def exists(self, model_id: str) -> bool:
    """检查模型是否存在(应用层引用完整性保证)"""

1.3 AIQuotaRepository

async def exists(self, quota_id: str) -> bool:
    """检查配额是否存在(应用层引用完整性保证)"""

1.4 UserRepository

async def exists(self, user_id: UUID) -> bool:
    """检查用户是否存在(应用层引用完整性保证)"""

1.5 ProjectRepository

async def exists(self, project_id: str) -> bool:
    """检查项目是否存在(应用层引用完整性保证)"""

设计说明

  • 使用高效的 COUNT 查询
  • 返回布尔值,避免加载完整对象
  • 遵循统一的命名和实现模式

2. AIService 层:完整重构

2.1 注入 CreditService

def __init__(self, db: AsyncSession):
    self.db = db
    self.job_repository = AIJobRepository(db)
    self.model_repository = AIModelRepository(db)
    self.usage_log_repository = AIUsageLogRepository(db)
    self.quota_repository = AIQuotaRepository(db)
    self.user_repository = UserRepository(db)
    self.project_repository = ProjectRepository(db)
    self.credit_service = CreditService(db)  # ✅ 注入 Credit Service

2.2 应用层引用完整性验证

async def _validate_user_exists(self, user_id: str) -> None:
    """验证用户是否存在(应用层引用完整性保证)"""
    if not await self.user_repository.exists(UUID(user_id)):
        raise NotFoundError(f"用户不存在: {user_id}")

async def _validate_model_exists(self, model_id: str) -> None:
    """验证模型是否存在(应用层引用完整性保证)"""
    if not await self.model_repository.exists(model_id):
        raise NotFoundError(f"AI 模型不存在: {model_id}")

async def _validate_project_exists(self, project_id: Optional[str]) -> None:
    """验证项目是否存在(应用层引用完整性保证)"""
    if project_id and not await self.project_repository.exists(project_id):
        raise NotFoundError(f"项目不存在: {project_id}")

2.3 积分预扣流程(以图片生成为例)

async def generate_image(self, user_id: str, prompt: str, ...) -> Dict[str, Any]:
    # 1. 验证用户是否存在
    await self._validate_user_exists(user_id)
    
    # 2. 验证项目是否存在(可选)
    await self._validate_project_exists(project_id)
    
    # 3. 检查配额
    await self._check_quota(user_id, 'image_generation')
    
    # 4. 获取模型配置
    model_config = await self._get_model(model, AIModelType.IMAGE)
    
    # 5. 验证模型是否存在
    await self._validate_model_exists(str(model_config.model_id))
    
    # 6. 计算所需积分
    feature_type = self._get_feature_type_from_job_type(AIJobType.IMAGE)
    credits_needed = await self.credit_service.calculate_credits(
        feature_type=feature_type,
        params={'model': model_config.model_name, 'quality': kwargs.get('quality', 'sd')}
    )
    
    # 7. 使用事务确保原子性
    async with self.db.begin():
        # 预扣积分
        try:
            consumption_log = await self.credit_service.consume_credits(
                user_id=UUID(user_id),
                amount=credits_needed,
                feature_type=feature_type,
                task_params={...}
            )
        except InsufficientCreditsError as e:
            raise ValidationError(f"积分不足: {str(e)}")
        
        # 创建任务记录
        job = await self.job_repository.create({
            'consumption_log_id': str(consumption_log.consumption_id),
            ...
        })
        
        # 更新 consumption_log 的 ai_job_id
        consumption_log.ai_job_id = UUID(job.ai_job_id)
        consumption_log.task_id = str(job.ai_job_id)
        await self.db.flush()
    
    # 8. 提交异步任务
    task = generate_image_task.delay(...)
    
    # 9. 更新任务 ID
    await self.job_repository.update(job.ai_job_id, {'task_id': task.id})
    
    return {'job_id': job.ai_job_id, 'task_id': task.id, 'status': 'pending', ...}

2.4 任务取消与积分退还

async def cancel_job(self, user_id: str, job_id: str) -> None:
    """取消任务并退还积分"""
    job = await self.job_repository.get_by_id(job_id)
    
    # 验证权限
    if job.user_id != user_id:
        raise ValidationError("没有权限取消此任务")
    
    # 取消 Celery 任务
    if job.task_id:
        celery_app.control.revoke(job.task_id, terminate=True)
    
    # 更新任务状态
    await self.job_repository.cancel(job_id)
    
    # 退还积分
    if job.consumption_log_id:
        await self.credit_service.refund_credits(
            consumption_id=UUID(job.consumption_log_id),
            reason="用户取消任务"
        )

2.5 所有生成方法已集成

  • generate_image() - 图片生成
  • generate_video() - 视频生成
  • generate_sound() - 音效生成
  • generate_voice() - 配音生成
  • generate_subtitle() - 字幕生成
  • process_text() - 文本处理

3. 技术规范遵循

3.1 应用层引用完整性保证

  • 无物理外键约束
  • 使用 exists() 方法验证关联 ID
  • 在 Service 层验证所有关联关系
  • 验证失败抛出 NotFoundError

3.2 事务管理

  • 使用 async with self.db.begin() 确保原子性
  • 积分扣除和任务创建在同一事务中
  • 事务失败自动回滚

3.3 错误处理

  • InsufficientCreditsError - 积分不足
  • ValidationError - 参数验证失败
  • NotFoundError - 资源不存在
  • 详细的错误日志记录

3.4 日志记录

  • 使用统一的 logging 模块
  • 记录关键操作(任务创建、积分扣除、任务取消)
  • 记录错误和警告信息

积分流程

1. 预扣积分(任务创建时)

consumption_log = await credit_service.consume_credits(
    user_id=UUID(user_id),
    amount=credits_needed,
    feature_type=feature_type,
    task_params={...}
)

状态TaskStatus.PENDING

2. 确认消耗(任务成功时)

# 在 Celery Worker 中调用
await credit_service.confirm_consumption(
    consumption_id=consumption_log.consumption_id,
    resource_id=result.resource_id
)

状态TaskStatus.SUCCESS

3. 退还积分(任务失败或取消时)

await credit_service.refund_credits(
    consumption_id=consumption_log.consumption_id,
    reason="任务失败/用户取消"
)

状态TaskStatus.REFUNDEDTaskStatus.FAILED


数据关联

ai_jobs 表

-- 新增字段(已在迁移中创建)
consumption_log_id UUID  -- 积分消耗日志 ID(应用层验证)

-- 索引
CREATE INDEX idx_ai_jobs_consumption_log_id ON ai_jobs (consumption_log_id) 
    WHERE consumption_log_id IS NOT NULL;

credit_consumption_logs 表

-- 新增字段(已在迁移中创建)
ai_job_id UUID  -- AI 任务 ID(应用层验证)

-- 索引
CREATE INDEX idx_credit_consumption_logs_ai_job_id ON credit_consumption_logs (ai_job_id) 
    WHERE ai_job_id IS NOT NULL;

使用示例

1. 创建图片生成任务

from app.services.ai_service import AIService

ai_service = AIService(db)

result = await ai_service.generate_image(
    user_id="019d1234-5678-7abc-def0-111111111111",
    prompt="一只可爱的猫咪在花园里玩耍",
    width=1024,
    height=1024,
    style="realistic"
)

# 返回
{
    "job_id": "019d1234-5678-7abc-def0-222222222222",
    "task_id": "abc-123-def",
    "status": "pending",
    "estimated_cost": 0.03,
    "estimated_credits": 10
}

2. 查询任务状态

status = await ai_service.get_job_status(job_id)

# 返回
{
    "job_id": "019d1234-5678-7abc-def0-222222222222",
    "job_type": 1,  # AIJobType.IMAGE
    "status": 2,    # AIJobStatus.PROCESSING
    "progress": 50,
    "credits_used": 10,
    ...
}

3. 取消任务并退还积分

await ai_service.cancel_job(
    user_id="019d1234-5678-7abc-def0-111111111111",
    job_id="019d1234-5678-7abc-def0-222222222222"
)

# 任务状态更新为 CANCELLED
# 积分自动退还到用户账户

影响范围

新增文件

  • docs/server/changelogs/2026-01-29-ai-service-credit-integration.md

修改文件

  • server/app/repositories/ai_job_repository.py - 添加 exists()
  • server/app/repositories/ai_model_repository.py - 添加 exists()
  • server/app/repositories/ai_quota_repository.py - 添加 exists()
  • server/app/repositories/user_repository.py - 添加 exists()
  • server/app/repositories/project_repository.py - 添加 exists()
  • server/app/services/ai_service.py - 完整重构,集成 Credit Service

数据库变更

  • 无(表结构已在之前的迁移中创建)

待完成工作

高优先级

  1. Celery Tasks 实现

    • 实现 generate_image_task
    • 实现 generate_video_task
    • 实现 generate_sound_task
    • 实现 generate_voice_task
    • 实现 generate_subtitle_task
    • 实现 process_text_task
    • 在任务成功时调用 confirm_consumption()
    • 在任务失败时调用 refund_credits()
  2. AI Providers 实现

    • 实现 OpenAI Provider
    • 实现 Stability AI Provider
    • 实现 Runway Provider
    • 添加错误处理和重试逻辑

中优先级

  1. 单元测试

    • 测试应用层引用完整性验证
    • 测试积分预扣流程
    • 测试积分退还流程
    • 测试事务回滚
  2. 集成测试

    • 测试完整的任务创建流程
    • 测试任务取消流程
    • 测试积分不足场景

注意事项

1. 应用层引用完整性

所有关联字段都没有物理外键,必须在 Service 层验证:

# ✅ 正确:先验证关联是否存在
await self._validate_user_exists(user_id)
await self._validate_model_exists(model_id)
await self._validate_project_exists(project_id)

# ❌ 错误:直接创建任务,不验证关联
job = AIJob(user_id=user_id, model_id=model_id, ...)

2. 事务管理

积分扣除和任务创建必须在同一事务中:

# ✅ 正确:使用事务
async with self.db.begin():
    consumption_log = await credit_service.consume_credits(...)
    job = await job_repository.create(...)
    consumption_log.ai_job_id = job.ai_job_id
    await self.db.flush()

# ❌ 错误:分开执行,可能导致数据不一致
consumption_log = await credit_service.consume_credits(...)
job = await job_repository.create(...)  # 如果这里失败,积分已扣除

3. 错误处理

必须捕获 InsufficientCreditsError 并转换为 ValidationError:

try:
    consumption_log = await credit_service.consume_credits(...)
except InsufficientCreditsError as e:
    raise ValidationError(f"积分不足: {str(e)}")

4. 日志记录

记录关键操作,便于排查问题:

logger.info(f"图片生成任务已创建: job_id={job.ai_job_id}, user_id={user_id}, credits={credits_needed}")
logger.warning(f"用户 {user_id} 积分不足: {str(e)}")
logger.error(f"退还积分失败: job_id={job_id}, error={str(e)}")

相关文档


作者

  • Kiro AI Assistant
  • 日期:2026-01-29