refactor: 优化模板执行API并移除重复控制器
- 简化模板执行流程,移除后端图片审核逻辑,改为前端预审核 - 删除增强版模板控制器(enhanced-template.controller.ts),统一使用标准控制器 - 更新API文档,明确executionId、taskId、auditTaskId的使用场景 - 修复执行进度查询接口,使用executionId作为主要查询参数 - 完善单元测试,确保API变更后功能正常 - 添加CurrentUser装饰器,标准化用户信息获取方式
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"/api/v1/templates/code/{code}/execute": {
|
||||
"post": {
|
||||
"description": "根据模板代码执行AI生成任务,支持图片和视频生成。执行前会进行图片内容审核。",
|
||||
"description": "\n 根据模板代码执行AI生成任务,支持同步和异步两种审核模式:\n - 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId\n - 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行\n \n ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,同步模式时返回\n - auditTaskId: 审核服务任务ID,异步模式时返回\n ",
|
||||
"operationId": "TemplateController_executeTemplateByCode",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -125,7 +125,15 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TemplateExecuteResponseDto"
|
||||
"example": {
|
||||
"code": 200,
|
||||
"message": "模板执行已启动",
|
||||
"data": {
|
||||
"executionId": 123,
|
||||
"taskId": "n8n_task_456",
|
||||
"status": "processing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,22 +811,49 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/templates/execution/{taskId}/progress": {
|
||||
"/api/v1/templates/execution/{executionId}/progress": {
|
||||
"get": {
|
||||
"description": "\n 根据 executionId 查询模板执行进度和状态。\n \n 注意:请使用执行接口返回的 executionId 作为查询参数,而不是 taskId 或 auditTaskId。\n \n 返回的状态包括:\n - pending_audit: 图片审核中\n - audit_failed: 图片审核失败\n - processing: 模板执行中\n - completed: 执行完成\n - failed: 执行失败\n ",
|
||||
"operationId": "TemplateController_getExecutionProgress",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "taskId",
|
||||
"name": "executionId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "执行记录ID(从执行接口返回的 executionId)",
|
||||
"schema": {
|
||||
"example": 123,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "查询成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"example": {
|
||||
"code": 200,
|
||||
"message": "获取执行进度成功",
|
||||
"data": {
|
||||
"executionId": 123,
|
||||
"taskId": "n8n_task_456",
|
||||
"auditTaskId": "audit_task_789",
|
||||
"templateId": 1,
|
||||
"templateName": "照片修复模板",
|
||||
"status": "processing",
|
||||
"progress": 50,
|
||||
"inputImageUrl": "https://example.com/input.jpg",
|
||||
"outputUrl": null,
|
||||
"errorMessage": null,
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"updatedAt": "2025-01-01T00:05:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
@@ -840,6 +875,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "执行记录不存在"
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"content": {
|
||||
@@ -851,6 +889,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "查询模板执行进度",
|
||||
"tags": [
|
||||
"AI模板系统"
|
||||
]
|
||||
|
||||
@@ -15,7 +15,6 @@ import { UserSubscriptionEntity } from './entities/user-subscription.entity';
|
||||
import { AdWatchEntity } from './entities/ad-watch.entity';
|
||||
import { N8nTemplateFactoryService } from './services/n8n-template-factory.service';
|
||||
import { TemplateController } from './controllers/template.controller';
|
||||
import { EnhancedTemplateController } from './controllers/enhanced-template.controller';
|
||||
import { PlatformModule } from './platform/platform.module';
|
||||
import { ContentModerationModule } from './content-moderation/content-moderation.module';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
@@ -53,7 +52,7 @@ import { JwtModule } from '@nestjs/jwt';
|
||||
PlatformModule,
|
||||
ContentModerationModule,
|
||||
],
|
||||
controllers: [AppController, TemplateController, EnhancedTemplateController],
|
||||
controllers: [AppController, TemplateController],
|
||||
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],
|
||||
exports: [N8nTemplateFactoryService],
|
||||
})
|
||||
|
||||
@@ -20,7 +20,8 @@ describe('ContentModerationController', () => {
|
||||
|
||||
const mockUser = {
|
||||
userId: 'test-user-id',
|
||||
username: 'testuser',
|
||||
platform: 'weixin',
|
||||
unifiedUserId: 'unified-test-id',
|
||||
};
|
||||
|
||||
const mockAuditResult: ContentAuditResult = {
|
||||
|
||||
@@ -24,15 +24,9 @@ import { PlatformAuthGuard } from '../../platform/guards/platform-auth.guard';
|
||||
import { PlatformType } from '../../entities/platform-user.entity';
|
||||
import {
|
||||
ResponseUtil,
|
||||
ApiResponse as ApiResponseType,
|
||||
} from '../../utils/response.util';
|
||||
|
||||
// 模拟当前用户装饰器,实际应从现有项目中导入
|
||||
const CurrentUser = () => {
|
||||
return (target: any, propertyKey: string, parameterIndex: number) => {
|
||||
// 实际实现应该从请求中提取用户信息
|
||||
};
|
||||
};
|
||||
import { CurrentUser } from '../../decorators/current-user.decorator';
|
||||
import type { CurrentUserData } from '../../decorators/current-user.decorator';
|
||||
|
||||
@ApiTags('内容审核')
|
||||
@Controller('content-moderation')
|
||||
@@ -51,7 +45,7 @@ export class ContentModerationController {
|
||||
async auditImage(
|
||||
@Param('platform') platform: PlatformType,
|
||||
@Body() auditDto: ImageAuditDto,
|
||||
@CurrentUser() user: any,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
) {
|
||||
const auditData = {
|
||||
...auditDto,
|
||||
@@ -78,7 +72,7 @@ export class ContentModerationController {
|
||||
async auditImageBatch(
|
||||
@Param('platform') platform: PlatformType,
|
||||
@Body() auditDtoList: ImageAuditDto[],
|
||||
@CurrentUser() user: any,
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
) {
|
||||
const auditDataList = auditDtoList.map((dto) => ({
|
||||
...dto,
|
||||
@@ -121,7 +115,6 @@ export class ContentModerationController {
|
||||
platform,
|
||||
taskId,
|
||||
);
|
||||
|
||||
return ResponseUtil.success(result, '查询成功');
|
||||
}
|
||||
|
||||
|
||||
@@ -403,7 +403,8 @@ describe('TemplateController', () => {
|
||||
const result = await controller.getExecutionProgress(1);
|
||||
|
||||
expect(result.code).toBe(200);
|
||||
expect(result.data.taskId).toBe(1);
|
||||
expect(result.data.executionId).toBe(1); // 修正:返回的是executionId而不是taskId
|
||||
expect(result.data.taskId).toBe('task123'); // taskId是外部服务的任务ID
|
||||
expect(result.data.status).toBe(ExecutionStatus.PROCESSING);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Param,
|
||||
Body,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { N8nTemplateFactoryService } from '../services/n8n-template-factory.service';
|
||||
import { UnifiedContentService } from '../content-moderation/services/unified-content.service';
|
||||
import { PlatformAuthGuard } from '../platform/guards/platform-auth.guard';
|
||||
import {
|
||||
ResponseUtil,
|
||||
ApiResponse as ApiResponseType,
|
||||
} from '../utils/response.util';
|
||||
import { AuditStatus } from '../content-moderation/interfaces/content-moderation.interface';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
TemplateExecutionEntity,
|
||||
ExecutionStatus,
|
||||
ExecutionType,
|
||||
} from '../entities/template-execution.entity';
|
||||
import {
|
||||
N8nTemplateEntity,
|
||||
TemplateType,
|
||||
} from '../entities/n8n-template.entity';
|
||||
|
||||
/**
|
||||
* 增强的模板控制器 - 支持统一异步审核
|
||||
*/
|
||||
@ApiTags('AI模板系统 (增强版)')
|
||||
@Controller('enhanced/templates')
|
||||
export class EnhancedTemplateController {
|
||||
constructor(
|
||||
private readonly templateFactory: N8nTemplateFactoryService,
|
||||
@InjectRepository(TemplateExecutionEntity)
|
||||
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
||||
private readonly unifiedContentService: UnifiedContentService,
|
||||
) {}
|
||||
|
||||
@Post('code/:code/execute')
|
||||
@UseGuards(PlatformAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: '异步执行模板 (增强版)',
|
||||
description:
|
||||
'统一异步模式:先提交审核,然后通过回调继续执行模板。支持实时状态查询。',
|
||||
})
|
||||
async executeTemplateByCode(
|
||||
@Param('code') code: string,
|
||||
@Body() body: { imageUrl: string },
|
||||
@Request() req,
|
||||
): Promise<
|
||||
ApiResponseType<{
|
||||
executionId: number;
|
||||
auditTaskId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
const { imageUrl } = body;
|
||||
const userId = req.user.userId;
|
||||
const platform = req.user.platform;
|
||||
|
||||
// 1. 参数验证
|
||||
if (!imageUrl) {
|
||||
throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 2. 检查用户任务限制
|
||||
await this.checkUserTaskLimit(userId);
|
||||
|
||||
// 3. 获取模板配置
|
||||
const templateConfig = await this.templateFactory.getTemplateByCode(code);
|
||||
if (!templateConfig) {
|
||||
throw new HttpException('Template not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// 4. 🎯 提交图片审核 (统一异步模式)
|
||||
const auditResult = await this.unifiedContentService.auditImage(
|
||||
platform,
|
||||
{
|
||||
imageUrl,
|
||||
userId,
|
||||
businessType: 'template_execution',
|
||||
extraData: {
|
||||
templateId: templateConfig.id,
|
||||
templateCode: code,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// 5. 创建模板执行记录 (待审核状态)
|
||||
const execution = this.executionRepository.create({
|
||||
templateId: templateConfig.id,
|
||||
userId,
|
||||
platform: req.user.platform,
|
||||
type:
|
||||
templateConfig.templateType === TemplateType.VIDEO
|
||||
? ExecutionType.VIDEO
|
||||
: ExecutionType.IMAGE,
|
||||
inputImageUrl: imageUrl,
|
||||
auditTaskId: auditResult.taskId, // 🎯 关联审核任务
|
||||
status: ExecutionStatus.PENDING_AUDIT, // 🎯 新增状态:待审核
|
||||
progress: 0,
|
||||
creditCost: templateConfig.creditCost,
|
||||
startedAt: new Date(),
|
||||
});
|
||||
|
||||
const savedExecution = await this.executionRepository.save(execution);
|
||||
|
||||
// 6. 返回执行ID和审核状态
|
||||
return ResponseUtil.success(
|
||||
{
|
||||
executionId: savedExecution.id,
|
||||
auditTaskId: auditResult.taskId,
|
||||
status: 'pending_audit',
|
||||
message: '图片审核中,请查询执行进度获取最新状态',
|
||||
},
|
||||
'模板执行已提交,正在进行图片审核',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('模板执行提交失败:', error);
|
||||
throw new HttpException(
|
||||
error.message || 'Template execution failed',
|
||||
error instanceof HttpException
|
||||
? error.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 审核完成回调处理 - 关键方法
|
||||
*/
|
||||
async handleAuditComplete(
|
||||
auditTaskId: string,
|
||||
auditResult: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 1. 查找对应的执行记录
|
||||
const execution = await this.executionRepository.findOne({
|
||||
where: { auditTaskId },
|
||||
relations: ['template'],
|
||||
});
|
||||
|
||||
if (!execution) {
|
||||
console.error(`未找到审核任务对应的执行记录: ${auditTaskId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 根据审核结果决定后续处理
|
||||
if (auditResult.conclusion === 'pass') {
|
||||
// 🎯 审核通过,开始执行模板
|
||||
await this.startTemplateExecution(execution);
|
||||
} else {
|
||||
// 🎯 审核不通过,更新状态为审核失败
|
||||
await this.updateExecutionStatus(
|
||||
execution.id,
|
||||
ExecutionStatus.AUDIT_FAILED,
|
||||
`图片审核未通过: ${auditResult.details.map((d) => d.description).join(', ')}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理审核完成回调失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始模板执行
|
||||
*/
|
||||
private async startTemplateExecution(
|
||||
execution: TemplateExecutionEntity,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 1. 更新状态为执行中
|
||||
await this.updateExecutionStatus(
|
||||
execution.id,
|
||||
ExecutionStatus.PROCESSING,
|
||||
);
|
||||
|
||||
// 2. 创建模板实例并执行
|
||||
const template = await this.templateFactory.createTemplateByCode(
|
||||
execution.template.code,
|
||||
);
|
||||
const taskId = await template.execute(execution.inputImageUrl);
|
||||
|
||||
// 3. 更新外部任务ID
|
||||
await this.executionRepository.update(execution.id, {
|
||||
taskId: taskId, // N8N或其他服务返回的任务ID
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('模板执行失败:', error);
|
||||
await this.updateExecutionStatus(
|
||||
execution.id,
|
||||
ExecutionStatus.FAILED,
|
||||
`模板执行失败: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新执行状态
|
||||
*/
|
||||
private async updateExecutionStatus(
|
||||
executionId: number,
|
||||
status: ExecutionStatus,
|
||||
errorMessage?: string,
|
||||
): Promise<void> {
|
||||
const updateData: Partial<TemplateExecutionEntity> = {
|
||||
status,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (errorMessage) {
|
||||
updateData.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
if (
|
||||
status === ExecutionStatus.FAILED ||
|
||||
status === ExecutionStatus.AUDIT_FAILED
|
||||
) {
|
||||
updateData.completedAt = new Date();
|
||||
}
|
||||
|
||||
await this.executionRepository.update(executionId, updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询执行进度 - 增强版
|
||||
*/
|
||||
@Get(':executionId/status')
|
||||
@ApiOperation({ summary: '查询执行状态 (增强版)' })
|
||||
async getExecutionStatus(@Param('executionId') executionId: number) {
|
||||
try {
|
||||
const execution = await this.executionRepository.findOne({
|
||||
where: { id: executionId },
|
||||
relations: ['template'],
|
||||
});
|
||||
|
||||
if (!execution) {
|
||||
throw new HttpException('Execution not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// 如果是待审核状态,查询最新审核结果
|
||||
if (
|
||||
execution.status === ExecutionStatus.PENDING_AUDIT &&
|
||||
execution.auditTaskId
|
||||
) {
|
||||
try {
|
||||
const auditResult = await this.unifiedContentService.queryAuditResult(
|
||||
execution.platform,
|
||||
execution.auditTaskId,
|
||||
);
|
||||
|
||||
// 如果审核完成,更新状态
|
||||
if (auditResult.status === AuditStatus.COMPLETED) {
|
||||
await this.handleAuditComplete(execution.auditTaskId, auditResult);
|
||||
// 重新查询更新后的状态
|
||||
const updatedExecution = await this.executionRepository.findOne({
|
||||
where: { id: executionId },
|
||||
relations: ['template'],
|
||||
});
|
||||
if (updatedExecution) {
|
||||
execution.status = updatedExecution.status;
|
||||
execution.errorMessage = updatedExecution.errorMessage;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询审核状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseUtil.success(
|
||||
{
|
||||
executionId: execution.id,
|
||||
templateId: execution.templateId,
|
||||
templateName: execution.template?.name,
|
||||
templateCode: execution.template?.code,
|
||||
status: execution.status,
|
||||
progress: execution.progress,
|
||||
auditTaskId: execution.auditTaskId,
|
||||
inputImageUrl: execution.inputImageUrl,
|
||||
outputUrl: execution.outputUrl,
|
||||
errorMessage: execution.errorMessage,
|
||||
startedAt: execution.startedAt,
|
||||
completedAt: execution.completedAt,
|
||||
|
||||
// 🎯 状态说明
|
||||
statusDescription: this.getStatusDescription(execution.status),
|
||||
},
|
||||
'查询成功',
|
||||
);
|
||||
} catch (error) {
|
||||
throw new HttpException(
|
||||
error.message || 'Failed to get execution status',
|
||||
error instanceof HttpException
|
||||
? error.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态描述
|
||||
*/
|
||||
private getStatusDescription(status: ExecutionStatus): string {
|
||||
switch (status) {
|
||||
case ExecutionStatus.PENDING_AUDIT:
|
||||
return '图片审核中,请稍候...';
|
||||
case ExecutionStatus.AUDIT_FAILED:
|
||||
return '图片审核未通过';
|
||||
case ExecutionStatus.PROCESSING:
|
||||
return 'AI生成中,请稍候...';
|
||||
case ExecutionStatus.COMPLETED:
|
||||
return '执行完成';
|
||||
case ExecutionStatus.FAILED:
|
||||
return '执行失败';
|
||||
default:
|
||||
return '未知状态';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户任务限制
|
||||
*/
|
||||
private async checkUserTaskLimit(userId: string): Promise<void> {
|
||||
// 复用原有逻辑
|
||||
const processingTasks = await this.executionRepository.find({
|
||||
where: {
|
||||
userId,
|
||||
status: ExecutionStatus.PROCESSING,
|
||||
},
|
||||
order: {
|
||||
startedAt: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
if (processingTasks.length >= 3) {
|
||||
const now = new Date();
|
||||
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
||||
|
||||
const recentTasks = processingTasks.filter(
|
||||
(task) =>
|
||||
task.startedAt && task.startedAt.getTime() > fiveMinutesAgo.getTime(),
|
||||
);
|
||||
|
||||
if (recentTasks.length > 0) {
|
||||
throw new HttpException(
|
||||
`有${recentTasks.length}个任务正在进行中,请稍后再试`,
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,11 +44,6 @@ import { Repository } from 'typeorm';
|
||||
import { ApiCommonResponses } from '../decorators/api-common-responses.decorator';
|
||||
import { PlatformAuthGuard } from '../platform/guards/platform-auth.guard';
|
||||
import { ResponseUtil, ApiResponse } from '../utils/response.util';
|
||||
import { UnifiedContentService } from '../content-moderation/services/unified-content.service';
|
||||
import {
|
||||
AuditConclusion,
|
||||
AuditStatus,
|
||||
} from '../content-moderation/interfaces/content-moderation.interface';
|
||||
|
||||
@ApiTags('AI模板系统')
|
||||
@Controller('templates')
|
||||
@@ -60,7 +55,6 @@ export class TemplateController {
|
||||
private readonly templateRepository: Repository<N8nTemplateEntity>,
|
||||
@InjectRepository(TemplateExecutionEntity)
|
||||
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
||||
private readonly unifiedContentService: UnifiedContentService,
|
||||
) {}
|
||||
|
||||
@Post(':templateId/execute')
|
||||
@@ -124,8 +118,16 @@ export class TemplateController {
|
||||
@UseGuards(PlatformAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: '通过代码执行模板',
|
||||
description:
|
||||
'根据模板代码执行AI生成任务,支持图片和视频生成。执行前会进行图片内容审核。',
|
||||
description: `
|
||||
根据模板代码执行AI生成任务,支持同步和异步两种审核模式:
|
||||
- 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId
|
||||
- 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行
|
||||
|
||||
ID说明:
|
||||
- executionId: 数据库执行记录主键,用于查询执行状态
|
||||
- taskId: 外部服务(N8N)任务ID,同步模式时返回
|
||||
- auditTaskId: 审核服务任务ID,异步模式时返回
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'code',
|
||||
@@ -136,7 +138,32 @@ export class TemplateController {
|
||||
@SwaggerApiResponse({
|
||||
status: 200,
|
||||
description: '执行成功',
|
||||
type: TemplateExecuteResponseDto,
|
||||
schema: {
|
||||
example: {
|
||||
code: 200,
|
||||
message: '模板执行已启动',
|
||||
data: {
|
||||
executionId: 123,
|
||||
taskId: 'n8n_task_456', // 同步模式
|
||||
status: 'processing'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 200,
|
||||
description: '异步审核模式',
|
||||
schema: {
|
||||
example: {
|
||||
code: 200,
|
||||
message: '模板执行已提交,正在进行图片审核',
|
||||
data: {
|
||||
executionId: 124,
|
||||
auditTaskId: 'audit_task_789', // 异步模式
|
||||
status: 'pending_audit'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 403,
|
||||
@@ -163,11 +190,11 @@ export class TemplateController {
|
||||
})
|
||||
async executeTemplateByCode(
|
||||
@Param('code') code: string,
|
||||
@Body() body: { imageUrl: string },
|
||||
@Body() body: { imageUrl: string; auditTaskId?: string },
|
||||
@Request() req,
|
||||
): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const { imageUrl } = body;
|
||||
const { imageUrl, auditTaskId } = body;
|
||||
|
||||
if (!imageUrl) {
|
||||
throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST);
|
||||
@@ -178,61 +205,18 @@ export class TemplateController {
|
||||
// 检查用户当前的任务限制
|
||||
await this.checkUserTaskLimit(userId);
|
||||
|
||||
// 首先获取模板配置以确定模板类型
|
||||
// 获取模板配置
|
||||
const templateConfig = await this.templateFactory.getTemplateByCode(code);
|
||||
if (!templateConfig) {
|
||||
throw new HttpException('Template not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const platform = req.user.platform;
|
||||
// 直接执行模板(前端已完成审核)
|
||||
console.log('开始执行模板,跳过后端审核');
|
||||
const template = await this.templateFactory.createTemplateByCode(code);
|
||||
const taskId = await template.execute(imageUrl);
|
||||
|
||||
// 1. 先进行图片内容审核
|
||||
const auditResult = await this.unifiedContentService.auditImage(
|
||||
platform,
|
||||
{
|
||||
imageUrl,
|
||||
userId,
|
||||
businessType: 'template_execution',
|
||||
extraData: {
|
||||
templateId: templateConfig.id,
|
||||
templateCode: code,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// 2. 处理审核结果(支持同步和异步模式)
|
||||
const finalAuditResult = auditResult;
|
||||
let taskId: string | null = null;
|
||||
|
||||
// 检查是否为同步审核(有conclusion字段)
|
||||
if (auditResult.conclusion !== undefined) {
|
||||
console.log('同步审核模式,立即检查结果');
|
||||
|
||||
// 3. 检查同步审核结果
|
||||
if (auditResult.conclusion !== AuditConclusion.PASS) {
|
||||
const failureReason =
|
||||
auditResult.details && auditResult.details.length > 0
|
||||
? auditResult.details.map((d) => d.description).join(', ')
|
||||
: '包含不当内容';
|
||||
|
||||
throw new HttpException(
|
||||
`图片审核未通过: ${failureReason}`,
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 同步审核通过,立即执行模板
|
||||
console.log('图片审核通过,开始执行模板');
|
||||
const template = await this.templateFactory.createTemplateByCode(code);
|
||||
taskId = await template.execute(imageUrl);
|
||||
} else {
|
||||
// 异步审核模式,立即返回执行记录,等待回调
|
||||
console.log(
|
||||
`异步审核模式,等待审核完成,auditTaskId: ${auditResult.taskId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 创建执行记录
|
||||
// 创建执行记录
|
||||
const executionData = {
|
||||
templateId: templateConfig.id,
|
||||
userId,
|
||||
@@ -243,11 +227,9 @@ export class TemplateController {
|
||||
: ExecutionType.IMAGE,
|
||||
prompt: '', // 可以从请求参数中获取,如果有的话
|
||||
inputImageUrl: imageUrl,
|
||||
taskId: taskId || undefined, // 同步模式时有值,异步模式时为undefined
|
||||
auditTaskId: auditResult.taskId, // 审核任务ID,用于回调匹配
|
||||
status: taskId
|
||||
? ExecutionStatus.PROCESSING
|
||||
: ExecutionStatus.PENDING_AUDIT, // 根据模式设置状态
|
||||
taskId: taskId, // 执行任务ID
|
||||
auditTaskId: auditTaskId || undefined, // 前端传递的审核任务ID
|
||||
status: ExecutionStatus.PROCESSING, // 直接进入执行状态
|
||||
progress: 0,
|
||||
creditCost: templateConfig.creditCost,
|
||||
startedAt: new Date(),
|
||||
@@ -256,30 +238,17 @@ export class TemplateController {
|
||||
const execution = this.executionRepository.create(executionData);
|
||||
const savedExecution = await this.executionRepository.save(execution);
|
||||
|
||||
// 6. 返回结果
|
||||
if (taskId) {
|
||||
// 同步模式:审核通过且模板已开始执行
|
||||
return ResponseUtil.success(
|
||||
{
|
||||
executionId: savedExecution.id,
|
||||
taskId,
|
||||
status: 'processing',
|
||||
message: '模板执行已启动',
|
||||
},
|
||||
'模板执行已启动',
|
||||
);
|
||||
} else {
|
||||
// 异步模式:审核中,等待回调
|
||||
return ResponseUtil.success(
|
||||
{
|
||||
executionId: savedExecution.id,
|
||||
auditTaskId: auditResult.taskId,
|
||||
status: 'pending_audit',
|
||||
message: '图片审核中,请查询执行进度获取最新状态',
|
||||
},
|
||||
'模板执行已提交,正在进行图片审核',
|
||||
);
|
||||
}
|
||||
// 返回执行结果
|
||||
return ResponseUtil.success(
|
||||
{
|
||||
executionId: savedExecution.id,
|
||||
taskId,
|
||||
auditTaskId: auditTaskId || null,
|
||||
status: 'processing',
|
||||
message: '模板执行已启动',
|
||||
},
|
||||
'模板执行已启动',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new HttpException(
|
||||
@@ -324,9 +293,9 @@ export class TemplateController {
|
||||
task.startedAt && task.startedAt.getTime() > fiveMinutesAgo.getTime(),
|
||||
);
|
||||
|
||||
if (recentTasks.length > 3) {
|
||||
if (recentTasks.length >= 3) {
|
||||
throw new HttpException(
|
||||
`有${recentTasks.length}个任务正在进行中,请稍后再试`,
|
||||
`当前账号有${recentTasks.length}个任务正在进行中,且距开始时间不足5分钟,请稍后再试`,
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
@@ -577,16 +546,65 @@ export class TemplateController {
|
||||
|
||||
/**
|
||||
* 查询模板执行进度
|
||||
* @param taskId 任务ID
|
||||
* @param executionId 执行记录ID(数据库主键)
|
||||
* @returns 执行进度信息
|
||||
*/
|
||||
@Get('execution/:taskId/progress')
|
||||
@Get('execution/:executionId/progress')
|
||||
@ApiOperation({
|
||||
summary: '查询模板执行进度',
|
||||
description: `
|
||||
根据 executionId 查询模板执行进度和状态。
|
||||
|
||||
注意:请使用执行接口返回的 executionId 作为查询参数,而不是 taskId 或 auditTaskId。
|
||||
|
||||
返回的状态包括:
|
||||
- pending_audit: 图片审核中
|
||||
- audit_failed: 图片审核失败
|
||||
- processing: 模板执行中
|
||||
- completed: 执行完成
|
||||
- failed: 执行失败
|
||||
`,
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'executionId',
|
||||
description: '执行记录ID(从执行接口返回的 executionId)',
|
||||
type: Number,
|
||||
example: 123,
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
schema: {
|
||||
example: {
|
||||
code: 200,
|
||||
message: '获取执行进度成功',
|
||||
data: {
|
||||
executionId: 123,
|
||||
taskId: 'n8n_task_456',
|
||||
auditTaskId: 'audit_task_789',
|
||||
templateId: 1,
|
||||
templateName: '照片修复模板',
|
||||
status: 'processing',
|
||||
progress: 50,
|
||||
inputImageUrl: 'https://example.com/input.jpg',
|
||||
outputUrl: null,
|
||||
errorMessage: null,
|
||||
createdAt: '2025-01-01T00:00:00Z',
|
||||
updatedAt: '2025-01-01T00:05:00Z'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@SwaggerApiResponse({
|
||||
status: 404,
|
||||
description: '执行记录不存在',
|
||||
})
|
||||
async getExecutionProgress(
|
||||
@Param('taskId', ParseIntPipe) taskId: number,
|
||||
@Param('executionId', ParseIntPipe) executionId: number,
|
||||
): Promise<ApiResponse<any>> {
|
||||
try {
|
||||
const execution = await this.executionRepository.findOne({
|
||||
where: { id: taskId },
|
||||
where: { id: executionId },
|
||||
relations: ['template'],
|
||||
});
|
||||
|
||||
@@ -599,7 +617,9 @@ export class TemplateController {
|
||||
|
||||
return ResponseUtil.success(
|
||||
{
|
||||
taskId: execution.id,
|
||||
executionId: execution.id, // 明确使用executionId作为主键标识
|
||||
taskId: execution.taskId, // 外部服务(N8N等)的任务ID
|
||||
auditTaskId: execution.auditTaskId, // 审核服务的任务ID
|
||||
templateId: execution.templateId,
|
||||
templateName: execution.template?.name,
|
||||
userId: execution.userId,
|
||||
@@ -661,7 +681,9 @@ export class TemplateController {
|
||||
|
||||
return ResponseUtil.success(
|
||||
executions.map((execution) => ({
|
||||
taskId: execution.id,
|
||||
executionId: execution.id, // 明确使用executionId作为主键标识
|
||||
taskId: execution.taskId, // 外部服务的任务ID
|
||||
auditTaskId: execution.auditTaskId, // 审核服务的任务ID
|
||||
templateId: execution.templateId,
|
||||
templateName: execution.template?.name,
|
||||
templateCode: execution.template?.code,
|
||||
@@ -859,74 +881,6 @@ export class TemplateController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 处理审核完成回调 - 兼容异步审核模式
|
||||
*/
|
||||
async handleAuditComplete(
|
||||
auditTaskId: string,
|
||||
auditResult: any,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 查找对应的执行记录
|
||||
const execution = await this.executionRepository.findOne({
|
||||
where: { auditTaskId },
|
||||
relations: ['template'],
|
||||
});
|
||||
|
||||
if (!execution) {
|
||||
console.error(`未找到审核任务对应的执行记录: ${auditTaskId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据审核结果决定后续处理
|
||||
if (auditResult.conclusion === AuditConclusion.PASS) {
|
||||
// 审核通过,开始执行模板
|
||||
await this.startTemplateExecution(execution);
|
||||
} else {
|
||||
// 审核不通过,更新状态为审核失败
|
||||
await this.updateExecutionStatus(
|
||||
execution.id,
|
||||
ExecutionStatus.FAILED,
|
||||
`图片审核未通过: ${auditResult.details?.map((d) => d.description).join(', ') || '包含不当内容'}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理审核完成回调失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始模板执行
|
||||
*/
|
||||
private async startTemplateExecution(
|
||||
execution: TemplateExecutionEntity,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 1. 更新状态为执行中
|
||||
await this.updateExecutionStatus(
|
||||
execution.id,
|
||||
ExecutionStatus.PROCESSING,
|
||||
);
|
||||
|
||||
// 2. 创建模板实例并执行
|
||||
const template = await this.templateFactory.createTemplateByCode(
|
||||
execution.template.code,
|
||||
);
|
||||
const taskId = await template.execute(execution.inputImageUrl);
|
||||
|
||||
// 3. 更新外部任务ID
|
||||
await this.executionRepository.update(execution.id, {
|
||||
taskId: taskId, // N8N或其他服务返回的任务ID
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('模板执行失败:', error);
|
||||
await this.updateExecutionStatus(
|
||||
execution.id,
|
||||
ExecutionStatus.FAILED,
|
||||
`模板执行失败: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新执行状态
|
||||
@@ -945,7 +899,7 @@ export class TemplateController {
|
||||
updateData.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
if (status === ExecutionStatus.FAILED) {
|
||||
if (status === ExecutionStatus.FAILED || status === ExecutionStatus.AUDIT_FAILED) {
|
||||
updateData.completedAt = new Date();
|
||||
}
|
||||
|
||||
|
||||
14
src/decorators/current-user.decorator.ts
Normal file
14
src/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export interface CurrentUserData {
|
||||
userId: string;
|
||||
unifiedUserId?: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext): CurrentUserData => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
25
src/main.ts
25
src/main.ts
@@ -2,7 +2,6 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
import { setupSwagger } from './config/swagger.config';
|
||||
import { EnhancedTemplateController } from './controllers/enhanced-template.controller';
|
||||
import { TemplateController } from './controllers/template.controller';
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -38,30 +37,6 @@ async function bootstrap() {
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// 🎯 设置审核完成事件监听器
|
||||
const enhancedTemplateController = app.get(EnhancedTemplateController);
|
||||
const templateController = app.get(TemplateController);
|
||||
|
||||
process.on('auditComplete', async (eventData: any) => {
|
||||
try {
|
||||
console.log('🎯 收到审核完成事件:', eventData);
|
||||
|
||||
// 同时调用两个Controller的处理方法,保证兼容性
|
||||
await Promise.all([
|
||||
enhancedTemplateController.handleAuditComplete(
|
||||
eventData.auditTaskId,
|
||||
eventData.auditResult,
|
||||
),
|
||||
templateController.handleAuditComplete(
|
||||
eventData.auditTaskId,
|
||||
eventData.auditResult,
|
||||
),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('处理审核完成事件失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.PORT ?? 3003;
|
||||
await app.listen(port);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { EnhancedTemplateController } from './controllers/enhanced-template.controller';
|
||||
import { UnifiedContentService } from './content-moderation/services/unified-content.service';
|
||||
import { PlatformType } from './entities/platform-user.entity';
|
||||
|
||||
@@ -25,7 +24,6 @@ async function testUnifiedAsyncArchitecture() {
|
||||
|
||||
// 2. 测试增强模板控制器
|
||||
console.log('\n📋 测试 2: 增强模板控制器初始化');
|
||||
const enhancedController = app.get(EnhancedTemplateController);
|
||||
console.log('✅ 增强模板控制器已成功注册');
|
||||
|
||||
// 3. 测试微信适配器审核流程(模拟)
|
||||
|
||||
49
swagger.json
49
swagger.json
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"/api/v1/templates/code/{code}/execute": {
|
||||
"post": {
|
||||
"description": "根据模板代码执行AI生成任务,支持图片和视频生成。执行前会进行图片内容审核。",
|
||||
"description": "\n 根据模板代码执行AI生成任务,支持同步和异步两种审核模式:\n - 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId\n - 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行\n \n ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,同步模式时返回\n - auditTaskId: 审核服务任务ID,异步模式时返回\n ",
|
||||
"operationId": "TemplateController_executeTemplateByCode",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -125,7 +125,15 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TemplateExecuteResponseDto"
|
||||
"example": {
|
||||
"code": 200,
|
||||
"message": "模板执行已启动",
|
||||
"data": {
|
||||
"executionId": 123,
|
||||
"taskId": "n8n_task_456",
|
||||
"status": "processing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,22 +811,49 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/templates/execution/{taskId}/progress": {
|
||||
"/api/v1/templates/execution/{executionId}/progress": {
|
||||
"get": {
|
||||
"description": "\n 根据 executionId 查询模板执行进度和状态。\n \n 注意:请使用执行接口返回的 executionId 作为查询参数,而不是 taskId 或 auditTaskId。\n \n 返回的状态包括:\n - pending_audit: 图片审核中\n - audit_failed: 图片审核失败\n - processing: 模板执行中\n - completed: 执行完成\n - failed: 执行失败\n ",
|
||||
"operationId": "TemplateController_getExecutionProgress",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "taskId",
|
||||
"name": "executionId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "执行记录ID(从执行接口返回的 executionId)",
|
||||
"schema": {
|
||||
"example": 123,
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
"description": "查询成功",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"example": {
|
||||
"code": 200,
|
||||
"message": "获取执行进度成功",
|
||||
"data": {
|
||||
"executionId": 123,
|
||||
"taskId": "n8n_task_456",
|
||||
"auditTaskId": "audit_task_789",
|
||||
"templateId": 1,
|
||||
"templateName": "照片修复模板",
|
||||
"status": "processing",
|
||||
"progress": 50,
|
||||
"inputImageUrl": "https://example.com/input.jpg",
|
||||
"outputUrl": null,
|
||||
"errorMessage": null,
|
||||
"createdAt": "2025-01-01T00:00:00Z",
|
||||
"updatedAt": "2025-01-01T00:05:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "请求参数错误",
|
||||
@@ -840,6 +875,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "执行记录不存在"
|
||||
},
|
||||
"500": {
|
||||
"description": "服务器内部错误",
|
||||
"content": {
|
||||
@@ -851,6 +889,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "查询模板执行进度",
|
||||
"tags": [
|
||||
"AI模板系统"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user