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
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.REFUNDED 或 TaskStatus.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
数据库变更
- 无(表结构已在之前的迁移中创建)
待完成工作
高优先级
-
Celery Tasks 实现
- 实现 generate_image_task
- 实现 generate_video_task
- 实现 generate_sound_task
- 实现 generate_voice_task
- 实现 generate_subtitle_task
- 实现 process_text_task
- 在任务成功时调用
confirm_consumption() - 在任务失败时调用
refund_credits()
-
AI Providers 实现
- 实现 OpenAI Provider
- 实现 Stability AI Provider
- 实现 Runway Provider
- 添加错误处理和重试逻辑
中优先级
-
单元测试
- 测试应用层引用完整性验证
- 测试积分预扣流程
- 测试积分退还流程
- 测试事务回滚
-
集成测试
- 测试完整的任务创建流程
- 测试任务取消流程
- 测试积分不足场景
注意事项
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