feat: 完善模板执行系统和任务状态管理

- 为 TemplateExecutionEntity 添加 taskId 字段用于任务追踪
- 完善 executeTemplateByCode 接口,保存执行记录到数据库
- 重构 app.controller.ts callback 逻辑,支持任务状态回调更新
- 添加平台认证守卫和用户信息获取
- 新增 AddTaskIdToTemplateExecution migration
- 修复模板类型映射和执行状态管理
- 优化 N8N 模板返回 taskId 而非直接结果
This commit is contained in:
imeepos
2025-09-04 19:56:02 +08:00
parent 42fa667d3c
commit 0e33292d7a
6 changed files with 147 additions and 193 deletions

View File

@@ -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',
};
}

View File

@@ -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 { }

View File

@@ -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,

View File

@@ -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;

View 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\`
`);
}
}

View File

@@ -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);
});
}