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 { Body, Controller, Post } from '@nestjs/common';
|
||||||
import { TemplateService } from './templates/index';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Template } from './templates/types';
|
import { Repository } from 'typeorm';
|
||||||
|
import { TemplateExecutionEntity, ExecutionStatus } from './entities/template-execution.entity';
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
status: boolean | string;
|
status: boolean | string;
|
||||||
data: T;
|
data: T;
|
||||||
msg: string;
|
msg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('api/v1/templates')
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly tempalte: TemplateService) {}
|
constructor(
|
||||||
|
@InjectRepository(TemplateExecutionEntity)
|
||||||
@Get()
|
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
||||||
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 || '模板执行异常',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('callback')
|
@Post('callback')
|
||||||
async callback(@Body() body: any): Promise<ApiResponse<any>> {
|
async callback(@Body() body: any): Promise<ApiResponse<any>> {
|
||||||
console.log(`🚀 [回调] 开始执行回调`);
|
console.log(`🚀 [回调] 开始执行回调`);
|
||||||
console.log(`📋 回调参数: ${JSON.stringify(body, null, 2)}`);
|
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 {
|
return {
|
||||||
status: true,
|
status: true,
|
||||||
data: body,
|
data: resultUrl,
|
||||||
msg: 'success',
|
msg: 'success',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,18 @@ import { N8nTemplateFactoryService } from './services/n8n-template-factory.servi
|
|||||||
import { TemplateController } from './controllers/template.controller';
|
import { TemplateController } from './controllers/template.controller';
|
||||||
import { PlatformModule } from './platform/platform.module';
|
import { PlatformModule } from './platform/platform.module';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
HttpModule,
|
HttpModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
secret: config.get('JWT_SECRET'),
|
||||||
|
signOptions: { expiresIn: '24h' },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
envFilePath: ['.env.local', '.env'],
|
envFilePath: ['.env.local', '.env'],
|
||||||
@@ -46,4 +54,4 @@ import { HttpModule } from '@nestjs/axios';
|
|||||||
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],
|
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],
|
||||||
exports: [N8nTemplateFactoryService],
|
exports: [N8nTemplateFactoryService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule { }
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@@ -32,10 +34,11 @@ import {
|
|||||||
TemplateListDto,
|
TemplateListDto,
|
||||||
BatchExecuteDto,
|
BatchExecuteDto,
|
||||||
} from '../dto/template.dto';
|
} 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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { ApiCommonResponses } from '../decorators/api-common-responses.decorator';
|
import { ApiCommonResponses } from '../decorators/api-common-responses.decorator';
|
||||||
|
import { PlatformAuthGuard } from 'src/platform/guards/platform-auth.guard';
|
||||||
|
|
||||||
@ApiTags('AI模板系统')
|
@ApiTags('AI模板系统')
|
||||||
@Controller('templates')
|
@Controller('templates')
|
||||||
@@ -47,7 +50,7 @@ export class TemplateController {
|
|||||||
private readonly templateRepository: Repository<N8nTemplateEntity>,
|
private readonly templateRepository: Repository<N8nTemplateEntity>,
|
||||||
@InjectRepository(TemplateExecutionEntity)
|
@InjectRepository(TemplateExecutionEntity)
|
||||||
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
private readonly executionRepository: Repository<TemplateExecutionEntity>,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post(':templateId/execute')
|
@Post(':templateId/execute')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -107,6 +110,7 @@ export class TemplateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('code/:code/execute')
|
@Post('code/:code/execute')
|
||||||
|
@UseGuards(PlatformAuthGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: '通过代码执行模板',
|
summary: '通过代码执行模板',
|
||||||
description: '根据模板代码执行AI生成任务,支持图片和视频生成',
|
description: '根据模板代码执行AI生成任务,支持图片和视频生成',
|
||||||
@@ -125,6 +129,7 @@ export class TemplateController {
|
|||||||
async executeTemplateByCode(
|
async executeTemplateByCode(
|
||||||
@Param('code') code: string,
|
@Param('code') code: string,
|
||||||
@Body() body: { imageUrl: string },
|
@Body() body: { imageUrl: string },
|
||||||
|
@Request() req
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { imageUrl } = body;
|
const { imageUrl } = body;
|
||||||
@@ -133,21 +138,41 @@ export class TemplateController {
|
|||||||
throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST);
|
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 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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: savedExecution.id,
|
||||||
templateCode: template.code,
|
|
||||||
templateName: template.name,
|
|
||||||
inputUrl: imageUrl,
|
|
||||||
outputUrl: result,
|
|
||||||
creditCost: template.creditCost,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
error.message || 'Template execution failed',
|
error.message || 'Template execution failed',
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export class TemplateExecutionEntity {
|
|||||||
@Column({ name: 'user_id' })
|
@Column({ name: 'user_id' })
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
|
/** 任务ID - 外部系统(如N8N)返回的任务标识符,用于回调时匹配任务 */
|
||||||
|
@Column({ name: 'task_id', length: 200, nullable: true })
|
||||||
|
taskId: string;
|
||||||
|
|
||||||
/** 执行平台 - 任务发起的平台来源 */
|
/** 执行平台 - 任务发起的平台来源 */
|
||||||
@Column({ type: 'enum', enum: PlatformType, enumName: 'PlatformType' })
|
@Column({ type: 'enum', enum: PlatformType, enumName: 'PlatformType' })
|
||||||
platform: 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) => res.data)
|
||||||
.then((res) => {
|
.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);
|
throw new Error(res.msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -87,14 +80,7 @@ export abstract class N8nVideoGenerateTemplate extends VideoGenerateTemplate {
|
|||||||
})
|
})
|
||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status) {
|
if(res.task_id) return res.task_id
|
||||||
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);
|
throw new Error(res.msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user