feat: 完善模板执行系统和任务状态管理
- 为 TemplateExecutionEntity 添加 taskId 字段用于任务追踪 - 完善 executeTemplateByCode 接口,保存执行记录到数据库 - 重构 app.controller.ts callback 逻辑,支持任务状态回调更新 - 添加平台认证守卫和用户信息获取 - 新增 AddTaskIdToTemplateExecution migration - 修复模板类型映射和执行状态管理 - 优化 N8N 模板返回 taskId 而非直接结果
This commit is contained in:
@@ -1,182 +1,73 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { TemplateService } from './templates/index';
|
||||
import { Template } from './templates/types';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TemplateExecutionEntity, ExecutionStatus } from './entities/template-execution.entity';
|
||||
interface ApiResponse<T = any> {
|
||||
status: boolean | string;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
@Controller('api/v1/templates')
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly tempalte: TemplateService) {}
|
||||
|
||||
@Get()
|
||||
async getTemplates(): Promise<ApiResponse<Template[]>> {
|
||||
console.log(`📋 [获取模板列表] 请求获取所有模板`);
|
||||
console.log(`⏰ 请求时间: ${new Date().toISOString()}`);
|
||||
|
||||
const templates = this.tempalte.getAllTemplates();
|
||||
|
||||
console.log(`✅ [获取模板列表] 成功获取 ${templates.length} 个模板`);
|
||||
|
||||
// 安全地输出模板列表
|
||||
try {
|
||||
const templateNames = templates
|
||||
.map((t) => t?.code || t?.name || '[未命名模板]')
|
||||
.filter(Boolean);
|
||||
console.log(`📊 模板列表: ${templateNames.join(', ')}`);
|
||||
} catch (error) {
|
||||
console.log(`📊 模板列表: [无法获取模板名称]`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: true,
|
||||
data: templates,
|
||||
msg: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':templateCode')
|
||||
async getTemplate(
|
||||
@Param('templateCode') templateCode: string,
|
||||
): Promise<ApiResponse<Template | null>> {
|
||||
console.log(`🔍 [获取模板] 请求获取模板: ${templateCode}`);
|
||||
console.log(`⏰ 请求时间: ${new Date().toISOString()}`);
|
||||
|
||||
const template = this.tempalte.getTemplate(templateCode);
|
||||
|
||||
if (template) {
|
||||
console.log(`✅ [获取模板] 模板找到: ${template.name || templateCode}`);
|
||||
|
||||
// 安全地输出模板信息,避免循环引用
|
||||
try {
|
||||
const templateInfo = {
|
||||
code: template.code,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
// 只输出基本信息,避免复杂对象
|
||||
};
|
||||
console.log(`📋 模板信息: ${JSON.stringify(templateInfo, null, 2)}`);
|
||||
} catch (jsonError) {
|
||||
console.log(`📋 模板信息: [无法序列化的复杂对象]`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: true,
|
||||
data: template,
|
||||
msg: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`❌ [获取模板] 模板不存在: ${templateCode}`);
|
||||
|
||||
return {
|
||||
status: false,
|
||||
data: null,
|
||||
msg: '模板不存在',
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
async executeTemplate(
|
||||
@Body('imageUrl') imageUrl: string,
|
||||
@Body('templateCode') templateCode: string,
|
||||
): Promise<ApiResponse<string | null>> {
|
||||
const startTime = Date.now();
|
||||
console.log(`🚀 [模板执行] 开始执行模板`);
|
||||
console.log(`📋 模板代码: ${templateCode}`);
|
||||
console.log(`🖼️ 图片URL: ${imageUrl}`);
|
||||
console.log(`⏰ 开始时间: ${new Date().toISOString()}`);
|
||||
|
||||
try {
|
||||
// 验证输入参数
|
||||
if (!templateCode) {
|
||||
console.log(`❌ [模板执行] 参数验证失败: 模板代码为空`);
|
||||
return {
|
||||
status: false,
|
||||
data: null,
|
||||
msg: '模板代码不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
console.log(`❌ [模板执行] 参数验证失败: 图片URL为空`);
|
||||
return {
|
||||
status: false,
|
||||
data: null,
|
||||
msg: '图片URL不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`✅ [模板执行] 参数验证通过,开始执行模板处理`);
|
||||
|
||||
const result = await this.tempalte.executeTemplate(
|
||||
templateCode,
|
||||
imageUrl,
|
||||
);
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
if (result) {
|
||||
console.log(`🎉 [模板执行] 执行成功`);
|
||||
console.log(`📊 执行结果类型: ${typeof result}`);
|
||||
|
||||
// 安全地获取结果长度
|
||||
if (typeof result === 'string') {
|
||||
console.log(`📊 执行结果长度: ${result.length} 字符`);
|
||||
} else if (result && typeof result === 'object') {
|
||||
console.log(`📊 执行结果: [对象类型]`);
|
||||
} else {
|
||||
console.log(`📊 执行结果: ${result}`);
|
||||
}
|
||||
|
||||
console.log(`⏱️ 执行耗时: ${executionTime}ms`);
|
||||
console.log(`✨ 完成时间: ${new Date().toISOString()}`);
|
||||
|
||||
return {
|
||||
status: true,
|
||||
data: result,
|
||||
msg: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`⚠️ [模板执行] 执行完成但结果为空或undefined`);
|
||||
console.log(`📊 结果值: ${result}`);
|
||||
console.log(`⏱️ 执行耗时: ${executionTime}ms`);
|
||||
|
||||
return {
|
||||
status: false,
|
||||
data: null,
|
||||
msg: '执行失败',
|
||||
};
|
||||
} catch (e) {
|
||||
const executionTime = Date.now() - startTime;
|
||||
console.log(`💥 [模板执行] 执行异常`);
|
||||
console.log(`❌ 错误信息: ${e?.message || '未知错误'}`);
|
||||
|
||||
// 安全地输出错误堆栈
|
||||
if (e?.stack) {
|
||||
console.log(`📍 错误堆栈: ${e.stack}`);
|
||||
}
|
||||
|
||||
console.log(`⏱️ 执行耗时: ${executionTime}ms`);
|
||||
console.log(`🔚 异常时间: ${new Date().toISOString()}`);
|
||||
|
||||
return {
|
||||
status: false,
|
||||
data: null,
|
||||
msg: e?.message || '模板执行异常',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TemplateExecutionEntity)
|
||||
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
||||
) {}
|
||||
@Post('callback')
|
||||
async callback(@Body() body: any): Promise<ApiResponse<any>> {
|
||||
console.log(`🚀 [回调] 开始执行回调`);
|
||||
console.log(`📋 回调参数: ${JSON.stringify(body, null, 2)}`);
|
||||
const task_id = body.task_id;
|
||||
if (!task_id) return { status: false, msg: `缺少task_id`, data: null }
|
||||
const res = body.data;
|
||||
let resultUrl = ``
|
||||
if (res.status) {
|
||||
const data = res.data;
|
||||
if (!data) throw new Error(`结果有误`);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
resultUrl = data[0];
|
||||
}
|
||||
resultUrl = data;
|
||||
}
|
||||
// 通过 taskId 查找对应的执行记录并更新状态
|
||||
const execution = await this.executionRepository.findOne({
|
||||
where: { taskId: task_id }
|
||||
});
|
||||
|
||||
if (!execution) {
|
||||
console.error(`未找到 taskId 为 ${task_id} 的执行记录`);
|
||||
return { status: false, msg: `未找到对应的执行记录`, data: null };
|
||||
}
|
||||
|
||||
// 更新执行状态
|
||||
const updateData: Partial<TemplateExecutionEntity> = {
|
||||
completedAt: new Date(),
|
||||
executionDuration: execution.startedAt
|
||||
? Date.now() - execution.startedAt.getTime()
|
||||
: undefined,
|
||||
};
|
||||
|
||||
updateData.executionResult = body;
|
||||
|
||||
if (res.status) {
|
||||
// 成功状态
|
||||
updateData.status = ExecutionStatus.COMPLETED;
|
||||
updateData.progress = 100;
|
||||
updateData.outputUrl = resultUrl;
|
||||
updateData.executionResult = res;
|
||||
} else {
|
||||
// 失败状态
|
||||
updateData.status = ExecutionStatus.FAILED;
|
||||
updateData.errorMessage = res.message || '任务执行失败';
|
||||
updateData.executionResult = res;
|
||||
}
|
||||
|
||||
await this.executionRepository.update(execution.id, updateData);
|
||||
|
||||
return {
|
||||
status: true,
|
||||
data: body,
|
||||
data: resultUrl,
|
||||
msg: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,10 +17,18 @@ import { N8nTemplateFactoryService } from './services/n8n-template-factory.servi
|
||||
import { TemplateController } from './controllers/template.controller';
|
||||
import { PlatformModule } from './platform/platform.module';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule,
|
||||
JwtModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '24h' },
|
||||
}),
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
@@ -46,4 +54,4 @@ import { HttpModule } from '@nestjs/axios';
|
||||
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],
|
||||
exports: [N8nTemplateFactoryService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule { }
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Query,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
@@ -32,10 +34,11 @@ import {
|
||||
TemplateListDto,
|
||||
BatchExecuteDto,
|
||||
} from '../dto/template.dto';
|
||||
import { TemplateExecutionEntity } from '../entities/template-execution.entity';
|
||||
import { TemplateExecutionEntity, ExecutionStatus, ExecutionType } from '../entities/template-execution.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ApiCommonResponses } from '../decorators/api-common-responses.decorator';
|
||||
import { PlatformAuthGuard } from 'src/platform/guards/platform-auth.guard';
|
||||
|
||||
@ApiTags('AI模板系统')
|
||||
@Controller('templates')
|
||||
@@ -47,7 +50,7 @@ export class TemplateController {
|
||||
private readonly templateRepository: Repository<N8nTemplateEntity>,
|
||||
@InjectRepository(TemplateExecutionEntity)
|
||||
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@Post(':templateId/execute')
|
||||
@ApiOperation({
|
||||
@@ -107,6 +110,7 @@ export class TemplateController {
|
||||
}
|
||||
|
||||
@Post('code/:code/execute')
|
||||
@UseGuards(PlatformAuthGuard)
|
||||
@ApiOperation({
|
||||
summary: '通过代码执行模板',
|
||||
description: '根据模板代码执行AI生成任务,支持图片和视频生成',
|
||||
@@ -125,6 +129,7 @@ export class TemplateController {
|
||||
async executeTemplateByCode(
|
||||
@Param('code') code: string,
|
||||
@Body() body: { imageUrl: string },
|
||||
@Request() req
|
||||
) {
|
||||
try {
|
||||
const { imageUrl } = body;
|
||||
@@ -133,21 +138,41 @@ export class TemplateController {
|
||||
throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 首先获取模板配置以确定模板类型
|
||||
const templateConfig = await this.templateFactory.getTemplateByCode(code);
|
||||
if (!templateConfig) {
|
||||
throw new HttpException('Template not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// 通过模板代码创建实例并执行
|
||||
const template = await this.templateFactory.createTemplateByCode(code);
|
||||
const result = await template.execute(imageUrl);
|
||||
const taskId = await template.execute(imageUrl);
|
||||
|
||||
// 将任务保存到 TemplateExecutionEntity
|
||||
const userId = req.user.userId;
|
||||
const execution = this.executionRepository.create({
|
||||
templateId: templateConfig.id,
|
||||
userId,
|
||||
platform: req.user.platform,
|
||||
type: templateConfig.templateType === TemplateType.VIDEO ? ExecutionType.VIDEO : ExecutionType.IMAGE,
|
||||
prompt: '', // 可以从请求参数中获取,如果有的话
|
||||
inputImageUrl: imageUrl,
|
||||
taskId: taskId, // 保存外部系统返回的任务ID,用于回调匹配
|
||||
status: ExecutionStatus.PROCESSING,
|
||||
progress: 0,
|
||||
creditCost: templateConfig.creditCost,
|
||||
startedAt: new Date(),
|
||||
});
|
||||
|
||||
const savedExecution = await this.executionRepository.save(execution);
|
||||
|
||||
// 返回任务id (执行记录的ID)
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
templateCode: template.code,
|
||||
templateName: template.name,
|
||||
inputUrl: imageUrl,
|
||||
outputUrl: result,
|
||||
creditCost: template.creditCost,
|
||||
},
|
||||
data: savedExecution.id,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw new HttpException(
|
||||
error.message || 'Template execution failed',
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -58,6 +58,10 @@ export class TemplateExecutionEntity {
|
||||
@Column({ name: 'user_id' })
|
||||
userId: string;
|
||||
|
||||
/** 任务ID - 外部系统(如N8N)返回的任务标识符,用于回调时匹配任务 */
|
||||
@Column({ name: 'task_id', length: 200, nullable: true })
|
||||
taskId: string;
|
||||
|
||||
/** 执行平台 - 任务发起的平台来源 */
|
||||
@Column({ type: 'enum', enum: PlatformType, enumName: 'PlatformType' })
|
||||
platform: PlatformType;
|
||||
|
||||
40
src/migrations/1756986632000-AddTaskIdToTemplateExecution.ts
Normal file
40
src/migrations/1756986632000-AddTaskIdToTemplateExecution.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 为模板执行表添加 taskId 字段
|
||||
* 用于存储外部系统返回的任务标识符,便于回调时匹配更新任务状态
|
||||
*/
|
||||
export class AddTaskIdToTemplateExecution1756986632000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTaskIdToTemplateExecution1756986632000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 为 template_executions 表添加 task_id 字段
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`template_executions\`
|
||||
ADD COLUMN \`task_id\` VARCHAR(200) NULL
|
||||
COMMENT '外部系统任务ID,用于回调匹配'
|
||||
`);
|
||||
|
||||
// 为 task_id 字段添加索引,提高查询性能
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX \`IDX_template_executions_task_id\`
|
||||
ON \`template_executions\` (\`task_id\`)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// 删除索引
|
||||
await queryRunner.query(`
|
||||
DROP INDEX \`IDX_template_executions_task_id\`
|
||||
ON \`template_executions\`
|
||||
`);
|
||||
|
||||
// 删除字段
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`template_executions\`
|
||||
DROP COLUMN \`task_id\`
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -33,14 +33,7 @@ export abstract class N8nImageGenerateTemplate extends ImageGenerateTemplate {
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.then((res) => {
|
||||
if (res.status) {
|
||||
const data = res.data;
|
||||
if (!data) throw new Error(`结果有误`);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
return data[0];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error(res.msg);
|
||||
});
|
||||
}
|
||||
@@ -87,14 +80,7 @@ export abstract class N8nVideoGenerateTemplate extends VideoGenerateTemplate {
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.then((res) => {
|
||||
if (res.status) {
|
||||
const data = res.data;
|
||||
if (!data) throw new Error(`结果有误`);
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
return data[0];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
if(res.task_id) return res.task_id
|
||||
throw new Error(res.msg);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user