- 新增TemplateExecutionEntity实体替代原有的GenerationTask方式 - 实现模板执行记录的完整生命周期管理,包含状态跟踪和性能指标 - 添加模板管理的完整CRUD操作API(创建、更新、删除、查询) - 更新数据库迁移文件,包含n8n_templates和template_executions表 - 完善模板工厂服务,支持执行记录管理和统计分析 - 重构DynamicN8nImageTemplate和DynamicN8nVideoTemplate类名 - 添加用户相关实体:User、PlatformUser、ExtensionData、UserCredit等 - 实现完整的数据库表结构和索引优化
275 lines
8.5 KiB
TypeScript
275 lines
8.5 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity';
|
|
import { TemplateExecutionEntity, ExecutionStatus } from '../entities/template-execution.entity';
|
|
import {
|
|
DynamicN8nImageTemplate,
|
|
DynamicN8nVideoTemplate
|
|
} from '../templates/n8n-dynamic-template';
|
|
|
|
/**
|
|
* N8n模板工厂服务
|
|
* 负责创建动态模板实例,支持从数据库加载配置
|
|
*/
|
|
@Injectable()
|
|
export class N8nTemplateFactoryService {
|
|
constructor(
|
|
@InjectRepository(N8nTemplateEntity)
|
|
private templateRepository: Repository<N8nTemplateEntity>,
|
|
@InjectRepository(TemplateExecutionEntity)
|
|
private executionRepository: Repository<TemplateExecutionEntity>,
|
|
) { }
|
|
|
|
/**
|
|
* 创建图片模板实例
|
|
* @param templateId 模板ID
|
|
* @returns 初始化后的图片模板实例
|
|
*/
|
|
async createImageTemplate(templateId: number): Promise<DynamicN8nImageTemplate> {
|
|
const instance = new DynamicN8nImageTemplate(templateId, this.templateRepository);
|
|
return await instance.onInit();
|
|
}
|
|
|
|
/**
|
|
* 创建视频模板实例
|
|
* @param templateId 模板ID
|
|
* @returns 初始化后的视频模板实例
|
|
*/
|
|
async createVideoTemplate(templateId: number): Promise<DynamicN8nVideoTemplate> {
|
|
const instance = new DynamicN8nVideoTemplate(templateId, this.templateRepository);
|
|
return await instance.onInit();
|
|
}
|
|
|
|
/**
|
|
* 通过模板代码创建实例
|
|
* @param code 模板代码
|
|
* @returns 对应类型的模板实例
|
|
*/
|
|
async createTemplateByCode(
|
|
code: string
|
|
): Promise<DynamicN8nImageTemplate | DynamicN8nVideoTemplate> {
|
|
const config = await this.templateRepository.findOne({
|
|
where: { code, isActive: true }
|
|
});
|
|
|
|
if (!config) {
|
|
throw new Error(`Template with code ${code} not found or inactive`);
|
|
}
|
|
|
|
if (config.templateType === TemplateType.IMAGE) {
|
|
return this.createImageTemplate(config.id);
|
|
} else {
|
|
return this.createVideoTemplate(config.id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 通用模板创建方法
|
|
* @param templateId 模板ID
|
|
* @returns 对应类型的模板实例
|
|
*/
|
|
async createTemplate(
|
|
templateId: number
|
|
): Promise<DynamicN8nImageTemplate | DynamicN8nVideoTemplate> {
|
|
const config = await this.templateRepository.findOne({
|
|
where: { id: templateId, isActive: true }
|
|
});
|
|
|
|
if (!config) {
|
|
throw new Error(`Template with id ${templateId} not found or inactive`);
|
|
}
|
|
|
|
if (config.templateType === TemplateType.IMAGE) {
|
|
return this.createImageTemplate(templateId);
|
|
} else {
|
|
return this.createVideoTemplate(templateId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取所有可用模板配置
|
|
* @returns 所有激活状态的模板配置
|
|
*/
|
|
async getAllTemplates(): Promise<N8nTemplateEntity[]> {
|
|
return this.templateRepository.find({
|
|
where: { isActive: true },
|
|
order: { sortOrder: 'DESC', createdAt: 'DESC' }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 按类型获取模板配置
|
|
* @param type 模板类型
|
|
* @returns 指定类型的模板配置列表
|
|
*/
|
|
async getTemplatesByType(type: TemplateType): Promise<N8nTemplateEntity[]> {
|
|
return this.templateRepository.find({
|
|
where: { templateType: type, isActive: true },
|
|
order: { sortOrder: 'DESC', createdAt: 'DESC' }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取图片模板配置列表
|
|
* @returns 图片类型的模板配置
|
|
*/
|
|
async getImageTemplates(): Promise<N8nTemplateEntity[]> {
|
|
return this.getTemplatesByType(TemplateType.IMAGE);
|
|
}
|
|
|
|
/**
|
|
* 获取视频模板配置列表
|
|
* @returns 视频类型的模板配置
|
|
*/
|
|
async getVideoTemplates(): Promise<N8nTemplateEntity[]> {
|
|
return this.getTemplatesByType(TemplateType.VIDEO);
|
|
}
|
|
|
|
/**
|
|
* 通过代码获取模板配置
|
|
* @param code 模板代码
|
|
* @returns 模板配置信息
|
|
*/
|
|
async getTemplateByCode(code: string): Promise<N8nTemplateEntity | null> {
|
|
return this.templateRepository.findOne({
|
|
where: { code, isActive: true }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 批量执行模板
|
|
* @param templateId 模板ID
|
|
* @param imageUrls 图片URL数组
|
|
* @returns 执行结果数组
|
|
*/
|
|
async batchExecute(templateId: number, imageUrls: string[]): Promise<string[]> {
|
|
const template = await this.createTemplate(templateId);
|
|
return Promise.all(imageUrls.map(url => template.execute(url)));
|
|
}
|
|
|
|
/**
|
|
* 根据用户偏好推荐模板(示例实现)
|
|
* @param userTags 用户偏好标签
|
|
* @param limit 推荐数量限制
|
|
* @returns 推荐的模板配置
|
|
*/
|
|
async recommendTemplates(userTags: string[], limit: number = 5): Promise<N8nTemplateEntity[]> {
|
|
// 这里可以实现更复杂的推荐算法
|
|
const query = this.templateRepository
|
|
.createQueryBuilder('template')
|
|
.where('template.isActive = :isActive', { isActive: true })
|
|
.orderBy('template.sortOrder', 'DESC')
|
|
.addOrderBy('template.createdAt', 'DESC')
|
|
.limit(limit);
|
|
|
|
// 如果提供了用户标签,可以基于标签匹配进行推荐
|
|
if (userTags.length > 0) {
|
|
query.andWhere(
|
|
'JSON_OVERLAPS(template.tags, :userTags)',
|
|
{ userTags: JSON.stringify(userTags) }
|
|
);
|
|
}
|
|
|
|
return query.getMany();
|
|
}
|
|
|
|
/**
|
|
* 创建执行记录
|
|
* @param data 执行记录数据
|
|
* @returns 创建的执行记录
|
|
*/
|
|
async createExecution(data: Partial<TemplateExecutionEntity>): Promise<TemplateExecutionEntity> {
|
|
const execution = this.executionRepository.create(data);
|
|
return this.executionRepository.save(execution);
|
|
}
|
|
|
|
/**
|
|
* 更新执行记录
|
|
* @param id 执行记录ID
|
|
* @param data 更新数据
|
|
* @returns 更新后的执行记录
|
|
*/
|
|
async updateExecution(id: number, data: Partial<TemplateExecutionEntity>): Promise<TemplateExecutionEntity> {
|
|
await this.executionRepository.update(id, data);
|
|
const item = await this.executionRepository.findOne({ where: { id } });
|
|
if (item) return item;
|
|
throw new Error(`not found ${id} from template execution table`)
|
|
}
|
|
|
|
/**
|
|
* 获取用户执行记录
|
|
* @param userId 用户ID
|
|
* @param limit 记录数量限制
|
|
* @returns 用户的执行记录列表
|
|
*/
|
|
async getUserExecutions(userId: string, limit = 20): Promise<TemplateExecutionEntity[]> {
|
|
return this.executionRepository.find({
|
|
where: { userId },
|
|
relations: ['template'],
|
|
order: { createdAt: 'DESC' },
|
|
take: limit
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取模板执行统计
|
|
* @param templateId 模板ID
|
|
* @returns 执行统计信息
|
|
*/
|
|
async getTemplateExecutionStats(templateId: number): Promise<{
|
|
totalUsage: number;
|
|
successRate: number;
|
|
averageExecutionTime: number;
|
|
totalCreditCost: number;
|
|
}> {
|
|
const stats = await this.executionRepository
|
|
.createQueryBuilder('execution')
|
|
.select('execution.status', 'status')
|
|
.addSelect('COUNT(*)', 'count')
|
|
.addSelect('AVG(execution.executionDuration)', 'avgDuration')
|
|
.addSelect('SUM(execution.creditCost)', 'totalCredit')
|
|
.where('execution.templateId = :templateId', { templateId })
|
|
.groupBy('execution.status')
|
|
.getRawMany();
|
|
|
|
const total = stats.reduce((sum, stat) => sum + parseInt(stat.count), 0);
|
|
const completed = stats.find(stat => stat.status === 'completed');
|
|
const successRate = total > 0 ? (parseInt(completed?.count || '0') / total) : 1.0;
|
|
const avgTime = completed ? parseFloat(completed.avgDuration || '0') : 0;
|
|
const totalCredit = stats.reduce((sum, stat) => sum + parseInt(stat.totalCredit || '0'), 0);
|
|
|
|
return {
|
|
totalUsage: total,
|
|
successRate,
|
|
averageExecutionTime: avgTime,
|
|
totalCreditCost: totalCredit
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 获取用户积分消耗统计
|
|
* @param userId 用户ID
|
|
* @returns 积分消耗统计
|
|
*/
|
|
async getUserCreditStats(userId: string): Promise<{
|
|
totalCreditSpent: number;
|
|
totalExecutions: number;
|
|
averageCreditPerExecution: number;
|
|
}> {
|
|
const stats = await this.executionRepository
|
|
.createQueryBuilder('execution')
|
|
.select('COUNT(*)', 'totalExecutions')
|
|
.addSelect('SUM(execution.creditCost)', 'totalCreditSpent')
|
|
.addSelect('AVG(execution.creditCost)', 'avgCreditPerExecution')
|
|
.where('execution.userId = :userId', { userId })
|
|
.andWhere('execution.status = :status', { status: ExecutionStatus.COMPLETED })
|
|
.getRawOne();
|
|
|
|
return {
|
|
totalCreditSpent: parseInt(stats.totalCreditSpent || '0'),
|
|
totalExecutions: parseInt(stats.totalExecutions || '0'),
|
|
averageCreditPerExecution: parseFloat(stats.avgCreditPerExecution || '0')
|
|
};
|
|
}
|
|
} |