# 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 ```python async def exists(self, job_id: str) -> bool: """检查任务是否存在(应用层引用完整性保证)""" ``` #### 1.2 AIModelRepository ```python async def exists(self, model_id: str) -> bool: """检查模型是否存在(应用层引用完整性保证)""" ``` #### 1.3 AIQuotaRepository ```python async def exists(self, quota_id: str) -> bool: """检查配额是否存在(应用层引用完整性保证)""" ``` #### 1.4 UserRepository ```python async def exists(self, user_id: UUID) -> bool: """检查用户是否存在(应用层引用完整性保证)""" ``` #### 1.5 ProjectRepository ```python async def exists(self, project_id: str) -> bool: """检查项目是否存在(应用层引用完整性保证)""" ``` **设计说明**: - 使用高效的 COUNT 查询 - 返回布尔值,避免加载完整对象 - 遵循统一的命名和实现模式 --- ### 2. AIService 层:完整重构 #### 2.1 注入 CreditService ```python 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 应用层引用完整性验证 ```python 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 积分预扣流程(以图片生成为例) ```python 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 任务取消与积分退还 ```python 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. 预扣积分(任务创建时) ```python consumption_log = await credit_service.consume_credits( user_id=UUID(user_id), amount=credits_needed, feature_type=feature_type, task_params={...} ) ``` **状态**:`TaskStatus.PENDING` ### 2. 确认消耗(任务成功时) ```python # 在 Celery Worker 中调用 await credit_service.confirm_consumption( consumption_id=consumption_log.consumption_id, resource_id=result.resource_id ) ``` **状态**:`TaskStatus.SUCCESS` ### 3. 退还积分(任务失败或取消时) ```python await credit_service.refund_credits( consumption_id=consumption_log.consumption_id, reason="任务失败/用户取消" ) ``` **状态**:`TaskStatus.REFUNDED` 或 `TaskStatus.FAILED` --- ## 数据关联 ### ai_jobs 表 ```sql -- 新增字段(已在迁移中创建) 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 表 ```sql -- 新增字段(已在迁移中创建) 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. 创建图片生成任务 ```python 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. 查询任务状态 ```python 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. 取消任务并退还积分 ```python 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 - [ ] 添加错误处理和重试逻辑 ### 中优先级 3. **单元测试** - [ ] 测试应用层引用完整性验证 - [ ] 测试积分预扣流程 - [ ] 测试积分退还流程 - [ ] 测试事务回滚 4. **集成测试** - [ ] 测试完整的任务创建流程 - [ ] 测试任务取消流程 - [ ] 测试积分不足场景 --- ## 注意事项 ### 1. 应用层引用完整性 所有关联字段都没有物理外键,必须在 Service 层验证: ```python # ✅ 正确:先验证关联是否存在 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. 事务管理 积分扣除和任务创建必须在同一事务中: ```python # ✅ 正确:使用事务 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: ```python try: consumption_log = await credit_service.consume_credits(...) except InsufficientCreditsError as e: raise ValidationError(f"积分不足: {str(e)}") ``` ### 4. 日志记录 记录关键操作,便于排查问题: ```python 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)}") ``` --- ## 相关文档 - [AI Service 设计文档](../../requirements/backend/04-services/ai/ai-service.md) - [AI Service 数据库实现](./2026-01-29-ai-service-complete-implementation.md) - [Credit Service 实现](./2026-01-28-credit-service-implementation.md) - [Jointo Tech Stack](../../../.claude/skills/jointo-tech-stack/SKILL.md) --- ## 作者 - Kiro AI Assistant - 日期:2026-01-29